Дополнительные действия
Denchikp (обсуждение | вклад) Нет описания правки |
Denchikp (обсуждение | вклад) Нет описания правки |
||
| Строка 744: | Строка 744: | ||
''Примечание:'' в текущем коде поле <code>secret</code> хранится, но подпись нужно добавить отдельным патчем (если ещё не добавлено). | ''Примечание:'' в текущем коде поле <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> | |||
{ | |||
"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":[] } | |||
} | |||
</pre> | |||
'''Response 404 (если не привязан):''' | |||
<pre> | |||
{ "authenticated": false, "bound": false } | |||
</pre> | |||
== 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> | |||
{ | |||
"status":"conflict", | |||
"message":"FILE_EXISTS", | |||
"requested_filename":"a.png", | |||
"existing_url":"https://cdn.denchikp.com/public/img/a.png", | |||
"domain":"public", | |||
"folder":"img" | |||
} | |||
</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> === | |||
Получить ссылки на скачивание (CDN URL + API download URL). | |||
'''Headers:''' | |||
* <code>X-Bot-Token: <BOT_TOKEN></code> | |||
'''Response 200 (пример):''' | |||
<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> | |||
'''Ошибки:''' | |||
* 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> === | |||
Просмотр файлов домена через бота. | |||
'''Headers:''' | |||
* <code>X-Bot-Token: <BOT_TOKEN></code> | |||
'''Body (JSON):''' | |||
<pre> | |||
{ "discord_id":"1234567890", "domain":"public", "folder":"img", "filter":"cat" } | |||
</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> === | |||
Папки домена через бота + статистика по папкам. | |||
'''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 == | |||
=== <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 ключу