Открыть меню
Переключить меню настроек
Открыть персональное меню
Вы не представились системе
Ваш IP-адрес будет виден всем, если вы внесёте какие-либо изменения.

CDN:API Docs: различия между версиями

Материал из DenchikP Docs
Нет описания правки
Нет описания правки
 
(не показана 1 промежуточная версия этого же участника)
Строка 1: Строка 1:
= DP‑CDN‑API (v4.4.1) =
= CDN API (Reference) =
__TOC__


== Обзор ==
== Общие требования ==
'''Base URL:''' https://cdnapi.denchikp.com<br>
'''Base URL:''' https://cdnapi.denchikp.com<br>
'''CDN URL файлов:''' https://cdn.denchikp.com/&lt;domain&gt;/&lt;folder&gt;/&lt;filename&gt;<br>
'''CDN URL файлов:''' https://cdn.denchikp.com/&lt;domain&gt;/&lt;folder&gt;/&lt;filename&gt;
'''Rate limits:''' не применяются (не реализованы)


DP‑CDN‑API — API для загрузки/удаления/просмотра файлов в CDN, управления квотами, вебхуками, статистикой и админ‑операциями. Также есть внутренние эндпоинты для Discord‑бота.
Большинство CDN‑эндпоинтов требуют:
* заголовок <code>X-Api-Key: &lt;API_KEY&gt;</code> 
или
* активную cookie‑сессию, где ключ может резолвиться автоматически (OAuth/CDN login + bind).


''Важно:'' не публикуйте реальные API‑ключи. Если ключ попал в публичный доступ — выполните ротацию ключа (см. Admin → Rotate).
Во многих операциях, где передаётся <code>url</code>, принимаются '''только''' ссылки вида:
 
<pre>
== Термины и ограничения ==
https://cdn.denchikp.com/&lt;domain&gt;/&lt;folder&gt;/&lt;filename&gt;
 
</pre>
=== Домен (domain) ===
* Если <code>domain</code> пустой или не передан — используется <code>public</code>.
* Разрешён только основной домен: ровно одна точка (пример: <code>example.com</code>). Поддомены запрещены.
* Запрещены символы/паттерны: <code>..</code>, <code>/</code>, <code>\</code>.
* Значение <code>public</code> разрешено.
 
=== Папка (folder) ===
Допустимые папки (строго):
* <code>img</code>, <code>css</code>, <code>js</code>, <code>fonts</code>, <code>files</code>
 
=== Лимиты ===
* Максимальный размер одного файла: 500 MB
* Предпросмотр/редактор текста: максимум 2 MB
* Bulk delete: максимум 500 URL за запрос
* <code>GET /files</code>: <code>limit</code> максимум 200
 
== Аутентификация (Security) ==
 
=== API Key (основной способ) ===
Передавайте ключ в заголовке:
 
{| class="wikitable"
! Header !! Тип !! Обязательно !! Описание
|-
| X-Api-Key || string || да* || API‑ключ клиента
|}
 
''Примечание:'' часть эндпоинтов может работать через cookie‑сессию (OAuth/CDN login), но для API‑интеграций рекомендуется всегда использовать <code>X-Api-Key</code>.
 
=== Ограничения ключа ===
* <code>enabled=false</code> → 403
* IP allowlist: если у ключа задан список IP, запросы разрешены только с этих IP (используется <code>request.client.host</code>)
* Domain allowlist: если задан список доменов — доступ только к ним (и/или <code>public</code>, если он в списке)
* <code>is_admin=true</code>: доступ к админ‑эндпоинтам, SSE и админ‑статистике
 
=== Cookie‑сессии (для браузера) ===
Cookie‑сессия создаётся после:
* OAuth: <code>/auth/google</code>, <code>/auth/discord</code>, <code>/auth/github</code> (+ callback)
* CDN login по ключу: <code>/auth/cdn/login</code>
 
Если OAuth‑аккаунт привязан к API‑ключу (<code>/auth/bind-api-key</code>), ключ может резолвиться из сессии без заголовка.
 
