From media-upload
Uploads screenshots, images, PDFs, and media to S3-compatible storage via MinIO Client, generates public/presigned URLs, supports batch uploads and history listing. Auto-triggers after Playwright screenshots.
npx claudepluginhub dapi/claude-code-marketplace --plugin media-uploadThis skill is limited to using the following tools:
Загрузка изображений и медиафайлов в S3-совместимое хранилище через `mc` (MinIO Client) с получением публичной ссылки.
Guides file storage and CDN setup with object storage (S3, GCS, Azure Blob), presigned URLs, image/video processing pipelines, lifecycle policies, cost optimization, and backups.
Uploads files to Cloudflare R2, AWS S3, or S3-compatible storage via CLI and generates configurable presigned download URLs for secure sharing.
Provides rclone setup, configuration, and commands to upload, sync, list, and manage files in AWS S3, Cloudflare R2, Backblaze B2, Google Drive, Dropbox.
Share bugs, ideas, or general feedback.
Загрузка изображений и медиафайлов в S3-совместимое хранилище через mc (MinIO Client) с получением публичной ссылки.
ОБЯЗАТЕЛЬНО после загрузки файла:
Если в конфиге есть public_url и url_mode НЕ равен "presigned":
${public_url}/${remote_path}Только если url_mode: "presigned" или public_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/* |
| Документы | application/pdf | |
| Видео | mp4, webm (будущее) | video/* |
/path/to/file.png в запросеbrowser_take_screenshot в текущей сессииВАЖНО: Этот скилл должен активироваться АВТОМАТИЧЕСКИ после создания скриншота через Playwright MCP.
Когда пользователь просит:
Алгоритм:
browser_navigate)browser_take_screenshot)Playwright MCP при вызове browser_take_screenshot возвращает:
Took the viewport screenshot and saved it as /tmp/page-2024-01-31-143052.png
Парси этот формат регулярным выражением:
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 (рекомендуется):
{
"mc_path": "screenshots/claude-screenshots",
"public_url": "https://cdn.example.com",
"url_mode": "public",
"organize_by": "date",
"max_file_size_mb": 100
}
Без CDN (только presigned):
{
"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_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:
selstorage.ru, не через S3 APIpresigned режим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
# 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
mc alias (настраивается через mc alias set)media-upload.jsonPutObject на конкретный bucket../, замена спецсимволовmy screenshot (1).png → my-screenshot-1.png| Порог | Действие |
|---|---|
| > 10 MB | Warning: показать размер |
| > 50 MB | Confirmation: запросить подтверждение |
| > 100 MB | Block: отказать (настраивается в конфигурации) |
⚠️ КРИТИЧЕСКИ ВАЖНО для Selectel:
MinIO Client по умолчанию использует eu-central-1 в подписи, а Selectel требует ru-3.
Вариант 1: Добавить регион в конфиг mc (рекомендуется)
# Добавить регион в существующий alias через jq
jq '.aliases.screenshots.region = "ru-3"' ~/.mc/config.json > /tmp/mc-config.json && \
mv /tmp/mc-config.json ~/.mc/config.json
Вариант 2: Через переменную окружения
export MC_REGION=ru-3
mc cp LOCAL_FILE ALIAS/BUCKET/PATH
SSL проблемы: Если есть ошибки SSL certificate, использовать --insecure:
mc cp --insecure LOCAL_FILE ALIAS/BUCKET/PATH
# Проверка алиаса
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
0 — успех1 — ошибка (детали в stderr)# Проверить наличие конфигурации
CONFIG_FILE="$HOME/.config/claude-code/media-upload.json"
if [[ ! -f "$CONFIG_FILE" ]]; then
# Запустить Setup Wizard
fi
# Проверить наличие 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
# Проверить существование и доступность
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
sanitize_filename() {
local filename="$1"
# Удалить путь, оставить только имя
filename="${filename##*/}"
# Заменить пробелы и спецсимволы на дефисы
filename=$(echo "$filename" | sed 's/[^a-zA-Z0-9._-]/-/g' | sed 's/--*/-/g' | sed 's/-$//')
echo "$filename"
}
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
# Проверить наличие региона в конфиге 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
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")
# Получить путь к файлу истории (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
Условие запуска:
~/.config/claude-code/media-upload.jsonmc_path, public_urlif ! 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"
mc_path (alias/bucket): например screenshots/screenshotspublic_url: например https://cdn.example.com/screenshotsmc ls "${MC_PATH}" --limit 1
if [[ $? -ne 0 ]]; then
echo "❌ Не удалось подключиться к ${MC_PATH}"
echo "Проверьте настройку alias: mc alias set ..."
exit 1
fi
echo "✅ Bucket accessible"
mkdir -p ~/.config/claude-code
cat > ~/.config/claude-code/media-upload.json << EOF
{
"mc_path": "${MC_PATH}",
"public_url": "${PUBLIC_URL}",
"organize_by": "date",
"max_file_size_mb": 100
}
EOF
chmod 600 ~/.config/claude-code/media-upload.json
echo "✅ Configuration saved"
При загрузке нескольких файлов:
# Пример: загрузи все png из ./screenshots/
# Защита от пустого glob
shopt -s nullglob
files=(./screenshots/*.png)
shopt -u nullglob
if [[ ${#files[@]} -eq 0 ]]; then
echo "❌ Файлы не найдены: ./screenshots/*.png"
exit 1
fi
success=()
failed=()
for file in "${files[@]}"; do
if upload_file "$file"; then
success+=("$file")
else
failed+=("$file")
fi
done
При ошибке части файлов показать:
Формат вывода:
⚠️ Uploaded 3 of 5 files:
✅ Успешно:
- screenshot-1.png → https://cdn...
- screenshot-2.png → https://cdn...
- screenshot-3.png → https://cdn...
❌ Ошибки:
- screenshot-4.png: Connection timeout
- screenshot-5.png: File too large (120MB > 100MB limit)
Файл: ~/.media-upload-history.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"
}
]
}
Просмотр истории:
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 для коротких постоянных ссылок
✅ 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):


| Ситуация | Действие |
|---|---|
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 |
Для коротких URL без presigned подписей:
⚠️ ВАЖНО: S3 API Selectel НЕ поддерживает анонимный доступ!
Даже если бакет настроен как "Публичный", запросы через S3 API (s3.ru-3.storage.selcloud.ru) требуют подписи.
Варианты решения:
Presigned URLs (рекомендуется):
url_mode: "presigned" в конфигурацииДомен selstorage.ru (публичный хостинг):
https://UUID.selstorage.ru/path/file.pngCDN Selectel (лучший вариант для продакшена):
*.selcdn.ru поддерживает анонимный доступПодробнее: https://qna.habr.com/q/1147270
mc anonymous set download ALIAS/BUCKET
aws s3api put-bucket-policy --bucket BUCKET --policy '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::BUCKET/*"
}]
}'