# Media Upload

> |

- **Type:** Skill
- **Install:** `agentstack add skill-dapi-claude-code-marketplace-media-upload`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [dapi](https://agentstack.voostack.com/s/dapi)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [dapi](https://github.com/dapi)
- **Source:** https://github.com/dapi/claude-code-marketplace/tree/master/media-upload/skills/media-upload

## Install

```sh
agentstack add skill-dapi-claude-code-marketplace-media-upload
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Media Upload Skill

Загрузка изображений и медиафайлов в S3-совместимое хранилище через `mc` (MinIO Client) с получением публичной ссылки.

## ⚠️ CRITICAL: Приоритет URL

**ОБЯЗАТЕЛЬНО** после загрузки файла:

1. Если в конфиге есть `public_url` и `url_mode` НЕ равен `"presigned"`:
   - **СРАЗУ** возвращай короткий CDN URL: `${public_url}/${remote_path}`
   - **НЕ генерируй** presigned URL

2. Только если `url_mode: "presigned"` или `public_url` отсутствует:
   - Генерируй presigned URL через `mc share download`

**Пример правильного поведения:**
```
Конфиг: { "public_url": "https://cdn.example.com", "url_mode": "public" }
Путь: 2026/02/03/screenshot.png
→ Возвращай: https://cdn.example.com/2026/02/03/screenshot.png
→ НЕ возвращай: https://s3...?X-Amz-Algorithm=...
```

## Поддерживаемые форматы

| Тип | Расширения | MIME-type |
|-----|------------|-----------|
| Изображения | png, jpg, jpeg, gif, webp, svg | image/* |
| Документы | pdf | application/pdf |
| Видео | mp4, webm (будущее) | video/* |

## Приоритет определения источника файла

1. **Явный путь** — пользователь указал `/path/to/file.png` в запросе
2. **Playwright MCP** — последний результат `browser_take_screenshot` в текущей сессии
3. **Glob паттерн** — "загрузи все png из ./screenshots/"
4. **Спросить пользователя** — если ничего не найдено

### Интеграция с Playwright MCP

**ВАЖНО**: Этот скилл должен активироваться АВТОМАТИЧЕСКИ после создания скриншота через Playwright MCP.

#### Паттерны запросов пользователя

Когда пользователь просит:
- "открой сайт X и сделай скриншот"
- "сделай скриншот страницы Y"
- "take a screenshot of Z"

**Алгоритм**:
1. Загрузить инструменты Playwright MCP через ToolSearch
2. Открыть браузер и перейти на страницу (`browser_navigate`)
3. Сделать скриншот (`browser_take_screenshot`)
4. **АВТОМАТИЧЕСКИ** загрузить скриншот в S3 (этот скилл)
5. Вернуть пользователю публичную ссылку

#### Формат ответа Playwright

Playwright MCP при вызове `browser_take_screenshot` возвращает:
```
Took the viewport screenshot and saved it as /tmp/page-2024-01-31-143052.png
```

Парси этот формат регулярным выражением:
```bash
SCREENSHOT_PATH=$(echo "$PLAYWRIGHT_OUTPUT" | grep -oP 'saved it as \K/[^\s]+\.png')
```

#### Пример полного флоу

```
User: "открой kiiiosk.store и сделай скриншот"

1. ToolSearch: load playwright tools
2. browser_navigate: https://kiiiosk.store
3. browser_take_screenshot → /tmp/page-2026-02-01-123456.png
4. media-upload skill activates automatically
5. MC_REGION=ru-3 mc cp --insecure /tmp/page-... screenshots/claude-screenshots/2026/02/01/...
6. Return: "✅ Screenshot uploaded: https://..."
```

## Конфигурация

### Файл конфигурации

**Путь**: `~/.config/claude-code/media-upload.json`

**С CDN (рекомендуется):**
```json
{
  "mc_path": "screenshots/claude-screenshots",
  "public_url": "https://cdn.example.com",
  "url_mode": "public",
  "organize_by": "date",
  "max_file_size_mb": 100
}
```

**Без CDN (только presigned):**
```json
{
  "mc_path": "screenshots/claude-screenshots",
  "url_mode": "presigned",
  "presigned_expire": "168h",
  "organize_by": "date",
  "max_file_size_mb": 100
}
```

**Примечание**: Если есть `public_url`, используй `url_mode: "public"` для коротких постоянных ссылок.

### Режимы URL (`url_mode`)

| Режим | Описание |
|-------|----------|
| `public` | Использовать публичный URL (требует CDN домен, не S3 API) |
| `presigned` | **Рекомендуется для Selectel**. Генерировать presigned URL через `mc share` |
| `auto` | Проверить публичный доступ, fallback на presigned |

**`presigned_expire`** — время жизни presigned URL (по умолчанию `168h` = 7 дней, **максимум** для mc)

**⚠️ ВАЖНО для Selectel Cloud Storage:**
- S3 API Selectel **НЕ поддерживает анонимный доступ** — все запросы должны быть подписаны
- Настройка "Публичный" в консоли Selectel работает **только через домен `selstorage.ru`**, не через S3 API
- Для S3-совместимого доступа используйте `presigned` режим

### Переменные окружения (высший приоритет)

```bash
MEDIA_UPLOAD_MC_PATH=screenshots/claude-screenshots
MEDIA_UPLOAD_PUBLIC_URL=https://s3.ru-3.storage.selcloud.ru/claude-screenshots
MEDIA_UPLOAD_ORGANIZE_BY=date
MEDIA_UPLOAD_MAX_FILE_SIZE_MB=100
MEDIA_UPLOAD_HISTORY_FILE=~/.media-upload-history.json
MEDIA_UPLOAD_URL_MODE=auto
MEDIA_UPLOAD_PRESIGNED_EXPIRE=168h
```

**Приоритет конфигурации**: Environment variables > JSON config > Default values

### Организация файлов в S3

```bash
# organize_by: "date" (по умолчанию)
screenshots/2024/01/31/screenshot-2024-01-31-143052.png

# organize_by: "type"
screenshots/images/screenshot-2024-01-31-143052.png
screenshots/documents/report.pdf

# organize_by: "flat"
screenshots/screenshot-2024-01-31-143052.png
```

## Security

### Credentials
- Credentials хранятся **только** в `mc alias` (настраивается через `mc alias set`)
- **Никогда** не хранить access/secret keys в `media-upload.json`
- Минимальные IAM права для mc alias: `PutObject` на конкретный bucket

### Валидация файлов
- Проверка magic bytes файла (соответствие расширению)
- Санитизация имени файла: удаление `../`, замена спецсимволов
- Пример: `my screenshot (1).png` → `my-screenshot-1.png`

### Права доступа
- Config файлы создаются с правами 600 (только владелец)

## Лимиты размера файлов

| Порог | Действие |
|-------|----------|
| > 10 MB | Warning: показать размер |
| > 50 MB | Confirmation: запросить подтверждение |
| > 100 MB | Block: отказать (настраивается в конфигурации) |

## mc CLI Contract

### Selectel Cloud Storage: Обязательные настройки

**⚠️ КРИТИЧЕСКИ ВАЖНО для Selectel:**

MinIO Client по умолчанию использует `eu-central-1` в подписи, а Selectel требует `ru-3`.

**Вариант 1: Добавить регион в конфиг mc (рекомендуется)**
```bash
# Добавить регион в существующий alias через jq
jq '.aliases.screenshots.region = "ru-3"' ~/.mc/config.json > /tmp/mc-config.json && \
  mv /tmp/mc-config.json ~/.mc/config.json
```

**Вариант 2: Через переменную окружения**
```bash
export MC_REGION=ru-3
mc cp LOCAL_FILE ALIAS/BUCKET/PATH
```

**SSL проблемы**: Если есть ошибки SSL certificate, использовать `--insecure`:
```bash
mc cp --insecure LOCAL_FILE ALIAS/BUCKET/PATH
```

### Используемые команды
```bash
# Проверка алиаса
mc alias list | grep ALIAS_NAME

# Загрузка файла (Selectel)
MC_REGION=ru-3 mc cp LOCAL_FILE ALIAS/BUCKET/PATH

# Загрузка файла (Selectel с SSL проблемами)
MC_REGION=ru-3 mc cp --insecure LOCAL_FILE ALIAS/BUCKET/PATH

# Проверка соединения (Setup Wizard)
MC_REGION=ru-3 mc ls ALIAS/BUCKET --limit 1
```

### Exit codes
- `0` — успех
- `1` — ошибка (детали в stderr)

## Алгоритм загрузки

### Шаг 1: Проверка конфигурации

```bash
# Проверить наличие конфигурации
CONFIG_FILE="$HOME/.config/claude-code/media-upload.json"
if [[ ! -f "$CONFIG_FILE" ]]; then
  # Запустить Setup Wizard
fi
```

### Шаг 2: Чтение конфигурации

```bash
# Проверить наличие jq
if ! command -v jq &> /dev/null; then
  echo "❌ jq не установлен (требуется для чтения конфигурации)"
  echo "Установка: brew install jq / apt install jq"
  exit 1
fi

# Проверить валидность JSON
if ! jq empty "$CONFIG_FILE" 2>/dev/null; then
  echo "❌ Невалидный JSON в $CONFIG_FILE"
  exit 1
fi

# Приоритет: ENV > JSON > defaults
MC_PATH="${MEDIA_UPLOAD_MC_PATH:-$(jq -r '.mc_path // empty' "$CONFIG_FILE")}"
PUBLIC_URL="${MEDIA_UPLOAD_PUBLIC_URL:-$(jq -r '.public_url // empty' "$CONFIG_FILE")}"
ORGANIZE_BY="${MEDIA_UPLOAD_ORGANIZE_BY:-$(jq -r '.organize_by // "date"' "$CONFIG_FILE")}"
MAX_SIZE="${MEDIA_UPLOAD_MAX_FILE_SIZE_MB:-$(jq -r '.max_file_size_mb // 100' "$CONFIG_FILE")}"

# Проверить обязательные поля
if [[ -z "$MC_PATH" ]]; then
  echo "❌ mc_path не задан в конфигурации"
  exit 1
fi
# public_url необязателен для режима presigned
```

### Шаг 3: Валидация файла

```bash
# Проверить существование и доступность
if [[ ! -f "$FILE" ]] || [[ ! -r "$FILE" ]]; then
  echo "❌ Файл не найден или недоступен для чтения: $FILE"
  exit 1
fi

# Проверить пустой файл
if [[ ! -s "$FILE" ]]; then
  echo "❌ Файл пустой, загрузка отменена"
  exit 1
fi

# Получить размер файла (кроссплатформенно)
if ! SIZE_BYTES=$(stat -f%z "$FILE" 2>/dev/null || stat -c%s "$FILE" 2>/dev/null); then
  echo "❌ Не удалось получить размер файла: $FILE"
  exit 1
fi
if ! [[ "$SIZE_BYTES" =~ ^[0-9]+$ ]]; then
  echo "❌ Ошибка определения размера файла: $FILE"
  exit 1
fi
SIZE_MB=$(( SIZE_BYTES / 1048576 ))

if [[ $SIZE_MB -gt $MAX_SIZE ]]; then
  echo "❌ Файл слишком большой: ${SIZE_MB}MB > ${MAX_SIZE}MB"
  exit 1
fi
```

### Шаг 4: Санитизация имени файла

```bash
sanitize_filename() {
  local filename="$1"
  # Удалить путь, оставить только имя
  filename="${filename##*/}"
  # Заменить пробелы и спецсимволы на дефисы
  filename=$(echo "$filename" | sed 's/[^a-zA-Z0-9._-]/-/g' | sed 's/--*/-/g' | sed 's/-$//')
  echo "$filename"
}
```

### Шаг 5: Формирование пути в S3

```bash
EXT="${FILE##*.}"
DATE_PATH=$(date +%Y/%m/%d)
SANITIZED_NAME=$(sanitize_filename "$FILE")

case "$ORGANIZE_BY" in
  date) REMOTE_PATH="${DATE_PATH}/${SANITIZED_NAME}" ;;
  type)
    # Определить тип по расширению
    case "${EXT,,}" in
      pdf) TYPE_DIR="documents" ;;
      png|jpg|jpeg|gif|webp|svg) TYPE_DIR="images" ;;
      mp4|webm) TYPE_DIR="videos" ;;
      *) TYPE_DIR="other" ;;
    esac
    REMOTE_PATH="${TYPE_DIR}/${SANITIZED_NAME}"
    ;;
  flat) REMOTE_PATH="${SANITIZED_NAME}" ;;