=== Bot token (внутреннее API для Discord‑бота) ===
Все эндпоинты <code>/bot/*</code> требуют заголовок:
 
{| class="wikitable"
! Header !! Обязательно !! Описание
|-
| X-Bot-Token || да || должен совпадать с DISCORD_BOT_INTERNAL_TOKEN на сервере
|}
 
== Права и квоты ==
 
=== Админ‑доступ (is_admin) ===
Только админам:
* <code>/admin/*</code>
* <code>/events</code> (SSE)
* большинство <code>/stats/*</code> (кроме публичной сводки)
 
=== Квоты (для не‑админов) ===
Квоты применяются к операциям загрузки/копирования:
* <code>max_file_bytes</code>
* <code>max_total_bytes</code>
* <code>max_total_files</code>
* <code>max_monthly_upload_bytes</code>
* <code>max_monthly_upload_files</code>


Эндпоинты квот/использования:
== Ключ / домены / квоты ==
* <code>GET /quota</code>
* <code>GET /usage</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>/api-key/me</code> ===
* <code>PUT /admin/api-keys/{api_key_id}/quota</code>
Информация о текущем API‑ключе (по <code>X-Api-Key</code> или по сессии).


== Ошибки (Errors) ==
'''Headers:'''
 
* <code>X-Api-Key: &lt;API_KEY&gt;</code> (рекомендуется)
{| class="wikitable"
! Код !! Значение
|-
| 400 || Некорректные параметры / URL / имя файла / папка
|-
| 401 || Нет ключа (и нет валидной сессии)
|-
| 403 || Запрещено: ключ отключен / IP не разрешён / домен не разрешён / не админ / квоты
|-
| 404 || Не найдено (файл, upload_id, webhook, ключ, запись)
|-
| 409 || Конфликт имени файла (если не overwrite)
|-
| 413 || Слишком большой файл
|-
| 500 || Внутренняя ошибка
|}
 
FastAPI обычно возвращает:
<pre>
{"detail":"..."}
</pre>


Пример конфликта при загрузке (409):
'''Response 200:'''
<pre>
<pre>
{
{
   "status": "conflict",
   "status":"ok",
   "message": "FILE_EXISTS",
   "key":{
  "requested_filename": "a.png",
    "name":"ClientName",
  "existing_url": "https://cdn.denchikp.com/public/img/a.png",
    "is_admin":false,
  "domain": "public",
    "domains":["public"],
  "folder": "img"
    "ips":["1.2.3.4"]
  }
}
}
</pre>
</pre>


= Monitoring =
'''Ошибки:'''
* 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(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>/healthz</code> ==
----
Проверка «жив».


'''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‑ключ:
== <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>/readyz</code> ==
Проверка готовности: MySQL + BASE_PATH + свободное место.
 
'''Response 200'''
<pre>
<pre>
{
{"status":"ok","mode":"allowlist","domains":["public","example.com"]}
  "status": "ready",
  "mysql": "ok",
  "storage": {
    "free_bytes": 123,
    "total_bytes": 456
  }
}
</pre>
</pre>
 
* ключ без доменов (разрешён как минимум public):
== <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>/version</code> ==
Версия сервиса.
 
'''Response 200'''
<pre>
<pre>
{
{"status":"ok","mode":"any","domains":["public"]}
  "service": "DP-CDN-API",
  "version": "4.4.0",
  "time": "2026-06-16T12:00:00"
}
</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>/status</code> ==
----
Сводка состояния API+CDN (файлы/размер/домены, версия API).


= CDN Core =
=== <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> ===
Текущее использование (без лимитов квоты).


== <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> ==
'''Response 200 (пример):'''
Информация о текущем ключе.
 
'''Auth:''' <code>X-Api-Key</code> или cookie‑сессия
 
'''Response 200'''
<pre>
<pre>
{
{
   "status": "ok",
   "status":"ok",
   "key": {
   "usage":{
     "name": "ClientName",
     "total_files":120,
     "is_admin": false,
     "total_bytes":123456789,
     "domains": ["public"],
     "month":"2026-06",
     "ips": ["1.2.3.4"]
     "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>/domains</code> ==
----
Доступные домены для ключа.
 
'''Auth:''' требуется
 
'''Response 200 (пример)'''
<pre>
{"status":"ok","mode":"allowlist","domains":["public","example.com"]}
</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> ==
Текущее использование (без лимитов).


== <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> ===
Квота и текущее использование (usage + remaining).
Квоты + использование + остатки (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>


= Files (DB / Meta) =
'''Ошибки:'''
* 403 — если ключ невалиден/запрещён по IP и т.п.


== <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> ==
== Файлы (БД / meta / exists) ==
Список «моих» файлов из БД (с пагинацией).


'''Auth:''' требуется
=== <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> ===
Список «моих» файлов из БД.


Query parameters:
'''Headers:'''
* <code>X-Api-Key: &lt;API_KEY&gt;</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 || max 200
| 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> ==
----
Метаданные файла (диск + БД).


'''Auth:''' требуется 
=== <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:
Метаданные файла: сведения из БД + проверка на диске.
 
'''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> ===
Проверка существования файла (в БД и на диске).
Проверка существования файла (в БД и на диске).


'''Auth:''' требуется 
'''Body (JSON):'''
Body:
<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>


= Upload =
'''Ошибки:'''
* 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).


'''Auth:''' требуется  
'''Headers:'''
* <code>X-Api-Key: &lt;API_KEY&gt;</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>
curl -X POST "https://cdnapi.denchikp.com/upload" \
{
   -H "X-Api-Key: &lt;API_KEY&gt;" \
  "status":"ok",
   -F "domain=public" \
  "url":"https://cdn.denchikp.com/public/img/...",
   -F "name_mode=uuid" \
  "size":12345,
   -F "file=@./cat.png"
   "owner":"ClientName",
  "ip":"1.2.3.4",
   "domain":"public",
   "folder":"img",
   "filename":"..."
}
</pre>
</pre>


'''Response 200 (пример)'''
'''Response 409 (конфликт имени, если не overwrite):'''
<pre>
<pre>
{
{
   "status": "ok",
   "status":"conflict",
   "url": "https://cdn.denchikp.com/public/img/...",
   "message":"FILE_EXISTS",
  "size": 12345,
   "requested_filename":"a.png",
   "owner": "ClientName",
   "existing_url":"https://cdn.denchikp.com/public/img/a.png",
   "ip": "1.2.3.4",
   "domain":"public",
   "domain": "public",
   "folder":"img"
   "folder": "img",
  "filename": "..."
}
}
</pre>
</pre>


'''Response 409 (конфликт)''' — см. раздел Ошибки.
'''Ошибки (частые):'''
* 403 — квота превышена / домен не разрешён / IP не разрешён / overwrite запрещён
* 413 — файл слишком большой
* 500 внутренняя ошибка


Webhook event: <code>UPLOAD</code> (payload.source="api")
Webhook event: <code>UPLOAD</code> (payload.source="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>/upload/by-url</code> ==
Пример (curl):
Сервер скачивает файл по URL и кладёт в CDN.
<pre>
curl -X POST "https://cdnapi.denchikp.com/upload" \
  -H "X-Api-Key: &lt;API_KEY&gt;" \
  -F "domain=public" \
  -F "name_mode=uuid" \
  -F "file=@./cat.png"
</pre>
 
----


'''Auth:''' требуется 
=== <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> ===
Body:
Сервер скачивает файл по ссылке и сохраняет в 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 адреса
* 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‑сессию.


'''Auth:''' требуется 
'''Body (JSON):'''
Body:
<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":"&lt;uuid&gt;","chunk_size":8388608}
{"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> ==
----
Загрузка части (multipart/form-data).


'''Auth:''' требуется  
=== <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> ==
'''Ошибки:'''
Склеивает части, создаёт итоговый файл, проверяет квоты и max size.
* 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 — квота/права


'''Auth:''' требуется 
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‑сессии.


'''Auth:''' требуется
=== <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‑сессию и удалить временные части.


= File Operations =
'''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).
Удалить много файлов за один запрос (до 500 URL).


'''Auth:''' требуется 
'''Body (JSON):'''
Body:
<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":"https://cdn.denchikp.com/public/files/a.txt","status":"ok"},
     {"url":"...","status":"ok"},
     {"url":"https://cdn.denchikp.com/public/files/b.txt","status":"error","error":"..."}
     {"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> ==
----
Перемещение файла.


'''Auth:''' требуется 
=== <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:
Перемещение/переименование файла (можно менять домен/папку/имя).
 
'''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/rename</code> ==
----
Alias для <code>/file/move</code>. Требует <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> ==
=== <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>, но меняется только имя).


'''Auth:''' требуется 
'''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> ===
Посчитать хэш файла.


'''Auth:''' требуется 
'''Body (JSON):'''
Body:
<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>sha256</code>, <code>sha1</code>, <code>md5</code>.
<code>algo</code>: sha256 | sha1 | md5


= Browse / Folders / Download =
'''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> ===
Просмотр файлов на диске в домене.
Просмотр файлов на диске в домене.


'''Auth:''' требуется 
'''Body (JSON):'''
Body:
<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>
 
----


'''Auth:''' требуется
=== <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> ===
Скачивание файла.


== <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> ==
'''Важно:'''
Скачать файл (FileResponse).
хотя параметр <code>{filename:path}</code> технически может содержать “/”, сервер отклонит имя, если в нём есть <code>/</code> или <code>\</code> или <code>..</code>.


'''Auth:''' требуется 
'''Response 200:'''
Response: бинарный поток (application/octet-stream)
* бинарный поток (FileResponse), <code>application/octet-stream</code>


= Validate URL =
'''Ошибки:'''
* 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 ==
Проверяет URL (http/https), выполняет HEAD/GET и возвращает заголовки.   
 
=== <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> и проверяет наличие на диске.


'''Auth:''' требуется 
'''Body (JSON):'''
Body:
<pre>
<pre>
{"url":"https://example.com/file.png"}
{"url":"https://example.com/file.png"}
</pre>
</pre>


= Delete file =
'''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>


== <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> ==
== Delete file ==
Удалить файл по CDN URL.


'''Auth:''' требуется 
=== <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> ===
Body:
Удаление файла по 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.
* только «текстовые» расширения/MIME
* размер файла ≤ 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> ===
Получить содержимое текстового файла.
Получить содержимое текстового файла.


'''Auth:''' требуется 
'''Body (JSON):'''
Body:
<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> ===
Сохранить содержимое текстового файла.
Сохранить содержимое текстового файла.


'''Auth:''' требуется 
'''Body (JSON):'''
Body:
<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.


'''Auth:''' требуется 
'''Headers:'''
Body:
* <code>X-Api-Key: &lt;API_KEY&gt;</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 текущего ключа.


'''Auth:''' требуется
'''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.


'''Auth:''' требуется
'''Path params:'''
* <code>webhook_id</code> — integer


== Формат доставки ==
'''Response 200:'''
На URL вебхука отправляется POST JSON:
<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>, рекомендуется отправлять:
Если у webhook задан <code>secret</code> и на сервере включена подпись, рекомендуется добавлять:


Headers:
Headers:
* <code>X-Webhook-Event: UPLOAD</code>
* <code>X-Webhook-Event: &lt;EVENT&gt;</code>
* <code>X-Webhook-Timestamp: 1718550000</code>
* <code>X-Webhook-Timestamp: &lt;unix_seconds&gt;</code>
* <code>X-Webhook-Signature: sha256=&lt;hex&gt;</code>
* <code>X-Webhook-Signature: sha256=&lt;hex&gt;</code>


Строка 611: Строка 743:
* получатель проверяет подпись и свежесть timestamp (например ±300 секунд)
* получатель проверяет подпись и свежесть timestamp (например ±300 секунд)


= Admin =
''Примечание:'' в текущем коде поле <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: &lt;BOT_TOKEN&gt;</code>
 
'''Body (JSON):'''
<pre>
{
  "discord_id": "1234567890",
  "api_key": "&lt;API_KEY&gt;",
  "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: &lt;BOT_TOKEN&gt;</code>
 
'''Body (JSON):'''
<pre>
{ "discord_id": "1234567890" }
</pre>
 
'''Response 200:'''
<pre>
{ "status": "ok", "message": "Discord аккаунт успешно отвязан от API ключа" }
</pre>


Все admin endpoints требуют <code>is_admin=true</code>.
'''Ошибки:'''
* 403 — недействительный <code>X-Bot-Token</code>
* 404 — привязка не найдена


== Scan ==
----
* <code>POST /admin/scan/start</code>
* <code>GET /admin/scan/status</code>


Webhook event: <code>SCAN_FINISHED</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/discord/me/{discord_id}</code> ===
Получить информацию о привязке Discord пользователя.


== API Keys ==
'''Headers:'''
=== <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>/admin/api-keys</code> ===
* <code>X-Bot-Token: &lt;BOT_TOKEN&gt;</code>
Query: <code>show_full=false</code> (если true — вернёт ключи полностью)


=== <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>/admin/api-keys</code> ===
'''Path params:'''
Body:
* <code>discord_id</code> — Discord ID
 
'''Response 200 (пример, если привязан):'''
<pre>
<pre>
{
{
   "name":"Client",
   "authenticated": true,
   "enabled":true,
   "bound": true,
   "is_admin":false,
   "discord_id": "1234567890",
   "ips":["1.2.3.4"],
   "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>


=== <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(196,181,253,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#c4b5fd;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;">PATCH</span> <code>/admin/api-keys/{api_key_id}</code> ===
'''Response 404 (если не привязан):'''
Body:
<pre>
<pre>
{"name":"NewName","enabled":true,"is_admin":false}
{ "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>/admin/api-keys/{api_key_id}/rotate</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).


=== <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>/admin/api-keys/{api_key_id}</code> ===
'''Headers:'''
Удаление ключа.
* <code>X-Bot-Token: &lt;BOT_TOKEN&gt;</code>


=== IP allowlist ===
'''Content-Type:''' <code>multipart/form-data</code>
* <code>POST /admin/api-keys/{api_key_id}/ips</code> body: <code>{"value":"1.2.3.4"}</code>
'''Max file size:''' 500 MB
* <code>DELETE /admin/api-keys/{api_key_id}/ips/{ip}</code>


=== Domain allowlist ===
'''Form fields:'''
* <code>POST /admin/api-keys/{api_key_id}/domains</code> body: <code>{"value":"example.com"}</code>
{| class="wikitable"
* <code>DELETE /admin/api-keys/{api_key_id}/domains/{domain}</code>
! 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>


=== Quota ===
'''Response 409 (если имя занято и overwrite не задан):'''
<code>PUT /admin/api-keys/{api_key_id}/quota</code>
<pre>
<pre>
{
{
   "max_file_bytes": 524288000,
   "status":"conflict",
   "max_total_bytes": 10737418240,
   "message":"FILE_EXISTS",
   "max_total_files": 50000,
   "requested_filename":"a.png",
   "max_monthly_upload_bytes": 5368709120,
   "existing_url":"https://cdn.denchikp.com/public/img/a.png",
   "max_monthly_upload_files": null
   "domain":"public",
  "folder":"img"
}
}
</pre>
</pre>


== Admin: Files / Logs / SSE ==
'''Ошибки (частые):'''
=== <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>/admin/files</code> ===
* 401 — Discord аккаунт не привязан к API ключу
Query: <code>domain</code>, <code>owner</code>, <code>exists_on_disk</code>
* 403 — нет доступа к домену / квота / overwrite запрещён
* 413 — файл слишком большой
* 500 — внутренняя ошибка


=== <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>/admin/logs</code> ===
Пример (curl):
Query: <code>action</code>, <code>level</code>, <code>domain</code>, <code>owner</code>, <code>limit</code> (≤1000)
<pre>
curl -X POST "https://cdnapi.denchikp.com/bot/upload" \
  -H "X-Bot-Token: &lt;BOT_TOKEN&gt;" \
  -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(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>/events</code> (SSE) ===
----
Поток <code>text/event-stream</code>, event: <code>cdn_log</code>. 
Query: <code>last_id=0</code>.


= 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(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> ===
Удалить файл через бота.


== Public ==
'''Headers:'''
=== <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>/stats/public-summary</code> ===
* <code>X-Bot-Token: &lt;BOT_TOKEN&gt;</code>
Публичная краткая статистика (без ключа).


== Admin (is_admin) ==
'''Body (JSON):'''
* <code>POST /stats</code> — детальная статистика + графики (base64 PNG data URL)
<pre>
* <code>GET /stats/summary</code>
{ "discord_id":"1234567890", "url":"https://cdn.denchikp.com/public/files/a.txt" }
* <code>GET /stats/domains</code>
</pre>
* <code>GET /stats/realtime</code>
* <code>GET /stats/top-files?period=month&amp;by=downloads|size&amp;limit=50</code>
* <code>GET /stats/export?period=month&amp;kind=uploads|downloads|deletions|browses&amp;format=csv</code>


''Примечание по реализации:'' часть статистики берётся из MySQL таблиц (uploads/downloads/deletions/browses + files), часть — из SQLite файла <code>cdn_stats.db</code>.
'''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>


= Auth (sessions) =
'''Ошибки:'''
* 400 — некорректный URL / путь не файл
* 401 — Discord аккаунт не привязан
* 403 — нет доступа к домену
* 404 — файл не найден


== OAuth redirects/callbacks ==
----
* <code>GET /auth/google</code>, <code>/auth/google/callback</code>
* <code>GET /auth/discord</code>, <code>/auth/discord/callback</code>
* <code>GET /auth/github</code>, <code>/auth/github/callback</code>


== CDN session login ==
=== <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(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>/auth/cdn/login</code> ===
Получить ссылки на скачивание (CDN URL + API download URL).
Body:
 
'''Headers:'''
* <code>X-Bot-Token: &lt;BOT_TOKEN&gt;</code>
 
'''Response 200 (пример):'''
<pre>
<pre>
{"api_key":"&lt;API_KEY&gt;"}
{
  "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>


=== <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>/auth/cdn/logout</code> ===
'''Ошибки:'''
Выход из CDN‑сессии.
* 400 — недопустимое имя файла (.. или / или \)
* 401 — Discord аккаунт не привязан
* 403 — нет доступа к домену
* 404 — файл не найден


== Current session ==
----
=== <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>/auth/me</code> ===
Возвращает текущую активную сессию (cdn/google/discord/github) и информацию о привязанном ключе (если есть).


== Bind/unbind API key ==
=== <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>/auth/bind-api-key</code> ===
Просмотр файлов домена через бота.
Body:
 
'''Headers:'''
* <code>X-Bot-Token: &lt;BOT_TOKEN&gt;</code>
 
'''Body (JSON):'''
<pre>
<pre>
{"api_key":"&lt;API_KEY&gt;"}
{ "discord_id":"1234567890", "domain":"public", "folder":"img", "filter":"cat" }
</pre>
</pre>


Требуется активная OAuth‑сессия (google/discord/github).
'''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(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>/auth/unbind-api-key</code> ===
----
Отвязка ключа от текущей OAuth‑сессии.


== Logout ==
=== <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>/auth/logout</code> ===
Папки домена через бота + статистика по папкам.
Выход из любой активной сессии (очистка cookie‑состояния).


= Bot (internal) =
'''Headers:'''
* <code>X-Bot-Token: &lt;BOT_TOKEN&gt;</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: &lt;BOT_TOKEN&gt;</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> ===
Получить содержимое текстового файла (предпросмотр для бота).


Все <code>/bot/*</code> требуют <code>X-Bot-Token</code>.
'''Ограничения:'''
* только текстовые файлы
* размер ≤ 2 MB


== Discord binding ==
'''Body (JSON):'''
* <code>POST /bot/discord/bind</code>
<pre>
* <code>POST /bot/discord/unbind</code>
{ "discord_id":"1234567890", "url":"https://cdn.denchikp.com/public/files/app.js" }
* <code>GET /bot/discord/me/{discord_id}</code>
</pre>


== Bot files ==
'''Response 200 (пример):'''
* <code>POST /bot/upload</code> (multipart/form-data: discord_id, file, domain, name_mode, custom_filename, conflict_action)
<pre>
* <code>DELETE /bot/delete</code> body: <code>{"discord_id":"...","url":"https://cdn.denchikp.com/..."}</code>
{
* <code>GET /bot/download/{discord_id}/{domain}/{folder}/{filename:path}</code>
  "status":"ok",
* <code>POST /bot/browse</code>
  "url":"https://cdn.denchikp.com/public/files/app.js",
* <code>GET /bot/folders/{discord_id}/{domain}</code>
  "domain":"public",
* <code>POST /bot/validate</code>
  "folder":"files",
* <code>POST /bot/file/content</code> (только текст, ≤2MB)
  "filename":"app.js",
  "mime_type":"application/javascript",
  "size":1200,
  "content":"console.log('hi');\n"
}
</pre>


== Bot stats ==
== Bot stats ==
* <code>GET /bot/public-summary</code>
 
* <code>GET /bot/stats/{discord_id}</code> (только если привязанный ключ is_admin=true)
=== <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:

Ошибки:

Загрузка

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/files
  • new_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":{...}}

События

События, которые реально используются в текущем коде:

  • UPLOAD
  • DELETE
  • BULK_DELETE
  • FILE_MOVE
  • FILE_COPY
  • FILE_SAVE
  • SCAN_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 ключу
Содержание