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

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

Материал из DenchikP Docs
Нет описания правки
Нет описания правки
Строка 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>.
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/api-key/me</code> ===
 
Информация о текущем API‑ключе (по <code>X-Api-Key</code> или по сессии).
=== Ограничения ключа ===
* <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>
 
Админ меняет квоты:
* <code>PUT /admin/api-keys/{api_key_id}/quota</code>
 
== Ошибки (Errors) ==
 
{| class="wikitable"
! Код !! Значение
|-
| 400 || Некорректные параметры / URL / имя файла / папка
|-
| 401 || Нет ключа (и нет валидной сессии)
|-
| 403 || Запрещено: ключ отключен / IP не разрешён / домен не разрешён / не админ / квоты
|-
| 404 || Не найдено (файл, upload_id, webhook, ключ, запись)
|-
| 409 || Конфликт имени файла (если не overwrite)
|-
| 413 || Слишком большой файл
|-
| 500 || Внутренняя ошибка
|}


FastAPI обычно возвращает:
'''Headers:'''
<pre>
* <code>X-Api-Key: &lt;API_KEY&gt;</code> (рекомендуется)
{"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>/api-key/me</code> ==
Информация о текущем ключе.


'''Auth:''' <code>X-Api-Key</code> или cookie‑сессия
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/usage</code> ===
Текущее использование (без лимитов квоты).


'''Response 200'''
'''Response 200 (пример):'''
<pre>
<pre>
{
{
   "status": "ok",
   "status":"ok",
   "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:''' требуется
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/quota</code> ===
Квоты + использование + остатки (remaining).


'''Response 200 (пример)'''
'''Response 200 (пример):'''
<pre>
{"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> ==
Квота и текущее использование (usage + remaining).
 
'''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:''' требуется 
----
Body:
 
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/upload/by-url</code> ===
Сервер скачивает файл по ссылке и сохраняет в CDN.
 
'''Body (JSON):'''
<pre>
<pre>
{"source_url":"https://example.com/file.png","domain":"public","folder":"img"}
{
  "source_url":"https://example.com/file.png",
  "domain":"public",
  "folder":"img"
}
</pre>
</pre>


Ограничения:
'''Ограничения:'''
* только http/https
* только http/https
* SSRF защита: запрещены private/loopback/link-local/reserved адреса
* 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).
 
=== <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).


'''Auth:''' требуется  
'''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> ==
----
Перемещение файла.
 
=== <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:''' требуется 
'''Body (JSON):'''
Body:
<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/rename</code> ===
Переименовать файл (alias для <code>/file/move</code>, но меняется только имя).
 
'''Body (JSON):'''
<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> ==
----
Копирование файла (учитывает квоты как новая загрузка).


'''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/copy</code> ===
Body:
Копирование файла (квоты учитываются как новая загрузка).
 
'''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> — подстрока в имени файла (опционально)


'''Auth:''' требуется
'''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>/download/{domain}/{folder}/{filename:path}</code> ==
----
Скачать файл (FileResponse).


'''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>/folders/{domain}</code> ===
Response: бинарный поток (application/octet-stream)
Список папок домена + агрегаты.


= Validate URL =
'''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>/validate</code> ==
----
Проверяет 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(96,165,250,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#60a5fa;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">GET</span> <code>/download/{domain}/{folder}/{filename:path}</code> ===
Скачивание файла.
 
'''Важно:'''
хотя параметр <code>{filename:path}</code> технически может содержать “/”, сервер отклонит имя, если в нём есть <code>/</code> или <code>\</code> или <code>..</code>.
 
'''Response 200:'''
* бинарный поток (FileResponse), <code>application/octet-stream</code>
 
'''Ошибки:'''
* 404 — файл не найден
* 403 — нет доступа к домену
 
== Validate URL ==
 
=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(52,211,153,.55);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 6px 18px rgba(0,0,0,.18);color:#34d399;font-weight:800;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.78em;letter-spacing:.10em;text-transform:uppercase;line-height:1.1;vertical-align:middle;">POST</span> <code>/validate</code> ===
Валидация ссылки (http/https). Делает HEAD, при необходимости GET range 0-0, возвращает HTTP статус и заголовки.   
Если URL — CDN URL, дополнительно парсит <code>domain/folder/filename</code> и проверяет наличие на диске.
Если URL — CDN URL, дополнительно парсит <code>domain/folder/filename</code> и проверяет наличие на диске.


'''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 (пример):'''
Сохранить содержимое текстового файла.
 
'''Auth:''' требуется 
Body:
<pre>
<pre>
{"url":"https://cdn.denchikp.com/public/files/app.js","content":"console.log('hi');\n"}
{
  "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>
</pre>


Webhook event: <code>FILE_SAVE</code>
'''Ошибки:'''
* 400 — файл не текстовый / >2MB / неподдерживаемая кодировка
* 404 — файл не найден


= Webhooks =
----


== <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>/file/save</code> ===
Создать webhook.
Сохранить содержимое текстового файла.


'''Auth:''' требуется 
'''Body (JSON):'''
Body:
<pre>
<pre>
{
{"url":"https://cdn.denchikp.com/public/files/app.js","content":"console.log('hi');\n"}
  "url":"https://example.com/hook",
  "events":["UPLOAD","DELETE"],
  "secret":"optional",
  "enabled":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>/webhooks</code> ==
'''Response 200:'''
Список webhooks текущего ключа.
 
'''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>/webhooks/{webhook_id}</code> ==
Удалить webhook.
 
'''Auth:''' требуется
 
== Формат доставки ==
На URL вебхука отправляется POST JSON:
<pre>
<pre>
{"event":"UPLOAD","payload":{...}}
{"status":"ok","message":"Файл успешно сохранен","url":"...","domain":"public","folder":"files","filename":"app.js","size":1200}
</pre>
</pre>


== События ==
Webhook event: <code>FILE_SAVE</code>
* UPLOAD
* DELETE
* BULK_DELETE
* FILE_MOVE
* FILE_COPY
* FILE_SAVE
* SCAN_FINISHED


== Подпись (HMAC) ==
----
Если на сервере включена подпись и у webhook задан <code>secret</code>, рекомендуется отправлять:


Headers:
= Webhooks (Reference) =
* <code>X-Webhook-Event: UPLOAD</code>
* <code>X-Webhook-Timestamp: 1718550000</code>
* <code>X-Webhook-Signature: sha256=&lt;hex&gt;</code>


Алгоритм:
Webhooks позволяют получать события от сервиса (UPLOAD/DELETE и т.д.) на ваш URL.
* base = <code>{timestamp}.{raw_body}</code>
* signature = <code>HMAC_SHA256(secret, base)</code> (hex)
* получатель проверяет подпись и свежесть timestamp (например ±300 секунд)


= Admin =
== Создание / управление ==


Все admin endpoints требуют <code>is_admin=true</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.


== Scan ==
'''Headers:'''
* <code>POST /admin/scan/start</code>
* <code>X-Api-Key: &lt;API_KEY&gt;</code>
* <code>GET /admin/scan/status</code>


Webhook event: <code>SCAN_FINISHED</code>
'''Body (JSON):'''
 
== API Keys ==
=== <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> ===
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> ===
Body:
<pre>
<pre>
{
{
   "name":"Client",
   "url":"https://example.com/hook",
   "enabled":true,
   "events":["UPLOAD","DELETE"],
  "is_admin":false,
   "secret":"optional",
   "ips":["1.2.3.4"],
   "enabled":true
   "domains":["public"]
}
}
</pre>
</pre>


=== <span style="display:inline-flex;align-items:center;justify-content:center;padding:2px 10px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(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 200:'''
Body:
<pre>
<pre>
{"name":"NewName","enabled":true,"is_admin":false}
{"status":"ok","id":123}
</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> ===
'''Ошибки:'''
Ротация ключа.
* 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(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> ===
----
Удаление ключа.


=== IP 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>/webhooks</code> ===
* <code>POST /admin/api-keys/{api_key_id}/ips</code> body: <code>{"value":"1.2.3.4"}</code>
Список webhooks текущего ключа.
* <code>DELETE /admin/api-keys/{api_key_id}/ips/{ip}</code>
 
=== Domain allowlist ===
* <code>POST /admin/api-keys/{api_key_id}/domains</code> body: <code>{"value":"example.com"}</code>
* <code>DELETE /admin/api-keys/{api_key_id}/domains/{domain}</code>


=== Quota ===
'''Response 200 (пример):'''
<code>PUT /admin/api-keys/{api_key_id}/quota</code>
<pre>
<pre>
{
{
   "max_file_bytes": 524288000,
   "status":"ok",
   "max_total_bytes": 10737418240,
   "webhooks":[
  "max_total_files": 50000,
    {
  "max_monthly_upload_bytes": 5368709120,
      "id":123,
  "max_monthly_upload_files": null
      "url":"https://example.com/hook",
      "events":["UPLOAD","DELETE"],
      "enabled":true,
      "created_at":"2026-06-16T12:00:00"
    }
  ]
}
}
</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> ===
Query: <code>domain</code>, <code>owner</code>, <code>exists_on_disk</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>/admin/logs</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> ===
Query: <code>action</code>, <code>level</code>, <code>domain</code>, <code>owner</code>, <code>limit</code> (≤1000)
Удалить webhook.
 
=== <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 =
 
== 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>/stats/public-summary</code> ===
Публичная краткая статистика (без ключа).
 
== Admin (is_admin) ==
* <code>POST /stats</code> — детальная статистика + графики (base64 PNG data URL)
* <code>GET /stats/summary</code>
* <code>GET /stats/domains</code>
* <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>.
 
= Auth (sessions) =


== OAuth redirects/callbacks ==
'''Path params:'''
* <code>GET /auth/google</code>, <code>/auth/google/callback</code>
* <code>webhook_id</code> — integer
* <code>GET /auth/discord</code>, <code>/auth/discord/callback</code>
* <code>GET /auth/github</code>, <code>/auth/github/callback</code>


== CDN session login ==
'''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(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> ===
Body:
<pre>
<pre>
{"api_key":"&lt;API_KEY&gt;"}
{"status":"ok"}
</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‑сессии.
* 404 — webhook не найден (или не принадлежит ключу)


== Current session ==
== Delivery format ==
=== <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 ==
Сервер отправляет на URL webhook’а запрос:
=== <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> ===
* Method: POST
Body:
* Content-Type: application/json
* Body:
<pre>
<pre>
{"api_key":"&lt;API_KEY&gt;"}
{"event":"UPLOAD","payload":{...}}
</pre>
</pre>


Требуется активная OAuth‑сессия (google/discord/github).
== События ==
События, которые реально используются в текущем коде:
* <code>UPLOAD</code>
* <code>DELETE</code>
* <code>BULK_DELETE</code>
* <code>FILE_MOVE</code>
* <code>FILE_COPY</code>
* <code>FILE_SAVE</code>
* <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(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> ===
== Подпись (HMAC) ==
Отвязка ключа от текущей OAuth‑сессии.
Если у webhook задан <code>secret</code> и на сервере включена подпись, рекомендуется добавлять:


== Logout ==
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>/auth/logout</code> ===
* <code>X-Webhook-Event: &lt;EVENT&gt;</code>
Выход из любой активной сессии (очистка cookie‑состояния).
* <code>X-Webhook-Timestamp: &lt;unix_seconds&gt;</code>
* <code>X-Webhook-Signature: sha256=&lt;hex&gt;</code>


= Bot (internal) =
Алгоритм:
 
* base = <code>{timestamp}.{raw_body}</code>
Все <code>/bot/*</code> требуют <code>X-Bot-Token</code>.
* signature = <code>HMAC_SHA256(secret, base)</code> (hex)
 
* получатель проверяет подпись и свежесть timestamp (например ±300 секунд)
== Discord binding ==
* <code>POST /bot/discord/bind</code>
* <code>POST /bot/discord/unbind</code>
* <code>GET /bot/discord/me/{discord_id}</code>
 
== Bot files ==
* <code>POST /bot/upload</code> (multipart/form-data: discord_id, file, domain, name_mode, custom_filename, conflict_action)
* <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>
* <code>POST /bot/browse</code>
* <code>GET /bot/folders/{discord_id}/{domain}</code>
* <code>POST /bot/validate</code>
* <code>POST /bot/file/content</code> (только текст, ≤2MB)


== Bot stats ==
''Примечание:'' в текущем коде поле <code>secret</code> хранится, но подпись нужно добавить отдельным патчем (если ещё не добавлено).
* <code>GET /bot/public-summary</code>
* <code>GET /bot/stats/{discord_id}</code> (только если привязанный ключ is_admin=true)

Версия от 00:42, 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 хранится, но подпись нужно добавить отдельным патчем (если ещё не добавлено).