esac
```

### Шаг 6: Загрузка через mc

```bash
# Проверить наличие региона в конфиге mc
MC_ALIAS=$(echo "$MC_PATH" | cut -d'/' -f1)
MC_HAS_REGION=$(jq -r ".aliases.${MC_ALIAS}.region // empty" ~/.mc/config.json 2>/dev/null)

# Если региона нет в конфиге, попробовать добавить (для Selectel)
if [[ -z "$MC_HAS_REGION" ]] && [[ "$MC_ALIAS" == "screenshots" ]]; then
  # Добавить регион ru-3 для Selectel
  jq ".aliases.${MC_ALIAS}.region = \"ru-3\"" ~/.mc/config.json > /tmp/mc-config.json && \
    mv /tmp/mc-config.json ~/.mc/config.json
  echo "ℹ️ Добавлен регион ru-3 в конфиг mc для Selectel"
fi

# Захватить stderr для диагностики
MC_OUTPUT=$(mc cp "$FILE" "${MC_PATH}/${REMOTE_PATH}" 2>&1)
MC_EXIT=$?

# Если ошибка региона - попробовать с MC_REGION
if [[ $MC_EXIT -ne 0 ]] && echo "$MC_OUTPUT" | grep -q "region.*wrong"; then
  echo "ℹ️ Повторная попытка с MC_REGION=ru-3"
  MC_OUTPUT=$(MC_REGION=ru-3 mc cp "$FILE" "${MC_PATH}/${REMOTE_PATH}" 2>&1)
  MC_EXIT=$?
