Дополнительные действия
Denchikp (обсуждение | вклад) Нет описания правки |
Denchikp (обсуждение | вклад) Нет описания правки |
||
| (не показана 1 промежуточная версия этого же участника) | |||
| Строка 1: | Строка 1: | ||
= | = CDN API (Reference) = | ||
== | == Общие требования == | ||
'''Base URL:''' https://cdnapi.denchikp.com<br> | '''Base URL:''' https://cdnapi.denchikp.com<br> | ||
'''CDN URL файлов:''' https://cdn.denchikp.com/<domain>/<folder>/<filename> | '''CDN URL файлов:''' https://cdn.denchikp.com/<domain>/<folder>/<filename> | ||
Большинство CDN‑эндпоинтов требуют: | |||
* заголовок <code>X-Api-Key: <API_KEY></code> | |||
или | |||
* активную cookie‑сессию, где ключ может резолвиться автоматически (OAuth/CDN login + bind). | |||
Во многих операциях, где передаётся <code>url</code>, принимаются '''только''' ссылки вида: | |||
<pre> | |||
https://cdn.denchikp.com/<domain>/<folder>/<filename> | |||
</pre> | |||
'' | |||
== Ключ / домены / квоты == | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/api-key/me</code> === | |||
Информация о текущем API‑ключе (по <code>X-Api-Key</code> или по сессии). | |||
'''Headers:''' | |||
* <code>X-Api-Key: <API_KEY></code> (рекомендуется) | |||
</ | |||
'''Response 200:''' | |||
<pre> | <pre> | ||
{ | { | ||
"status": " | "status":"ok", | ||
" | "key":{ | ||
"name":"ClientName", | |||
"is_admin":false, | |||
"domains":["public"], | |||
"ips":["1.2.3.4"] | |||
} | |||
} | } | ||
</pre> | </pre> | ||
'''Ошибки:''' | |||
* 401 — нет ключа и нет валидной авторизации | |||
---- | |||
'''Response 200''' | === <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/domains</code> === | ||
Список доменов, доступных для ключа. | |||
'''Response 200 (примеры):''' | |||
* admin‑ключ: | |||
<pre> | <pre> | ||
{"status":"ok"} | {"status":"ok","mode":"any","domains":[]} | ||
</pre> | </pre> | ||
* allowlist‑ключ: | |||
<pre> | <pre> | ||
{ | {"status":"ok","mode":"allowlist","domains":["public","example.com"]} | ||
} | |||
</pre> | </pre> | ||
* ключ без доменов (разрешён как минимум public): | |||
<pre> | <pre> | ||
{ | {"status":"ok","mode":"any","domains":["public"]} | ||
} | |||
</pre> | </pre> | ||
---- | |||
= | === <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/usage</code> === | ||
Текущее использование (без лимитов квоты). | |||
'''Response 200 (пример):''' | |||
'''Response 200''' | |||
<pre> | <pre> | ||
{ | { | ||
"status": "ok", | "status":"ok", | ||
" | "usage":{ | ||
" | "total_files":120, | ||
" | "total_bytes":123456789, | ||
" | "month":"2026-06", | ||
" | "monthly_uploaded_files":10, | ||
"monthly_uploaded_bytes":12300000 | |||
} | } | ||
} | } | ||
</pre> | </pre> | ||
---- | |||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/quota</code> == | === <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/quota</code> === | ||
Квоты + использование + остатки (remaining). | |||
'''Response 200 (пример)''' | '''Response 200 (пример):''' | ||
<pre> | <pre> | ||
{ | { | ||
"status": "ok", | "status":"ok", | ||
"api_key_name": "ClientName", | "api_key_name":"ClientName", | ||
"is_admin": false, | "is_admin":false, | ||
"quota": { | "quota":{ | ||
"max_file_bytes": 524288000, | "max_file_bytes":524288000, | ||
"max_total_bytes": 10737418240, | "max_total_bytes":10737418240, | ||
"max_total_files": 50000, | "max_total_files":50000, | ||
"max_monthly_upload_bytes": 5368709120, | "max_monthly_upload_bytes":5368709120, | ||
"max_monthly_upload_files": null | "max_monthly_upload_files":null | ||
}, | }, | ||
"usage": { | "usage":{ | ||
"total_files": 120, | "total_files":120, | ||
"total_bytes": 123456789, | "total_bytes":123456789, | ||
"month": "2026-06", | "month":"2026-06", | ||
"monthly_uploaded_files": 10, | "monthly_uploaded_files":10, | ||
"monthly_uploaded_bytes": 12300000 | "monthly_uploaded_bytes":12300000 | ||
}, | }, | ||
"remaining": { | "remaining":{ | ||
"total_bytes": 10000000000, | "total_bytes":10000000000, | ||
"total_files": 49880, | "total_files":49880, | ||
"monthly_upload_bytes": 5300000000, | "monthly_upload_bytes":5300000000, | ||
"monthly_upload_files": null | "monthly_upload_files":null | ||
} | } | ||
} | } | ||
</pre> | </pre> | ||
'''Ошибки:''' | |||
* 403 — если ключ невалиден/запрещён по IP и т.п. | |||
== | == Файлы (БД / meta / exists) == | ||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/files</code> === | |||
Список «моих» файлов из БД. | |||
'''Headers:''' | |||
* <code>X-Api-Key: <API_KEY></code> | |||
'''Query parameters (все опциональны):''' | |||
{| class="wikitable" | {| class="wikitable" | ||
! Name !! Type !! Default !! Description | ! Name !! Type !! Default !! Description | ||
|- | |- | ||
| q || string || || поиск по filename/original_filename (LIKE) | | q || string || — || поиск по <code>filename</code>/<code>original_filename</code> (LIKE) | ||
|- | |- | ||
| domain || string || || фильтр домена (нормализуется) | | domain || string || — || фильтр домена (нормализуется) | ||
|- | |- | ||
| folder || string || || фильтр папки | | folder || string || — || фильтр папки | ||
|- | |- | ||
| ext || string || || | | ext || string || — || фильтр расширения | ||
|- | |- | ||
| exists_on_disk || bool || true || фильтр по наличию на диске | | exists_on_disk || bool || true || фильтр по наличию на диске | ||
|- | |- | ||
| limit || int || 50 || | | limit || int || 50 || максимум 200 | ||
|- | |- | ||
| offset || int || 0 || смещение | | offset || int || 0 || смещение | ||
|} | |} | ||
'''Response 200 (пример)''' | '''Response 200 (пример):''' | ||
<pre> | <pre> | ||
{ | { | ||
"status": "ok", | "status":"ok", | ||
"total": 123, | "total":123, | ||
"limit": 50, | "limit":50, | ||
"offset": 0, | "offset":0, | ||
"files": [ | "files":[ | ||
{ | { | ||
"id": 1, | "id":1, | ||
"domain": "public", | "domain":"public", | ||
"folder": "img", | "folder":"img", | ||
"filename": "a.png", | "filename":"a.png", | ||
"original_filename": "cat.png", | "original_filename":"cat.png", | ||
"url": "https://cdn.denchikp.com/public/img/a.png", | "url":"https://cdn.denchikp.com/public/img/a.png", | ||
"size": 12345, | "size":12345, | ||
"created_at": "2026-06-16T10:00:00", | "created_at":"2026-06-16T10:00:00", | ||
"exists_on_disk": true | "exists_on_disk":true | ||
} | } | ||
] | ] | ||
| Строка 279: | Строка 164: | ||
</pre> | </pre> | ||
---- | |||
''' | === <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/file/meta</code> === | ||
Метаданные файла: сведения из БД + проверка на диске. | |||
'''Body (JSON):''' | |||
<pre> | <pre> | ||
{"url":"https://cdn.denchikp.com/public/img/a.png"} | {"url":"https://cdn.denchikp.com/public/img/a.png"} | ||
</pre> | </pre> | ||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/file/exists</code> == | '''Response 200 (схема):''' | ||
<pre> | |||
{ | |||
"status":"ok", | |||
"url":"...", | |||
"domain":"public", | |||
"folder":"img", | |||
"filename":"a.png", | |||
"db":{ | |||
"id":1, | |||
"owner":"ClientName", | |||
"api_key_id":10, | |||
"size":12345, | |||
"exists_on_disk":true, | |||
"created_at":"..." | |||
}, | |||
"disk":{ | |||
"exists":true, | |||
"size":12345, | |||
"mime_type":"image/png" | |||
} | |||
} | |||
</pre> | |||
'''Ошибки:''' | |||
* 400 — URL не CDN формата/не парсится | |||
* 403 — нет доступа к домену | |||
* 401 — нет ключа | |||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/file/exists</code> === | |||
Проверка существования файла (в БД и на диске). | Проверка существования файла (в БД и на диске). | ||
''' | '''Body (JSON):''' | ||
<pre> | <pre> | ||
{"url":"https://cdn.denchikp.com/public/img/a.png"} | {"url":"https://cdn.denchikp.com/public/img/a.png"} | ||
</pre> | </pre> | ||
'''Response 200''' | '''Response 200:''' | ||
<pre> | <pre> | ||
{"status":"ok","exists_in_db":true,"exists_on_disk":true} | {"status":"ok","exists_in_db":true,"exists_on_disk":true} | ||
</pre> | </pre> | ||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/file/redirect</code> == | ---- | ||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/file/redirect</code> === | |||
Публичный редирект на CDN URL. | Публичный редирект на CDN URL. | ||
Query: | '''Query:''' | ||
* <code>?url=https://cdn.denchikp.com/...</code> | * <code>?url=https://cdn.denchikp.com/...</code> | ||
'''Ошибки:''' | |||
* 400 — если url не начинается с <code>https://cdn.denchikp.com/</code> | |||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/upload</code> == | == Загрузка == | ||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/upload</code> === | |||
Загрузка файла (multipart/form-data). | Загрузка файла (multipart/form-data). | ||
''' | '''Headers:''' | ||
* <code>X-Api-Key: <API_KEY></code> | |||
'''Content-Type:''' <code>multipart/form-data</code> | |||
'''Max size:''' 500 MB | '''Max size:''' 500 MB | ||
Form fields: | '''Form fields:''' | ||
{| class="wikitable" | {| class="wikitable" | ||
! Field !! Type !! Required !! Description | ! Field !! Type !! Required !! Default !! Description | ||
|- | |- | ||
| file || file || да || загружаемый файл | | file || file || да || — || загружаемый файл | ||
|- | |- | ||
| domain || string || нет || "" → public | | domain || string || нет || "" || "" → public | ||
|- | |- | ||
| name_mode || string || нет || uuid | original | custom | | name_mode || string || нет || uuid || uuid | original | custom | ||
|- | |- | ||
| custom_filename || string || нет || используется при name_mode=custom | | custom_filename || string || нет || "" || используется при name_mode=custom | ||
|- | |- | ||
| conflict_action || string || нет || overwrite → перезаписать (с проверками прав) | | conflict_action || string || нет || "" || overwrite → перезаписать (с проверками прав) | ||
|} | |} | ||
'''Response 200 (пример):''' | |||
<pre> | <pre> | ||
{ | |||
"status":"ok", | |||
"url":"https://cdn.denchikp.com/public/img/...", | |||
"size":12345, | |||
"owner":"ClientName", | |||
"ip":"1.2.3.4", | |||
"domain":"public", | |||
"folder":"img", | |||
"filename":"..." | |||
} | |||
</pre> | </pre> | ||
'''Response | '''Response 409 (конфликт имени, если не overwrite):''' | ||
<pre> | <pre> | ||
{ | { | ||
"status": " | "status":"conflict", | ||
" | "message":"FILE_EXISTS", | ||
"requested_filename":"a.png", | |||
" | "existing_url":"https://cdn.denchikp.com/public/img/a.png", | ||
" | "domain":"public", | ||
"domain": "public", | "folder":"img" | ||
"folder": "img | |||
} | } | ||
</pre> | </pre> | ||
''' | '''Ошибки (частые):''' | ||
* 403 — квота превышена / домен не разрешён / IP не разрешён / overwrite запрещён | |||
* 413 — файл слишком большой | |||
* 500 — внутренняя ошибка | |||
Webhook event: <code>UPLOAD</code> (payload.source="api") | Webhook event: <code>UPLOAD</code> (payload.source="api") | ||
Пример (curl): | |||
<pre> | |||
curl -X POST "https://cdnapi.denchikp.com/upload" \ | |||
-H "X-Api-Key: <API_KEY>" \ | |||
-F "domain=public" \ | |||
-F "name_mode=uuid" \ | |||
-F "file=@./cat.png" | |||
</pre> | |||
---- | |||
''' | === <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/upload/by-url</code> === | ||
Сервер скачивает файл по ссылке и сохраняет в CDN. | |||
'''Body (JSON):''' | |||
<pre> | <pre> | ||
{"source_url":"https://example.com/file.png","domain":"public","folder":"img"} | { | ||
"source_url":"https://example.com/file.png", | |||
"domain":"public", | |||
"folder":"img" | |||
} | |||
</pre> | </pre> | ||
Ограничения: | '''Ограничения:''' | ||
* только http/https | * только http/https | ||
* | * SSRF‑защита: запрещено скачивать с private/loopback/link-local/reserved/multicast адресов | ||
* max 500 MB | * max 500 MB | ||
'''Response 200 (пример):''' | |||
<pre> | |||
{"status":"ok","url":"https://cdn.denchikp.com/public/img/...","size":12345,"domain":"public","folder":"img"} | |||
</pre> | |||
Webhook event: <code>UPLOAD</code> (payload.source="by_url") | Webhook event: <code>UPLOAD</code> (payload.source="by_url") | ||
= Multipart Upload = | == Multipart Upload == | ||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/upload/session/start</code> == | === <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/upload/session/start</code> === | ||
Создать multipart‑сессию. | Создать multipart‑сессию. | ||
''' | '''Body (JSON):''' | ||
<pre> | <pre> | ||
{"domain":"public","ext":"mp4","mime_type":"video/mp4","folder":"files"} | { | ||
"domain":"public", | |||
"ext":"mp4", | |||
"mime_type":"video/mp4", | |||
"folder":"files" | |||
} | |||
</pre> | </pre> | ||
'''Response 200''' | '''Response 200:''' | ||
<pre> | <pre> | ||
{"status":"ok","upload_id":" | {"status":"ok","upload_id":"<uuid>","chunk_size":8388608} | ||
</pre> | </pre> | ||
---- | |||
''' | === <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(251,191,36,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#fbbf24;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">PUT</span> <code>/upload/session/{upload_id}/part/{part_no}</code> === | ||
Загрузить часть файла (chunk). | |||
'''Content-Type:''' <code>multipart/form-data</code> | |||
Form-data: | Form-data: | ||
* <code>file</code> — chunk | * <code>file</code> — chunk | ||
'''Response 200''' | '''Response 200:''' | ||
<pre> | <pre> | ||
{"status":"ok","part_no":1,"bytes":8388608} | {"status":"ok","part_no":1,"bytes":8388608} | ||
</pre> | </pre> | ||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/upload/session/{upload_id}/complete</code> == | '''Ошибки:''' | ||
* 404 — upload_id не найден | |||
* 403 — upload_id принадлежит другому ключу (для не‑админов) | |||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/upload/session/{upload_id}/complete</code> === | |||
Склеить части и создать итоговый файл. | |||
'''Ошибки:''' | |||
* 400 — нет частей | |||
* 413 — итоговый файл > 500MB | |||
* 403 — квота/права | |||
Webhook event: <code>UPLOAD</code> (payload.source="multipart") | Webhook event: <code>UPLOAD</code> (payload.source="multipart") | ||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/upload/session/{upload_id}/abort</code> === | |||
Отменить multipart‑сессию и удалить временные части. | |||
= | '''Response 200:''' | ||
<pre> | |||
{"status":"ok"} | |||
</pre> | |||
== Операции с файлами == | |||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/files/bulk-delete</code> == | === <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/files/bulk-delete</code> === | ||
Удалить | Удалить много файлов за один запрос (до 500 URL). | ||
''' | '''Body (JSON):''' | ||
<pre> | <pre> | ||
{"urls":["https://cdn.denchikp.com/public/files/a.txt","..."]} | {"urls":["https://cdn.denchikp.com/public/files/a.txt","..."]} | ||
</pre> | </pre> | ||
'''Response 200 (пример)''' | '''Response 200 (пример):''' | ||
<pre> | <pre> | ||
{ | { | ||
"status": "ok", | "status":"ok", | ||
"results": [ | "results":[ | ||
{"url":" | {"url":"...","status":"ok"}, | ||
{"url":" | {"url":"...","status":"error","error":"..."} | ||
] | ] | ||
} | } | ||
| Строка 438: | Строка 406: | ||
Webhook event: <code>BULK_DELETE</code> | Webhook event: <code>BULK_DELETE</code> | ||
---- | |||
''' | === <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/file/move</code> === | ||
Перемещение/переименование файла (можно менять домен/папку/имя). | |||
'''Body (JSON):''' | |||
<pre> | <pre> | ||
{ | { | ||
| Строка 450: | Строка 419: | ||
"new_filename":"b.png" | "new_filename":"b.png" | ||
} | } | ||
</pre> | |||
'''Правила:''' | |||
* <code>new_folder</code> должен быть одним из: img/css/js/fonts/files | |||
* <code>new_filename</code> не должен содержать <code>..</code>, <code>/</code>, <code>\</code> | |||
'''Response 200:''' | |||
<pre> | |||
{"status":"ok","from":"https://.../a.png","to":"https://.../b.png"} | |||
</pre> | </pre> | ||
Webhook event: <code>FILE_MOVE</code> | Webhook event: <code>FILE_MOVE</code> | ||
---- | |||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/file/ | === <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/file/rename</code> === | ||
Переименовать файл (alias для <code>/file/move</code>, но меняется только имя). | |||
''' | '''Body (JSON):''' | ||
Body: | <pre> | ||
{ | |||
"url":"https://cdn.denchikp.com/public/img/a.png", | |||
"new_filename":"b.png" | |||
} | |||
</pre> | |||
'''Ошибки:''' | |||
* 400 — <code>new_filename</code> обязателен | |||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/file/copy</code> === | |||
Копирование файла (квоты учитываются как новая загрузка). | |||
'''Body (JSON):''' | |||
<pre> | <pre> | ||
{ | { | ||
| Строка 469: | Строка 461: | ||
"target_filename":"copy.png" | "target_filename":"copy.png" | ||
} | } | ||
</pre> | |||
'''Response 200:''' | |||
<pre> | |||
{"status":"ok","url":"https://cdn.denchikp.com/public/img/copy.png","size":12345} | |||
</pre> | </pre> | ||
Webhook event: <code>FILE_COPY</code> | Webhook event: <code>FILE_COPY</code> | ||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/file/hash</code> == | ---- | ||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/file/hash</code> === | |||
Посчитать хэш файла. | |||
''' | '''Body (JSON):''' | ||
<pre> | <pre> | ||
{"url":"https://cdn.denchikp.com/public/files/a.bin","algo":"sha256"} | {"url":"https://cdn.denchikp.com/public/files/a.bin","algo":"sha256"} | ||
</pre> | </pre> | ||
<code>algo</code>: sha256 | sha1 | md5 | |||
'''Response 200:''' | |||
<pre> | |||
{"status":"ok","algo":"sha256","hash":"<hex>","url":"https://..."} | |||
</pre> | |||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/browse</code> == | == Browse / Folders / Download == | ||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/browse</code> === | |||
Просмотр файлов на диске в домене. | Просмотр файлов на диске в домене. | ||
''' | '''Body (JSON):''' | ||
<pre> | <pre> | ||
{"domain":"public","folder":"img","filter":"cat"} | {"domain":"public","folder":"img","filter":"cat"} | ||
</pre> | </pre> | ||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/folders/{domain}</code> == | Поля: | ||
* <code>domain</code> — обязательно | |||
* <code>folder</code> — опционально | |||
* <code>filter</code> — подстрока в имени файла (опционально) | |||
'''Response 200 (схема):''' | |||
<pre> | |||
{"status":"ok","domain":"public","total_files":2,"files":[{"name":"a.png","path":"img/a.png","size":12345,"url":"..."}]} | |||
</pre> | |||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/folders/{domain}</code> === | |||
Список папок домена + агрегаты. | |||
'''Response 200 (пример):''' | |||
<pre> | |||
{ | |||
"status":"ok", | |||
"domain":"public", | |||
"total_files":10, | |||
"total_size":1234567, | |||
"folders":[{"name":"img","file_count":5,"size":1000000,"path":"public/img"}] | |||
} | |||
</pre> | |||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/download/{domain}/{folder}/{filename:path}</code> === | |||
Скачивание файла. | |||
'''Важно:''' | |||
хотя параметр <code>{filename:path}</code> технически может содержать “/”, сервер отклонит имя, если в нём есть <code>/</code> или <code>\</code> или <code>..</code>. | |||
''' | '''Response 200:''' | ||
* бинарный поток (FileResponse), <code>application/octet-stream</code> | |||
'''Ошибки:''' | |||
* 404 — файл не найден | |||
* 403 — нет доступа к домену | |||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/validate</code> == | == Validate URL == | ||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/validate</code> === | |||
Валидация ссылки (http/https). Делает HEAD, при необходимости GET range 0-0, возвращает HTTP статус и заголовки. | |||
Если URL — CDN URL, дополнительно парсит <code>domain/folder/filename</code> и проверяет наличие на диске. | Если URL — CDN URL, дополнительно парсит <code>domain/folder/filename</code> и проверяет наличие на диске. | ||
''' | '''Body (JSON):''' | ||
<pre> | <pre> | ||
{"url":"https://example.com/file.png"} | {"url":"https://example.com/file.png"} | ||
</pre> | </pre> | ||
'''Response 200 (схема, укорочено):''' | |||
<pre> | |||
{ | |||
"status":"ok", | |||
"checked_by":"ClientName", | |||
"url":"https://example.com/file.png", | |||
"is_valid_url":true, | |||
"is_cdn_url":false, | |||
"available":true, | |||
"http_status":200, | |||
"headers":{"content_type":"image/png","content_length":"12345","last_modified":null,"etag":null,"server":"nginx"}, | |||
"file_info":{"filename":"file.png","extension":"png","host":"example.com","path":"/file.png"}, | |||
"cdn_info":null, | |||
"error":null | |||
} | |||
</pre> | |||
== | == Delete file == | ||
''' | === <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(251,113,133,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#fb7185;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">DELETE</span> <code>/delete</code> === | ||
Удаление файла по CDN URL. | |||
'''Body (JSON):''' | |||
<pre> | <pre> | ||
{"url":"https://cdn.denchikp.com/public/files/a.txt"} | {"url":"https://cdn.denchikp.com/public/files/a.txt"} | ||
</pre> | |||
'''Response 200 (пример):''' | |||
<pre> | |||
{ | |||
"status":"ok", | |||
"message":"Файл успешно удален", | |||
"domain":"public", | |||
"folder":"files", | |||
"filename":"a.txt", | |||
"url":"https://cdn.denchikp.com/public/files/a.txt", | |||
"owner":"ClientName", | |||
"ip":"1.2.3.4" | |||
} | |||
</pre> | </pre> | ||
Webhook event: <code>DELETE</code> (payload.source="api") | Webhook event: <code>DELETE</code> (payload.source="api") | ||
= Text Editor (Read/Save) = | == Text Editor (Read/Save) == | ||
Только для “текстовых” файлов и ограничение по размеру: ≤ 2 MB. | |||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/file/content</code> == | ---- | ||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/file/content</code> === | |||
Получить содержимое текстового файла. | Получить содержимое текстового файла. | ||
''' | '''Body (JSON):''' | ||
<pre> | <pre> | ||
{"url":"https://cdn.denchikp.com/public/files/app.js"} | {"url":"https://cdn.denchikp.com/public/files/app.js"} | ||
</pre> | </pre> | ||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/file/save</code> == | '''Response 200 (пример):''' | ||
<pre> | |||
{ | |||
"status":"ok", | |||
"url":"https://cdn.denchikp.com/public/files/app.js", | |||
"domain":"public", | |||
"folder":"files", | |||
"filename":"app.js", | |||
"mime_type":"application/javascript", | |||
"size":1200, | |||
"content":"console.log('hi');\n" | |||
} | |||
</pre> | |||
'''Ошибки:''' | |||
* 400 — файл не текстовый / >2MB / неподдерживаемая кодировка | |||
* 404 — файл не найден | |||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/file/save</code> === | |||
Сохранить содержимое текстового файла. | Сохранить содержимое текстового файла. | ||
''' | '''Body (JSON):''' | ||
<pre> | <pre> | ||
{"url":"https://cdn.denchikp.com/public/files/app.js","content":"console.log('hi');\n"} | {"url":"https://cdn.denchikp.com/public/files/app.js","content":"console.log('hi');\n"} | ||
</pre> | |||
'''Response 200:''' | |||
<pre> | |||
{"status":"ok","message":"Файл успешно сохранен","url":"...","domain":"public","folder":"files","filename":"app.js","size":1200} | |||
</pre> | </pre> | ||
Webhook event: <code>FILE_SAVE</code> | Webhook event: <code>FILE_SAVE</code> | ||
= Webhooks = | ---- | ||
= Webhooks (Reference) = | |||
Webhooks позволяют получать события от сервиса (UPLOAD/DELETE и т.д.) на ваш URL. | |||
== Создание / управление == | |||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/webhooks</code> == | === <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/webhooks</code> === | ||
Создать webhook. | Создать webhook. | ||
''' | '''Headers:''' | ||
Body: | * <code>X-Api-Key: <API_KEY></code> | ||
'''Body (JSON):''' | |||
<pre> | <pre> | ||
{ | { | ||
| Строка 573: | Строка 665: | ||
</pre> | </pre> | ||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/webhooks</code> == | '''Response 200:''' | ||
<pre> | |||
{"status":"ok","id":123} | |||
</pre> | |||
'''Ошибки:''' | |||
* 401/403 — нет доступа | |||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/webhooks</code> === | |||
Список webhooks текущего ключа. | Список webhooks текущего ключа. | ||
''' | '''Response 200 (пример):''' | ||
<pre> | |||
{ | |||
"status":"ok", | |||
"webhooks":[ | |||
{ | |||
"id":123, | |||
"url":"https://example.com/hook", | |||
"events":["UPLOAD","DELETE"], | |||
"enabled":true, | |||
"created_at":"2026-06-16T12:00:00" | |||
} | |||
] | |||
} | |||
</pre> | |||
== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(251,113,133,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#fb7185;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">DELETE</span> <code>/webhooks/{webhook_id}</code> == | ---- | ||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(251,113,133,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#fb7185;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">DELETE</span> <code>/webhooks/{webhook_id}</code> === | |||
Удалить webhook. | Удалить webhook. | ||
''' | '''Path params:''' | ||
* <code>webhook_id</code> — integer | |||
== | '''Response 200:''' | ||
<pre> | |||
{"status":"ok"} | |||
</pre> | |||
'''Ошибки:''' | |||
* 404 — webhook не найден (или не принадлежит ключу) | |||
== Delivery format == | |||
Сервер отправляет на URL webhook’а запрос: | |||
* Method: POST | |||
* Content-Type: application/json | |||
* Body: | |||
<pre> | <pre> | ||
{"event":"UPLOAD","payload":{...}} | {"event":"UPLOAD","payload":{...}} | ||
| Строка 590: | Строка 721: | ||
== События == | == События == | ||
* UPLOAD | События, которые реально используются в текущем коде: | ||
* DELETE | * <code>UPLOAD</code> | ||
* BULK_DELETE | * <code>DELETE</code> | ||
* FILE_MOVE | * <code>BULK_DELETE</code> | ||
* FILE_COPY | * <code>FILE_MOVE</code> | ||
* FILE_SAVE | * <code>FILE_COPY</code> | ||
* SCAN_FINISHED | * <code>FILE_SAVE</code> | ||
* <code>SCAN_FINISHED</code> | |||
== Подпись (HMAC) == | == Подпись (HMAC) == | ||
Если | Если у webhook задан <code>secret</code> и на сервере включена подпись, рекомендуется добавлять: | ||
Headers: | Headers: | ||
* <code>X-Webhook-Event: | * <code>X-Webhook-Event: <EVENT></code> | ||
* <code>X-Webhook-Timestamp: | * <code>X-Webhook-Timestamp: <unix_seconds></code> | ||
* <code>X-Webhook-Signature: sha256=<hex></code> | * <code>X-Webhook-Signature: sha256=<hex></code> | ||
| Строка 611: | Строка 743: | ||
* получатель проверяет подпись и свежесть timestamp (например ±300 секунд) | * получатель проверяет подпись и свежесть timestamp (например ±300 секунд) | ||
= | ''Примечание:'' в текущем коде поле <code>secret</code> хранится, но подпись нужно добавить отдельным патчем (если ещё не добавлено). | ||
= Bot (internal) = | |||
Все эндпоинты <code>/bot/*</code> требуют заголовок <code>X-Bot-Token</code>. | |||
{| class="wikitable" | |||
! Header !! Обязательно !! Описание | |||
|- | |||
| X-Bot-Token || да || внутренний токен, должен совпадать с <code>DISCORD_BOT_INTERNAL_TOKEN</code> на сервере | |||
|} | |||
== Discord binding == | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/bot/discord/bind</code> === | |||
Привязать Discord пользователя к API‑ключу (для бота). | |||
'''Headers:''' | |||
* <code>X-Bot-Token: <BOT_TOKEN></code> | |||
'''Body (JSON):''' | |||
<pre> | |||
{ | |||
"discord_id": "1234567890", | |||
"api_key": "<API_KEY>", | |||
"username": "optional", | |||
"global_name": "optional", | |||
"email": "optional", | |||
"avatar": "optional" | |||
} | |||
</pre> | |||
'''Response 200 (пример):''' | |||
<pre> | |||
{ | |||
"status": "ok", | |||
"message": "Discord аккаунт успешно привязан к API ключу", | |||
"discord_id": "1234567890", | |||
"key_info": { | |||
"name": "ClientName", | |||
"domains": ["public"], | |||
"enabled": true, | |||
"is_admin": false, | |||
"ips": [] | |||
} | |||
} | |||
</pre> | |||
'''Ошибки:''' | |||
* 403 — недействительный <code>X-Bot-Token</code> | |||
* 400 — API ключ не существует | |||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/bot/discord/unbind</code> === | |||
Отвязать Discord пользователя от API‑ключа. | |||
'''Headers:''' | |||
* <code>X-Bot-Token: <BOT_TOKEN></code> | |||
'''Body (JSON):''' | |||
<pre> | |||
{ "discord_id": "1234567890" } | |||
</pre> | |||
'''Response 200:''' | |||
<pre> | |||
{ "status": "ok", "message": "Discord аккаунт успешно отвязан от API ключа" } | |||
</pre> | |||
'''Ошибки:''' | |||
* 403 — недействительный <code>X-Bot-Token</code> | |||
* 404 — привязка не найдена | |||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/bot/discord/me/{discord_id}</code> === | |||
Получить информацию о привязке Discord пользователя. | |||
'''Headers:''' | |||
* <code>X-Bot-Token: <BOT_TOKEN></code> | |||
'''Path params:''' | |||
* <code>discord_id</code> — Discord ID | |||
'''Response 200 (пример, если привязан):''' | |||
<pre> | <pre> | ||
{ | { | ||
" | "authenticated": true, | ||
" | "bound": true, | ||
" | "discord_id": "1234567890", | ||
" | "user": { "name": "Display Name", "email": "[email protected]", "avatar": "https://..." }, | ||
"domains":["public"] | "key_info": { "name":"ClientName","domains":["public"],"enabled":true,"is_admin":false,"ips":[] } | ||
} | } | ||
</pre> | </pre> | ||
'''Response 404 (если не привязан):''' | |||
<pre> | <pre> | ||
{" | { "authenticated": false, "bound": false } | ||
</pre> | </pre> | ||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/ | == Bot files == | ||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/bot/upload</code> === | |||
Загрузка файла через Discord‑бота (multipart/form-data). | |||
'''Headers:''' | |||
* <code>X-Bot-Token: <BOT_TOKEN></code> | |||
'''Content-Type:''' <code>multipart/form-data</code> | |||
'''Max file size:''' 500 MB | |||
= | '''Form fields:''' | ||
{| class="wikitable" | |||
! Field !! Type !! Required !! Default !! Description | |||
|- | |||
| discord_id || string || да || — || Discord ID | |||
|- | |||
| file || file || да || — || файл | |||
|- | |||
| domain || string || нет || "" || "" → public | |||
|- | |||
| name_mode || string || нет || uuid || uuid | original | custom | |||
|- | |||
| custom_filename || string || нет || "" || используется при custom | |||
|- | |||
| conflict_action || string || нет || "" || overwrite → перезаписать (с проверками) | |||
|} | |||
'''Response 200 (пример):''' | |||
<pre> | |||
{ | |||
"status":"ok", | |||
"url":"https://cdn.denchikp.com/public/img/...", | |||
"size":12345, | |||
"owner":"ClientName", | |||
"domain":"public", | |||
"folder":"img", | |||
"filename":"..." | |||
} | |||
</pre> | |||
'''Response 409 (если имя занято и overwrite не задан):''' | |||
<pre> | <pre> | ||
{ | { | ||
" | "status":"conflict", | ||
" | "message":"FILE_EXISTS", | ||
" | "requested_filename":"a.png", | ||
" | "existing_url":"https://cdn.denchikp.com/public/img/a.png", | ||
" | "domain":"public", | ||
"folder":"img" | |||
} | } | ||
</pre> | </pre> | ||
'''Ошибки (частые):''' | |||
* 401 — Discord аккаунт не привязан к API ключу | |||
* 403 — нет доступа к домену / квота / overwrite запрещён | |||
* 413 — файл слишком большой | |||
* 500 — внутренняя ошибка | |||
Пример (curl): | |||
<pre> | |||
curl -X POST "https://cdnapi.denchikp.com/bot/upload" \ | |||
-H "X-Bot-Token: <BOT_TOKEN>" \ | |||
-F "discord_id=1234567890" \ | |||
-F "domain=public" \ | |||
-F "name_mode=uuid" \ | |||
-F "file=@./cat.png" | |||
</pre> | |||
---- | |||
= | === <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(251,113,133,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#fb7185;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">DELETE</span> <code>/bot/delete</code> === | ||
Удалить файл через бота. | |||
'''Headers:''' | |||
* <code>X-Bot-Token: <BOT_TOKEN></code> | |||
'''Body (JSON):''' | |||
<pre> | |||
{ "discord_id":"1234567890", "url":"https://cdn.denchikp.com/public/files/a.txt" } | |||
</pre> | |||
'' | '''Response 200 (пример):''' | ||
<pre> | |||
{ | |||
"status":"ok", | |||
"message":"Файл успешно удален", | |||
"domain":"public", | |||
"folder":"files", | |||
"filename":"a.txt", | |||
"url":"https://cdn.denchikp.com/public/files/a.txt", | |||
"owner":"ClientName" | |||
} | |||
</pre> | |||
'''Ошибки:''' | |||
* 400 — некорректный URL / путь не файл | |||
* 401 — Discord аккаунт не привязан | |||
* 403 — нет доступа к домену | |||
* 404 — файл не найден | |||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/bot/download/{discord_id}/{domain}/{folder}/{filename:path}</code> === | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba( | Получить ссылки на скачивание (CDN URL + API download URL). | ||
'''Headers:''' | |||
* <code>X-Bot-Token: <BOT_TOKEN></code> | |||
'''Response 200 (пример):''' | |||
<pre> | <pre> | ||
{" | { | ||
"status":"ok", | |||
"owner":"ClientName", | |||
"domain":"public", | |||
"folder":"img", | |||
"filename":"a.png", | |||
"size":12345, | |||
"cdn_url":"https://cdn.denchikp.com/public/img/a.png", | |||
"api_download_url":"https://cdnapi.denchikp.com/download/public/img/a.png" | |||
} | |||
</pre> | </pre> | ||
'''Ошибки:''' | |||
* 400 — недопустимое имя файла (.. или / или \) | |||
* 401 — Discord аккаунт не привязан | |||
* 403 — нет доступа к домену | |||
* 404 — файл не найден | |||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/bot/browse</code> === | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/ | Просмотр файлов домена через бота. | ||
Body: | |||
'''Headers:''' | |||
* <code>X-Bot-Token: <BOT_TOKEN></code> | |||
'''Body (JSON):''' | |||
<pre> | <pre> | ||
{" | { "discord_id":"1234567890", "domain":"public", "folder":"img", "filter":"cat" } | ||
</pre> | </pre> | ||
'''Response 200 (пример, укорочено):''' | |||
<pre> | |||
{ "status":"ok", "domain":"public", "total_files":1, "files":[{"name":"a.png","path":"img/a.png","size":12345,"url":"..."}] } | |||
</pre> | |||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/bot/folders/{discord_id}/{domain}</code> === | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/ | Папки домена через бота + статистика по папкам. | ||
= Bot ( | '''Headers:''' | ||
* <code>X-Bot-Token: <BOT_TOKEN></code> | |||
'''Response 200 (пример):''' | |||
<pre> | |||
{ | |||
"status":"ok", | |||
"domain":"public", | |||
"total_files":10, | |||
"total_size":1234567, | |||
"folders":[{"name":"img","file_count":5,"size":1000000,"path":"public/img"}] | |||
} | |||
</pre> | |||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/bot/validate</code> === | |||
Валидация ссылки через бота (HEAD/GET, статус и заголовки). Для CDN URL также возвращает parsed info и exists_on_disk. | |||
'''Headers:''' | |||
* <code>X-Bot-Token: <BOT_TOKEN></code> | |||
'''Body (JSON):''' | |||
<pre> | |||
{ "discord_id":"1234567890", "url":"https://example.com/file.png" } | |||
</pre> | |||
'''Response 200 (схема, укорочено):''' | |||
<pre> | |||
{ | |||
"status":"ok", | |||
"checked_by":"ClientName", | |||
"url":"https://example.com/file.png", | |||
"is_valid_url":true, | |||
"is_cdn_url":false, | |||
"available":true, | |||
"http_status":200, | |||
"headers":{"content_type":"image/png","content_length":"12345"}, | |||
"file_info":{"filename":"file.png","extension":"png","host":"example.com","path":"/file.png"}, | |||
"cdn_info":null, | |||
"error":null | |||
} | |||
</pre> | |||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/bot/file/content</code> === | |||
Получить содержимое текстового файла (предпросмотр для бота). | |||
'''Ограничения:''' | |||
* только текстовые файлы | |||
* размер ≤ 2 MB | |||
'''Body (JSON):''' | |||
<pre> | |||
{ "discord_id":"1234567890", "url":"https://cdn.denchikp.com/public/files/app.js" } | |||
</pre> | |||
'''Response 200 (пример):''' | |||
<pre> | |||
{ | |||
"status":"ok", | |||
"url":"https://cdn.denchikp.com/public/files/app.js", | |||
"domain":"public", | |||
"folder":"files", | |||
"filename":"app.js", | |||
"mime_type":"application/javascript", | |||
"size":1200, | |||
"content":"console.log('hi');\n" | |||
} | |||
</pre> | |||
== Bot stats == | == Bot stats == | ||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/bot/public-summary</code> === | |||
Публичная статистика CDN (вызов внутри бота). Фактически возвращает то же, что <code>GET /stats/public-summary</code>, но требует <code>X-Bot-Token</code>. | |||
'''Response 200 (пример):''' | |||
<pre> | |||
{ | |||
"status":"ok", | |||
"stats":{ | |||
"uploaded_files":1000, | |||
"current_files":900, | |||
"total_size_bytes":123456789, | |||
"total_size_mb":117.74, | |||
"domains":10, | |||
"clients":3, | |||
"registered_api_clients":5 | |||
} | |||
} | |||
</pre> | |||
---- | |||
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/bot/stats/{discord_id}</code> === | |||
Админ‑статистика для бота: доступно только если привязанный к <code>discord_id</code> ключ имеет <code>is_admin=true</code>. Возвращает ту же структуру, что <code>/bot/public-summary</code>. | |||
'''Ошибки:''' | |||
* 403 — только администраторы могут просматривать статистику | |||
* 401 — Discord аккаунт не привязан к API ключу | |||
Текущая версия от 00:45, 17 июня 2026
CDN API (Reference)
Общие требования
Base URL: https://cdnapi.denchikp.com
CDN URL файлов: https://cdn.denchikp.com/<domain>/<folder>/<filename>
Большинство CDN‑эндпоинтов требуют:
- заголовок
X-Api-Key: <API_KEY>
или
- активную cookie‑сессию, где ключ может резолвиться автоматически (OAuth/CDN login + bind).
Во многих операциях, где передаётся url, принимаются только ссылки вида:
https://cdn.denchikp.com/<domain>/<folder>/<filename>
Ключ / домены / квоты
GET /api-key/me
Информация о текущем API‑ключе (по X-Api-Key или по сессии).
Headers:
X-Api-Key: <API_KEY>(рекомендуется)
Response 200:
{
"status":"ok",
"key":{
"name":"ClientName",
"is_admin":false,
"domains":["public"],
"ips":["1.2.3.4"]
}
}
Ошибки:
- 401 — нет ключа и нет валидной авторизации
GET /domains
Список доменов, доступных для ключа.
Response 200 (примеры):
- admin‑ключ:
{"status":"ok","mode":"any","domains":[]}
- allowlist‑ключ:
{"status":"ok","mode":"allowlist","domains":["public","example.com"]}
- ключ без доменов (разрешён как минимум public):
{"status":"ok","mode":"any","domains":["public"]}
GET /usage
Текущее использование (без лимитов квоты).
Response 200 (пример):
{
"status":"ok",
"usage":{
"total_files":120,
"total_bytes":123456789,
"month":"2026-06",
"monthly_uploaded_files":10,
"monthly_uploaded_bytes":12300000
}
}
GET /quota
Квоты + использование + остатки (remaining).
Response 200 (пример):
{
"status":"ok",
"api_key_name":"ClientName",
"is_admin":false,
"quota":{
"max_file_bytes":524288000,
"max_total_bytes":10737418240,
"max_total_files":50000,
"max_monthly_upload_bytes":5368709120,
"max_monthly_upload_files":null
},
"usage":{
"total_files":120,
"total_bytes":123456789,
"month":"2026-06",
"monthly_uploaded_files":10,
"monthly_uploaded_bytes":12300000
},
"remaining":{
"total_bytes":10000000000,
"total_files":49880,
"monthly_upload_bytes":5300000000,
"monthly_upload_files":null
}
}
Ошибки:
- 403 — если ключ невалиден/запрещён по IP и т.п.
Файлы (БД / meta / exists)
GET /files
Список «моих» файлов из БД.
Headers:
X-Api-Key: <API_KEY>
Query parameters (все опциональны):
| Name | Type | Default | Description |
|---|---|---|---|
| q | string | — | поиск по filename/original_filename (LIKE)
|
| domain | string | — | фильтр домена (нормализуется) |
| folder | string | — | фильтр папки |
| ext | string | — | фильтр расширения |
| exists_on_disk | bool | true | фильтр по наличию на диске |
| limit | int | 50 | максимум 200 |
| offset | int | 0 | смещение |
Response 200 (пример):
{
"status":"ok",
"total":123,
"limit":50,
"offset":0,
"files":[
{
"id":1,
"domain":"public",
"folder":"img",
"filename":"a.png",
"original_filename":"cat.png",
"url":"https://cdn.denchikp.com/public/img/a.png",
"size":12345,
"created_at":"2026-06-16T10:00:00",
"exists_on_disk":true
}
]
}
POST /file/meta
Метаданные файла: сведения из БД + проверка на диске.
Body (JSON):
{"url":"https://cdn.denchikp.com/public/img/a.png"}
Response 200 (схема):
{
"status":"ok",
"url":"...",
"domain":"public",
"folder":"img",
"filename":"a.png",
"db":{
"id":1,
"owner":"ClientName",
"api_key_id":10,
"size":12345,
"exists_on_disk":true,
"created_at":"..."
},
"disk":{
"exists":true,
"size":12345,
"mime_type":"image/png"
}
}
Ошибки:
- 400 — URL не CDN формата/не парсится
- 403 — нет доступа к домену
- 401 — нет ключа
POST /file/exists
Проверка существования файла (в БД и на диске).
Body (JSON):
{"url":"https://cdn.denchikp.com/public/img/a.png"}
Response 200:
{"status":"ok","exists_in_db":true,"exists_on_disk":true}
GET /file/redirect
Публичный редирект на CDN URL.
Query:
?url=https://cdn.denchikp.com/...
Ошибки:
- 400 — если url не начинается с
https://cdn.denchikp.com/
Загрузка
POST /upload
Загрузка файла (multipart/form-data).
Headers:
X-Api-Key: <API_KEY>
Content-Type: multipart/form-data
Max size: 500 MB
Form fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| file | file | да | — | загружаемый файл |
| domain | string | нет | "" | "" → public |
| name_mode | string | нет | uuid | original | custom |
| custom_filename | string | нет | "" | используется при name_mode=custom |
| conflict_action | string | нет | "" | overwrite → перезаписать (с проверками прав) |
Response 200 (пример):
{
"status":"ok",
"url":"https://cdn.denchikp.com/public/img/...",
"size":12345,
"owner":"ClientName",
"ip":"1.2.3.4",
"domain":"public",
"folder":"img",
"filename":"..."
}
Response 409 (конфликт имени, если не overwrite):
{
"status":"conflict",
"message":"FILE_EXISTS",
"requested_filename":"a.png",
"existing_url":"https://cdn.denchikp.com/public/img/a.png",
"domain":"public",
"folder":"img"
}
Ошибки (частые):
- 403 — квота превышена / домен не разрешён / IP не разрешён / overwrite запрещён
- 413 — файл слишком большой
- 500 — внутренняя ошибка
Webhook event: UPLOAD (payload.source="api")
Пример (curl):
curl -X POST "https://cdnapi.denchikp.com/upload" \ -H "X-Api-Key: <API_KEY>" \ -F "domain=public" \ -F "name_mode=uuid" \ -F "file=@./cat.png"
POST /upload/by-url
Сервер скачивает файл по ссылке и сохраняет в CDN.
Body (JSON):
{
"source_url":"https://example.com/file.png",
"domain":"public",
"folder":"img"
}
Ограничения:
- только http/https
- SSRF‑защита: запрещено скачивать с private/loopback/link-local/reserved/multicast адресов
- max 500 MB
Response 200 (пример):
{"status":"ok","url":"https://cdn.denchikp.com/public/img/...","size":12345,"domain":"public","folder":"img"}
Webhook event: UPLOAD (payload.source="by_url")
Multipart Upload
POST /upload/session/start
Создать multipart‑сессию.
Body (JSON):
{
"domain":"public",
"ext":"mp4",
"mime_type":"video/mp4",
"folder":"files"
}
Response 200:
{"status":"ok","upload_id":"<uuid>","chunk_size":8388608}
PUT /upload/session/{upload_id}/part/{part_no}
Загрузить часть файла (chunk).
Content-Type: multipart/form-data
Form-data:
file— chunk
Response 200:
{"status":"ok","part_no":1,"bytes":8388608}
Ошибки:
- 404 — upload_id не найден
- 403 — upload_id принадлежит другому ключу (для не‑админов)
POST /upload/session/{upload_id}/complete
Склеить части и создать итоговый файл.
Ошибки:
- 400 — нет частей
- 413 — итоговый файл > 500MB
- 403 — квота/права
Webhook event: UPLOAD (payload.source="multipart")
POST /upload/session/{upload_id}/abort
Отменить multipart‑сессию и удалить временные части.
Response 200:
{"status":"ok"}
Операции с файлами
POST /files/bulk-delete
Удалить много файлов за один запрос (до 500 URL).
Body (JSON):
{"urls":["https://cdn.denchikp.com/public/files/a.txt","..."]}
Response 200 (пример):
{
"status":"ok",
"results":[
{"url":"...","status":"ok"},
{"url":"...","status":"error","error":"..."}
]
}
Webhook event: BULK_DELETE
POST /file/move
Перемещение/переименование файла (можно менять домен/папку/имя).
Body (JSON):
{
"url":"https://cdn.denchikp.com/public/img/a.png",
"new_domain":"public",
"new_folder":"files",
"new_filename":"b.png"
}
Правила:
new_folderдолжен быть одним из: img/css/js/fonts/filesnew_filenameне должен содержать..,/,\
Response 200:
{"status":"ok","from":"https://.../a.png","to":"https://.../b.png"}
Webhook event: FILE_MOVE
POST /file/rename
Переименовать файл (alias для /file/move, но меняется только имя).
Body (JSON):
{
"url":"https://cdn.denchikp.com/public/img/a.png",
"new_filename":"b.png"
}
Ошибки:
- 400 —
new_filenameобязателен
POST /file/copy
Копирование файла (квоты учитываются как новая загрузка).
Body (JSON):
{
"url":"https://cdn.denchikp.com/public/img/a.png",
"target_domain":"public",
"target_folder":"img",
"target_filename":"copy.png"
}
Response 200:
{"status":"ok","url":"https://cdn.denchikp.com/public/img/copy.png","size":12345}
Webhook event: FILE_COPY
POST /file/hash
Посчитать хэш файла.
Body (JSON):
{"url":"https://cdn.denchikp.com/public/files/a.bin","algo":"sha256"}
algo: sha256 | sha1 | md5
Response 200:
{"status":"ok","algo":"sha256","hash":"<hex>","url":"https://..."}
Browse / Folders / Download
POST /browse
Просмотр файлов на диске в домене.
Body (JSON):
{"domain":"public","folder":"img","filter":"cat"}
Поля:
domain— обязательноfolder— опциональноfilter— подстрока в имени файла (опционально)
Response 200 (схема):
{"status":"ok","domain":"public","total_files":2,"files":[{"name":"a.png","path":"img/a.png","size":12345,"url":"..."}]}
GET /folders/{domain}
Список папок домена + агрегаты.
Response 200 (пример):
{
"status":"ok",
"domain":"public",
"total_files":10,
"total_size":1234567,
"folders":[{"name":"img","file_count":5,"size":1000000,"path":"public/img"}]
}
GET /download/{domain}/{folder}/{filename:path}
Скачивание файла.
Важно:
хотя параметр {filename:path} технически может содержать “/”, сервер отклонит имя, если в нём есть / или \ или ...
Response 200:
- бинарный поток (FileResponse),
application/octet-stream
Ошибки:
- 404 — файл не найден
- 403 — нет доступа к домену
Validate URL
POST /validate
Валидация ссылки (http/https). Делает HEAD, при необходимости GET range 0-0, возвращает HTTP статус и заголовки.
Если URL — CDN URL, дополнительно парсит domain/folder/filename и проверяет наличие на диске.
Body (JSON):
{"url":"https://example.com/file.png"}
Response 200 (схема, укорочено):
{
"status":"ok",
"checked_by":"ClientName",
"url":"https://example.com/file.png",
"is_valid_url":true,
"is_cdn_url":false,
"available":true,
"http_status":200,
"headers":{"content_type":"image/png","content_length":"12345","last_modified":null,"etag":null,"server":"nginx"},
"file_info":{"filename":"file.png","extension":"png","host":"example.com","path":"/file.png"},
"cdn_info":null,
"error":null
}
Delete file
DELETE /delete
Удаление файла по CDN URL.
Body (JSON):
{"url":"https://cdn.denchikp.com/public/files/a.txt"}
Response 200 (пример):
{
"status":"ok",
"message":"Файл успешно удален",
"domain":"public",
"folder":"files",
"filename":"a.txt",
"url":"https://cdn.denchikp.com/public/files/a.txt",
"owner":"ClientName",
"ip":"1.2.3.4"
}
Webhook event: DELETE (payload.source="api")
Text Editor (Read/Save)
Только для “текстовых” файлов и ограничение по размеру: ≤ 2 MB.
POST /file/content
Получить содержимое текстового файла.
Body (JSON):
{"url":"https://cdn.denchikp.com/public/files/app.js"}
Response 200 (пример):
{
"status":"ok",
"url":"https://cdn.denchikp.com/public/files/app.js",
"domain":"public",
"folder":"files",
"filename":"app.js",
"mime_type":"application/javascript",
"size":1200,
"content":"console.log('hi');\n"
}
Ошибки:
- 400 — файл не текстовый / >2MB / неподдерживаемая кодировка
- 404 — файл не найден
POST /file/save
Сохранить содержимое текстового файла.
Body (JSON):
{"url":"https://cdn.denchikp.com/public/files/app.js","content":"console.log('hi');\n"}
Response 200:
{"status":"ok","message":"Файл успешно сохранен","url":"...","domain":"public","folder":"files","filename":"app.js","size":1200}
Webhook event: FILE_SAVE
Webhooks (Reference)
Webhooks позволяют получать события от сервиса (UPLOAD/DELETE и т.д.) на ваш URL.
Создание / управление
POST /webhooks
Создать webhook.
Headers:
X-Api-Key: <API_KEY>
Body (JSON):
{
"url":"https://example.com/hook",
"events":["UPLOAD","DELETE"],
"secret":"optional",
"enabled":true
}
Response 200:
{"status":"ok","id":123}
Ошибки:
- 401/403 — нет доступа
GET /webhooks
Список webhooks текущего ключа.
Response 200 (пример):
{
"status":"ok",
"webhooks":[
{
"id":123,
"url":"https://example.com/hook",
"events":["UPLOAD","DELETE"],
"enabled":true,
"created_at":"2026-06-16T12:00:00"
}
]
}
DELETE /webhooks/{webhook_id}
Удалить webhook.
Path params:
webhook_id— integer
Response 200:
{"status":"ok"}
Ошибки:
- 404 — webhook не найден (или не принадлежит ключу)
Delivery format
Сервер отправляет на URL webhook’а запрос:
- Method: POST
- Content-Type: application/json
- Body:
{"event":"UPLOAD","payload":{...}}
События
События, которые реально используются в текущем коде:
UPLOADDELETEBULK_DELETEFILE_MOVEFILE_COPYFILE_SAVESCAN_FINISHED
Подпись (HMAC)
Если у webhook задан secret и на сервере включена подпись, рекомендуется добавлять:
Headers:
X-Webhook-Event: <EVENT>X-Webhook-Timestamp: <unix_seconds>X-Webhook-Signature: sha256=<hex>
Алгоритм:
- base =
{timestamp}.{raw_body} - signature =
HMAC_SHA256(secret, base)(hex) - получатель проверяет подпись и свежесть timestamp (например ±300 секунд)
Примечание: в текущем коде поле secret хранится, но подпись нужно добавить отдельным патчем (если ещё не добавлено).
Bot (internal)
Все эндпоинты /bot/* требуют заголовок X-Bot-Token.
| Header | Обязательно | Описание |
|---|---|---|
| X-Bot-Token | да | внутренний токен, должен совпадать с DISCORD_BOT_INTERNAL_TOKEN на сервере
|
Discord binding
POST /bot/discord/bind
Привязать Discord пользователя к API‑ключу (для бота).
Headers:
X-Bot-Token: <BOT_TOKEN>
Body (JSON):
{
"discord_id": "1234567890",
"api_key": "<API_KEY>",
"username": "optional",
"global_name": "optional",
"email": "optional",
"avatar": "optional"
}
Response 200 (пример):
{
"status": "ok",
"message": "Discord аккаунт успешно привязан к API ключу",
"discord_id": "1234567890",
"key_info": {
"name": "ClientName",
"domains": ["public"],
"enabled": true,
"is_admin": false,
"ips": []
}
}
Ошибки:
- 403 — недействительный
X-Bot-Token - 400 — API ключ не существует
POST /bot/discord/unbind
Отвязать Discord пользователя от API‑ключа.
Headers:
X-Bot-Token: <BOT_TOKEN>
Body (JSON):
{ "discord_id": "1234567890" }
Response 200:
{ "status": "ok", "message": "Discord аккаунт успешно отвязан от API ключа" }
Ошибки:
- 403 — недействительный
X-Bot-Token - 404 — привязка не найдена
GET /bot/discord/me/{discord_id}
Получить информацию о привязке Discord пользователя.
Headers:
X-Bot-Token: <BOT_TOKEN>
Path params:
discord_id— Discord ID
Response 200 (пример, если привязан):
{
"authenticated": true,
"bound": true,
"discord_id": "1234567890",
"user": { "name": "Display Name", "email": "[email protected]", "avatar": "https://..." },
"key_info": { "name":"ClientName","domains":["public"],"enabled":true,"is_admin":false,"ips":[] }
}
Response 404 (если не привязан):
{ "authenticated": false, "bound": false }
Bot files
POST /bot/upload
Загрузка файла через Discord‑бота (multipart/form-data).
Headers:
X-Bot-Token: <BOT_TOKEN>
Content-Type: multipart/form-data
Max file size: 500 MB
Form fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| discord_id | string | да | — | Discord ID |
| file | file | да | — | файл |
| domain | string | нет | "" | "" → public |
| name_mode | string | нет | uuid | original | custom |
| custom_filename | string | нет | "" | используется при custom |
| conflict_action | string | нет | "" | overwrite → перезаписать (с проверками) |
Response 200 (пример):
{
"status":"ok",
"url":"https://cdn.denchikp.com/public/img/...",
"size":12345,
"owner":"ClientName",
"domain":"public",
"folder":"img",
"filename":"..."
}
Response 409 (если имя занято и overwrite не задан):
{
"status":"conflict",
"message":"FILE_EXISTS",
"requested_filename":"a.png",
"existing_url":"https://cdn.denchikp.com/public/img/a.png",
"domain":"public",
"folder":"img"
}
Ошибки (частые):
- 401 — Discord аккаунт не привязан к API ключу
- 403 — нет доступа к домену / квота / overwrite запрещён
- 413 — файл слишком большой
- 500 — внутренняя ошибка
Пример (curl):
curl -X POST "https://cdnapi.denchikp.com/bot/upload" \ -H "X-Bot-Token: <BOT_TOKEN>" \ -F "discord_id=1234567890" \ -F "domain=public" \ -F "name_mode=uuid" \ -F "file=@./cat.png"
DELETE /bot/delete
Удалить файл через бота.
Headers:
X-Bot-Token: <BOT_TOKEN>
Body (JSON):
{ "discord_id":"1234567890", "url":"https://cdn.denchikp.com/public/files/a.txt" }
Response 200 (пример):
{
"status":"ok",
"message":"Файл успешно удален",
"domain":"public",
"folder":"files",
"filename":"a.txt",
"url":"https://cdn.denchikp.com/public/files/a.txt",
"owner":"ClientName"
}
Ошибки:
- 400 — некорректный URL / путь не файл
- 401 — Discord аккаунт не привязан
- 403 — нет доступа к домену
- 404 — файл не найден
GET /bot/download/{discord_id}/{domain}/{folder}/{filename:path}
Получить ссылки на скачивание (CDN URL + API download URL).
Headers:
X-Bot-Token: <BOT_TOKEN>
Response 200 (пример):
{
"status":"ok",
"owner":"ClientName",
"domain":"public",
"folder":"img",
"filename":"a.png",
"size":12345,
"cdn_url":"https://cdn.denchikp.com/public/img/a.png",
"api_download_url":"https://cdnapi.denchikp.com/download/public/img/a.png"
}
Ошибки:
- 400 — недопустимое имя файла (.. или / или \)
- 401 — Discord аккаунт не привязан
- 403 — нет доступа к домену
- 404 — файл не найден
POST /bot/browse
Просмотр файлов домена через бота.
Headers:
X-Bot-Token: <BOT_TOKEN>
Body (JSON):
{ "discord_id":"1234567890", "domain":"public", "folder":"img", "filter":"cat" }
Response 200 (пример, укорочено):
{ "status":"ok", "domain":"public", "total_files":1, "files":[{"name":"a.png","path":"img/a.png","size":12345,"url":"..."}] }
GET /bot/folders/{discord_id}/{domain}
Папки домена через бота + статистика по папкам.
Headers:
X-Bot-Token: <BOT_TOKEN>
Response 200 (пример):
{
"status":"ok",
"domain":"public",
"total_files":10,
"total_size":1234567,
"folders":[{"name":"img","file_count":5,"size":1000000,"path":"public/img"}]
}
POST /bot/validate
Валидация ссылки через бота (HEAD/GET, статус и заголовки). Для CDN URL также возвращает parsed info и exists_on_disk.
Headers:
X-Bot-Token: <BOT_TOKEN>
Body (JSON):
{ "discord_id":"1234567890", "url":"https://example.com/file.png" }
Response 200 (схема, укорочено):
{
"status":"ok",
"checked_by":"ClientName",
"url":"https://example.com/file.png",
"is_valid_url":true,
"is_cdn_url":false,
"available":true,
"http_status":200,
"headers":{"content_type":"image/png","content_length":"12345"},
"file_info":{"filename":"file.png","extension":"png","host":"example.com","path":"/file.png"},
"cdn_info":null,
"error":null
}
POST /bot/file/content
Получить содержимое текстового файла (предпросмотр для бота).
Ограничения:
- только текстовые файлы
- размер ≤ 2 MB
Body (JSON):
{ "discord_id":"1234567890", "url":"https://cdn.denchikp.com/public/files/app.js" }
Response 200 (пример):
{
"status":"ok",
"url":"https://cdn.denchikp.com/public/files/app.js",
"domain":"public",
"folder":"files",
"filename":"app.js",
"mime_type":"application/javascript",
"size":1200,
"content":"console.log('hi');\n"
}
Bot stats
GET /bot/public-summary
Публичная статистика CDN (вызов внутри бота). Фактически возвращает то же, что GET /stats/public-summary, но требует X-Bot-Token.
Response 200 (пример):
{
"status":"ok",
"stats":{
"uploaded_files":1000,
"current_files":900,
"total_size_bytes":123456789,
"total_size_mb":117.74,
"domains":10,
"clients":3,
"registered_api_clients":5
}
}
GET /bot/stats/{discord_id}
Админ‑статистика для бота: доступно только если привязанный к discord_id ключ имеет is_admin=true. Возвращает ту же структуру, что /bot/public-summary.
Ошибки:
- 403 — только администраторы могут просматривать статистику
- 401 — Discord аккаунт не привязан к API ключу