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

+43 -2
View File
@@ -63,7 +63,7 @@ func (h *Handler) sourceDTO(s Source) gen.Source {
Id: s.ID, Name: s.Name, Type: gen.SourceType(s.Type), NfsServer: s.NFSServer, NfsExport: s.NFSExport,
NfsOptions: s.NFSOptions, LocalPath: s.LocalPath, TagPriority: gen.TagsFirst,
Status: gen.SourceStatus(s.Status), StatusMessage: s.StatusMessage, LastScanAt: s.LastScanAt,
CreatedAt: s.CreatedAt,
CreatedAt: s.CreatedAt, AcceptsUploads: s.AcceptsUploads, WriteTemplate: s.WriteTemplate,
}
if !s.TagsFirst {
out.TagPriority = gen.PathFirst
@@ -118,6 +118,7 @@ func (h *Handler) CreateSource(ctx context.Context, req gen.CreateSourceRequestO
src, err := h.svc.CreateSource(ctx, SourceInput{
Name: &b.Name, Type: SourceType(b.Type), NFSServer: b.NfsServer, NFSExport: b.NfsExport, NFSOptions: b.NfsOptions,
LocalPath: b.LocalPath, TagsFirst: tagsFirst(b.TagPriority), ScanIntervalMinutes: b.ScanIntervalMinutes,
AcceptsUploads: b.AcceptsUploads, WriteTemplate: b.WriteTemplate,
})
if err != nil {
return nil, err
@@ -140,6 +141,7 @@ func (h *Handler) UpdateSource(ctx context.Context, req gen.UpdateSourceRequestO
src, err := h.svc.UpdateSource(ctx, req.Id, SourceInput{
Name: b.Name, NFSServer: b.NfsServer, NFSExport: b.NfsExport, NFSOptions: b.NfsOptions, LocalPath: b.LocalPath,
TagsFirst: tagsFirst(b.TagPriority), ScanIntervalMinutes: b.ScanIntervalMinutes, Enabled: b.Enabled,
AcceptsUploads: b.AcceptsUploads, WriteTemplate: b.WriteTemplate,
})
if err != nil {
return nil, err
@@ -161,7 +163,46 @@ func (h *Handler) TestSource(ctx context.Context, req gen.TestSourceRequestObjec
if err != nil {
return nil, err
}
return gen.TestSource200JSONResponse{Ok: r.OK, Message: r.Message, AudioFiles: r.AudioFiles}, nil
out := gen.TestSource200JSONResponse{Ok: r.OK, Message: r.Message, AudioFiles: r.AudioFiles}
if r.Write != nil {
out.WriteOk, out.WriteMessage = &r.Write.OK, &r.Write.Message
}
return out, nil
}
// SuggestWriteTemplate реализует gen.StrictServerInterface.
func (h *Handler) SuggestWriteTemplate(ctx context.Context, req gen.SuggestWriteTemplateRequestObject) (gen.SuggestWriteTemplateResponseObject, error) {
tpl, err := h.svc.SuggestWriteTemplate(ctx, req.Id)
if err != nil {
return nil, err
}
return gen.SuggestWriteTemplate200JSONResponse{Template: tpl}, nil
}
// CheckWriteTemplate реализует gen.StrictServerInterface.
func (h *Handler) CheckWriteTemplate(ctx context.Context, req gen.CheckWriteTemplateRequestObject) (gen.CheckWriteTemplateResponseObject, error) {
res, err := h.svc.CheckWriteTemplate(ctx, req.Id, req.Body.Template)
if err != nil {
return nil, err
}
out := gen.CheckWriteTemplate200JSONResponse{
Ok: res.OK, Total: res.Total, RoundTrip: res.RoundTrip, SamePath: res.SamePath,
Mismatches: make([]gen.WriteTemplateMismatch, 0, len(res.Mismatches)),
}
for _, m := range res.Mismatches {
dto := gen.WriteTemplateMismatch{RelPath: m.RelPath, Fields: m.Fields}
if dto.Fields == nil {
dto.Fields = []string{}
}
if m.Rendered != "" {
dto.Rendered = &m.Rendered
}
if m.Error != "" {
dto.Error = &m.Error
}
out.Mismatches = append(out.Mismatches, dto)
}
return out, nil
}
func patternsDTO(rows []PatternRow) gen.PatternList {