fi

# Если SSL ошибка - попробовать с --insecure
if [[ $MC_EXIT -ne 0 ]] && echo "$MC_OUTPUT" | grep -qi "ssl\|certificate"; then
  echo "ℹ️ Повторная попытка с --insecure"
  MC_OUTPUT=$(mc cp --insecure "$FILE" "${MC_PATH}/${REMOTE_PATH}" 2>&1)
  MC_EXIT=$?
fi

if [[ $MC_EXIT -ne 0 ]]; then
  echo "❌ Ошибка загрузки файла: $FILE"
  echo ""
  echo "Детали ошибки:"
  echo "$MC_OUTPUT"
  exit 1
fi
```

### Шаг 6.1: Генерация URL

```bash
URL_MODE="${MEDIA_UPLOAD_URL_MODE:-$(jq -r '.url_mode // "auto"' "$CONFIG_FILE")}"
PRESIGNED_EXPIRE="${MEDIA_UPLOAD_PRESIGNED_EXPIRE:-$(jq -r '.presigned_expire // "168h"' "$CONFIG_FILE")}"

generate_url() {
  local remote_path="$1"
  local public_url_full="${PUBLIC_URL}/${remote_path}"

  case "$URL_MODE" in
    public)
      echo "$public_url_full"
      ;;
    presigned)
      # Генерировать presigned URL
      mc share download --expire="$PRESIGNED_EXPIRE" "${MC_PATH}/${remote_path}" 2>&1 | \
        grep "^Share:" | cut -d' ' -f2
      ;;
    auto|*)
      # Проверить публичный доступ
      HTTP_CODE=$(curl -sI -o /dev/null -w "%{http_code}" "$public_url_full" 2>/dev/null || echo "000")

      if [[ "$HTTP_CODE" == "200" ]]; then
        echo "$public_url_full"
      else
        # Fallback на presigned
        mc share download --expire="$PRESIGNED_EXPIRE" "${MC_PATH}/${remote_path}" 2>&1 | \
          grep "^Share:" | cut -d' ' -f2
      fi
      ;;
  esac
}

