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
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:
1 parent
9b50038bb4
commit
f66be2ba0a
27 files changed
+2422
-213
No files matched your search
@@ -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;
|
||||
|
||||
@@ -159,5 +159,29 @@
|
||||
"wave_listen": "Played ≥ 50%",
|
||||
"wave_early_skip": "Early skips",
|
||||
"wave_likes": "Likes per 100"
|
||||
},
|
||||
"write_template": {
|
||||
"title": "Uploads and path template",
|
||||
"intro": "Approved tracks from the upload queue are placed here using a template that repeats this storage's folder layout.",
|
||||
"accepts": "Accept uploads into this storage",
|
||||
"nfs_rw_hint": "The storage will be reconnected: the agent also mounts it for writing (visible only to the agent). The NFS export must allow writes for PIPU_FILE_UID.",
|
||||
"template": "Path template",
|
||||
"hint": "For example {albumartist}/{year} - {album}/{disc:1-}{track:02} - {title}.{ext}; [ … ] is an optional part",
|
||||
"suggest": "Suggest from layout",
|
||||
"check": "Check",
|
||||
"save": "Save",
|
||||
"saved": "Saved",
|
||||
"required": "Set a path template to accept uploads.",
|
||||
"no_tracks": "The storage has no tracks yet, so there is nothing to check the template against.",
|
||||
"check_ok": "The template is reversible: {{round_trip}} of {{total}} paths parse back unchanged, {{same_path}} match the real ones.",
|
||||
"check_warn": "Mismatches found: {{round_trip}} of {{total}} parse back unchanged, {{same_path}} match the real paths (90% needed). New files may be laid out differently from existing ones.",
|
||||
"col_real": "Real path",
|
||||
"col_rendered": "From template",
|
||||
"col_problem": "Mismatch",
|
||||
"no_pattern": "no pattern matched",
|
||||
"other_path": "different path",
|
||||
"badge": "accepts uploads",
|
||||
"write_ok": "Write: ok",
|
||||
"write_fail": "Write failed: {{message}}"
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,8 @@
|
||||
"source_type_invalid": "Unknown storage type",
|
||||
"query_empty": "Enter a query",
|
||||
"wave_activity_invalid": "Unknown activity",
|
||||
"wave_mood_invalid": "Unknown mood"
|
||||
"wave_mood_invalid": "Unknown mood",
|
||||
"template_invalid": "Invalid path template: {{reason}}"
|
||||
},
|
||||
"users": {
|
||||
"not_found": "User not found",
|
||||
@@ -56,7 +57,8 @@
|
||||
"library": {
|
||||
"source_not_found": "Storage not found",
|
||||
"scan_running": "A scan is already running",
|
||||
"source_offline": "The storage is not connected"
|
||||
"source_offline": "The storage is not connected",
|
||||
"template_unavailable": "Could not suggest a template: no storage pattern converts to a template, or the storage has no files"
|
||||
},
|
||||
"catalog": {
|
||||
"not_found": "Not found"
|
||||
|
||||
@@ -159,5 +159,29 @@
|
||||
"wave_listen": "Дослушано ≥ 50%",
|
||||
"wave_early_skip": "Ранние скипы",
|
||||
"wave_likes": "Лайков на 100"
|
||||
},
|
||||
"write_template": {
|
||||
"title": "Приём загрузок и шаблон пути",
|
||||
"intro": "Сюда раскладываются одобренные треки из очереди на добавление — по шаблону, который повторяет структуру папок этого хранилища.",
|
||||
"accepts": "Принимать загрузки в это хранилище",
|
||||
"nfs_rw_hint": "Хранилище будет переподключено: агент смонтирует его ещё и на запись (видно только ему). На NFS-сервере нужны права записи для PIPU_FILE_UID.",
|
||||
"template": "Шаблон пути",
|
||||
"hint": "Например {albumartist}/{year} - {album}/{disc:1-}{track:02} - {title}.{ext}; [ … ] — необязательная часть",
|
||||
"suggest": "Подобрать по структуре",
|
||||
"check": "Проверить",
|
||||
"save": "Сохранить",
|
||||
"saved": "Сохранено",
|
||||
"required": "Чтобы принимать загрузки, задайте шаблон пути.",
|
||||
"no_tracks": "В хранилище пока нет треков — проверить шаблон не на чем.",
|
||||
"check_ok": "Шаблон обратим: {{round_trip}} из {{total}} путей разобраны без изменений, {{same_path}} совпали с реальными.",
|
||||
"check_warn": "Есть расхождения: разобрано без изменений {{round_trip}} из {{total}}, совпало с реальными {{same_path}} (нужно ≥ 90%). Новые файлы могут лечь не так, как старые.",
|
||||
"col_real": "Реальный путь",
|
||||
"col_rendered": "По шаблону",
|
||||
"col_problem": "Расхождение",
|
||||
"no_pattern": "не разобрал ни один регэксп",
|
||||
"other_path": "другой путь",
|
||||
"badge": "принимает загрузки",
|
||||
"write_ok": "Запись: ок",
|
||||
"write_fail": "Запись не удалась: {{message}}"
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,8 @@
|
||||
"source_type_invalid": "Неизвестный тип хранилища",
|
||||
"query_empty": "Введите запрос",
|
||||
"wave_activity_invalid": "Неизвестное занятие",
|
||||
"wave_mood_invalid": "Неизвестное настроение"
|
||||
"wave_mood_invalid": "Неизвестное настроение",
|
||||
"template_invalid": "Неверный шаблон пути: {{reason}}"
|
||||
},
|
||||
"users": {
|
||||
"not_found": "Пользователь не найден",
|
||||
@@ -56,7 +57,8 @@
|
||||
"library": {
|
||||
"source_not_found": "Хранилище не найдено",
|
||||
"scan_running": "Скан уже идёт",
|
||||
"source_offline": "Хранилище не подключено"
|
||||
"source_offline": "Хранилище не подключено",
|
||||
"template_unavailable": "Не удалось подобрать шаблон: ни один регэксп хранилища не переводится в шаблон или в хранилище нет файлов"
|
||||
},
|
||||
"catalog": {
|
||||
"not_found": "Не найдено"
|
||||
|
||||
Reference in new issue
Block a user