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 ключу