PUBLIC_URL_FULL=$(generate_url "$REMOTE_PATH")
```

### Шаг 7: Запись в историю

```bash
# Получить путь к файлу истории (ENV > JSON config > default)
HISTORY_FILE="${MEDIA_UPLOAD_HISTORY_FILE:-$(jq -r '.history_file // empty' "$CONFIG_FILE" 2>/dev/null | sed "s|~|$HOME|")}"
HISTORY_FILE="${HISTORY_FILE:-$HOME/.media-upload-history.json}"

# Создать файл если не существует
if [[ ! -f "$HISTORY_FILE" ]]; then
  echo '{"uploads":[]}' > "$HISTORY_FILE"
  chmod 600 "$HISTORY_FILE"
fi

# Добавить запись (через jq)
# SIZE_BYTES определен в Шаге 3
if ! jq --arg file "$FILE" \
       --arg url "$PUBLIC_URL_FULL" \
       --arg size "$SIZE_BYTES" \
       --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
       '.uploads += [{"file":$file,"url":$url,"size":($size|tonumber),"timestamp":$ts}]' \
       "$HISTORY_FILE" > "${HISTORY_FILE}.tmp"; then
  echo "⚠️ Загрузка успешна, но запись в историю не удалась"
  rm -f "${HISTORY_FILE}.tmp"
