feat: storage writes via storage-agent, accepts_uploads and write_template (M5.5)
ci / api / api (push) Successful in 33s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 43s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m11s
ci / ts / ts (packages + web) (push) Successful in 1m10s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 51s

- storage-agent: POST /internal/files copies a staged file into a source
  (temp file, fsync, SHA-256 check, hard link so nothing is overwritten,
  " (2)" suffix on conflict, owner PIPU_FILE_UID/GID) and
  POST /internal/files/test. NFS sources that accept uploads are mounted
  rw in an agent-only /library-rw and bind-mounted read-only for others;
  local sources are written through /library-host-rw.
- meta: write templates ({track:02}, {disc:1-}, [optional]) with name
  sanitising and length limits, suggestion from the source's regexes
  and a reversibility check against 50 catalogue tracks.
- API and admin UI: accepts_uploads / write_template on sources,
  write-template suggest and check endpoints, write test result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 13:46:56 +03:00
1 parent 9b50038bb4
commit f66be2ba0a
27 files changed
+2422 -213

No files matched your search

+134
View File
@@ -432,6 +432,51 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/admin/sources/{id}/write-template/suggest": {
parameters: {
query?: never;
header?: never;
path: {
id: components["parameters"]["SourceId"];
};
cookie?: never;
};
get?: never;
put?: never;
/**
* Подобрать шаблон пути по существующей структуре (admin)
* @description Выражение хранилища, совпавшее с наибольшим числом путей выборки, превращается в шаблон.
* Не удалось (нет файлов или выражения не переводятся) — `422 library.template_unavailable`.
*/
post: operations["suggestWriteTemplate"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/admin/sources/{id}/write-template/check": {
parameters: {
query?: never;
header?: never;
path: {
id: components["parameters"]["SourceId"];
};
cookie?: never;
};
get?: never;
put?: never;
/**
* Проверить шаблон на обратимость (admin)
* @description Шаблон применяется к 50 трекам хранилища, пути разбираются его выражениями обратно.
*/
post: operations["checkWriteTemplate"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/admin/sources/{id}/scan": {
parameters: {
query?: never;
@@ -1108,6 +1153,10 @@ export interface components {
tag_priority: components["schemas"]["TagPriority"];
/** @description Автоматический скан, null — только вручную */
scan_interval_minutes: number | null;
/** @description Принимает импорт из очереди на добавление (NFS монтируется агентом ещё и на запись) */
accepts_uploads: boolean;
/** @description Шаблон пути новых файлов (docs/uploads.md#7), null — не задан */
write_template: string | null;
status: components["schemas"]["SourceStatus"];
status_message: string | null;
/** Format: date-time */
@@ -1128,6 +1177,8 @@ export interface components {
local_path?: string;
tag_priority?: components["schemas"]["TagPriority"];
scan_interval_minutes?: number | null;
accepts_uploads?: boolean;
write_template?: string;
};
SourcePatch: {
name?: string;
@@ -1140,12 +1191,45 @@ export interface components {
scan_interval_minutes?: number;
/** @description false — отключить хранилище (треки становятся недоступны), true — подключить снова */
enabled?: boolean;
/** @description Смена переподключает NFS-хранилище (rw-точка агента) */
accepts_uploads?: boolean;
/**
* @description Шаблон пути новых файлов; пусто — убрать. Неверный синтаксис — `422 validation.template_invalid`
* с `params.reason`. Проверка обратимости не обязательна для сохранения — её показывает `write-template/check`.
*/
write_template?: string;
};
SourceTestResult: {
ok: boolean;
message: string;
/** @description Сколько аудиофайлов найдено (проверка останавливается на 1000) */
audio_files: number;
/** @description Удалось создать и удалить `.pipu-write-test` (только для accepts_uploads; иначе null) */
write_ok?: boolean | null;
write_message?: string | null;
};
WriteTemplateSuggestion: {
template: string;
};
WriteTemplateCheckRequest: {
template: string;
};
WriteTemplateMismatch: {
/** @description Реальный путь файла */
rel_path: string;
/** @description Путь по шаблону, null — не построился */
rendered: string | null;
/** @description Поля, которые регэкспы хранилища распознали не так; `pattern` — не совпало ни одно выражение */
fields: string[];
error: string | null;
};
WriteTemplateCheck: {
/** @description Все пути разобраны обратно без изменений и ≥ 90% совпали с реальными */
ok: boolean;
total: number;
round_trip: number;
same_path: number;
mismatches: components["schemas"]["WriteTemplateMismatch"][];
};
PathPattern: {
regex: string;
@@ -2222,6 +2306,56 @@ export interface operations {
default: components["responses"]["Problem"];
};
};
suggestWriteTemplate: {
parameters: {
query?: never;
header?: never;
path: {
id: components["parameters"]["SourceId"];
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Предложенный шаблон */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["WriteTemplateSuggestion"];
};
};
default: components["responses"]["Problem"];
};
};
checkWriteTemplate: {
parameters: {
query?: never;
header?: never;
path: {
id: components["parameters"]["SourceId"];
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["WriteTemplateCheckRequest"];
};
};
responses: {
/** @description Итог проверки */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["WriteTemplateCheck"];
};
};
default: components["responses"]["Problem"];
};
};
scanSource: {
parameters: {
query?: never;