elif ! mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE"; then
  echo "⚠️ Не удалось сохранить историю: ошибка перемещения файла"
  rm -f "${HISTORY_FILE}.tmp"
fi
```

## Setup Wizard (первый запуск)

**Условие запуска**:
- Отсутствует файл `~/.config/claude-code/media-upload.json`
- ИЛИ отсутствуют обязательные поля: `mc_path`, `public_url`

### Процесс

1. Проверить установку mc:
```bash
if ! command -v mc &> /dev/null; then
  echo "❌ mc не установлен"
  echo "Установка:"
  echo "  macOS: brew install minio-mc"
  echo "  Linux: apt install minio-mc"
  exit 1
fi
echo "✅ mc is installed"
```

2. Запросить параметры через AskUserQuestion:
- `mc_path` (alias/bucket): например `screenshots/screenshots`
- `public_url`: например `https://cdn.example.com/screenshots`

3. Проверить соединение:
```bash
mc ls "${MC_PATH}" --limit 1
if [[ $? -ne 0 ]]; then
  echo "❌ Не удалось подключиться к ${MC_PATH}"
  echo "Проверьте настройку alias: mc alias set ..."
  exit 1
fi
echo "✅ Bucket accessible"
```

4. Сохранить конфигурацию:
```bash
mkdir -p ~/.config/claude-code
cat > ~/.config/claude-code/media-upload.json  100MB limit)
```

## История загрузок

**Файл**: `~/.media-upload-history.json`
```json
{
  "uploads": [
    {
      "file": "/tmp/page-2024-01-31-143052.png",
      "url": "https://cdn.example.com/screenshots/2024/01/31/screenshot-2024-01-31-143052.png",
      "size": 245678,
      "timestamp": "2024-01-31T14:30:52Z"
    }
  ]
}
```

**Просмотр истории**:
```bash
jq '.uploads[-10:]' ~/.media-upload-history.json
```

## Выход после успешной загрузки

### Одиночный файл:

**Публичный URL** (короткий, постоянный):
```
✅ Image uploaded!

 URL: https://s3.example.com/bucket/2024/01/31/screenshot.png
 Markdown: 
 Size: 245 KB
```

**Presigned URL** (длинный, временный):
```
✅ Image uploaded!

 URL: https://s3.example.com/bucket/2024/01/31/screenshot.png?X-Amz-...
⏰ Expires: 7 days
 Markdown: 
 Size: 245 KB

 Tip: Настройте публичный доступ к bucket для коротких постоянных ссылок
```

### Batch:
```
✅ 5 images uploaded!

| File | URL | Size |
|------|-----|------|
| screenshot-1.png | https://cdn... | 120 KB |
| screenshot-2.png | https://cdn... | 245 KB |
| ... | ... | ... |

Total: 1.2 MB

 Markdown (all):

```

## Edge Cases

| Ситуация | Действие |
|----------|----------|
| `mc` не установлен | Показать `brew install minio-mc` / `apt install minio-mc` |
| Alias не настроен | Показать `mc alias set screenshots ...` |
| Файл не найден | Спросить путь явно |
| Неподдерживаемый формат | Предупредить, но загрузить если попросят |
| Файл слишком большой | Показать размер, запросить подтверждение (>50MB) или отказать (>100MB) |
| Нет Playwright скриншотов | Спросить путь явно |
| Ошибка загрузки | Показать stderr от mc |
| Спецсимволы в имени | Санитизировать: `my file (1).png` → `my-file-1.png` |
| Пустой файл (0 байт) | Показать ошибку: "Файл пустой, загрузка отменена" |
| Bucket приватный (403) | В режиме `auto` — fallback на presigned URL |
| Presigned URL слишком длинный | Рекомендовать настроить публичный bucket |
| SSL ошибка при проверке | Использовать `curl -k` или сразу presigned |
| Region mismatch (Selectel) | Установить `MC_REGION=ru-3` перед командами mc |
| SSL certificate verify failed | Использовать `mc cp --insecure` или `aws --no-verify-ssl` |

## Рекомендации по нас

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [dapi](https://github.com/dapi)
- **Source:** [dapi/claude-code-marketplace](https://github.com/dapi/claude-code-marketplace)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-dapi-claude-code-marketplace-media-upload
- Seller: https://agentstack.voostack.com/s/dapi
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
