ci / api / api (push) Successful in 26s
ci / mobile / mobile (Metro-бандл Android) (push) Failing after 22s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 37s
ci / ts / ts (packages + web) (push) Successful in 51s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 53s
- schema for sources, path patterns, media files, artists, albums, tracks, genres (migration 00003) - path patterns (RE2 with named groups, skip), merge with tags by source priority, fallbacks from the file name and folder, name normalization and multi-artist split - scanner: walk, skip unchanged files by size and mtime, ffprobe in 8 workers with a single catalog writer, renames by content hash keep the track, duplicates fold into one track with the best file as primary, missing files are kept 30 days - storage-agent mounts NFS with validated server, export and an option whitelist, no shell; test mount counts audio files - admin API: sources CRUD, connection test, patterns with preview, scan, library status, file errors, host folder picker; scheduled scans and health checks; mounts are restored on start - catalog API: artist, artist albums, album with tracks, track - api image is alpine with ffmpeg for ffprobe Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
3.0 KiB
Go
106 lines
3.0 KiB
Go
package library
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"pipu/api/internal/storageagent"
|
|
)
|
|
|
|
// Agent — клиент storage-agent (монтирование NFS).
|
|
type Agent interface {
|
|
Mount(ctx context.Context, sourceID uuid.UUID, spec storageagent.NFSSpec) (storageagent.MountResult, error)
|
|
Unmount(ctx context.Context, sourceID uuid.UUID) error
|
|
Test(ctx context.Context, spec storageagent.NFSSpec) (storageagent.MountResult, error)
|
|
}
|
|
|
|
// HTTPAgent ходит в storage-agent по внутренней сети с общим секретом.
|
|
type HTTPAgent struct {
|
|
BaseURL string
|
|
Token string
|
|
Client *http.Client
|
|
}
|
|
|
|
// NewHTTPAgent создаёт клиента агента.
|
|
func NewHTTPAgent(baseURL, token string) *HTTPAgent {
|
|
return &HTTPAgent{BaseURL: baseURL, Token: token, Client: &http.Client{Timeout: 40 * time.Second}}
|
|
}
|
|
|
|
// AgentError — агент отклонил запрос (например, недопустимые параметры NFS).
|
|
type AgentError struct {
|
|
Status int
|
|
Field string
|
|
Reason string
|
|
}
|
|
|
|
func (e *AgentError) Error() string {
|
|
return fmt.Sprintf("storage-agent: %d %s %s", e.Status, e.Field, e.Reason)
|
|
}
|
|
|
|
func (a *HTTPAgent) do(ctx context.Context, method, path string, body any, out any) error {
|
|
var rd *bytes.Reader
|
|
if body != nil {
|
|
raw, err := json.Marshal(body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rd = bytes.NewReader(raw)
|
|
} else {
|
|
rd = bytes.NewReader(nil)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, a.BaseURL+path, rd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+a.Token)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res, err := a.Client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("storage-agent: %w", err)
|
|
}
|
|
defer func() { _ = res.Body.Close() }()
|
|
if res.StatusCode >= 400 {
|
|
var p struct {
|
|
Detail string `json:"detail"`
|
|
Errors []struct {
|
|
Field string `json:"field"`
|
|
} `json:"errors"`
|
|
}
|
|
_ = json.NewDecoder(res.Body).Decode(&p)
|
|
ae := &AgentError{Status: res.StatusCode, Reason: p.Detail}
|
|
if len(p.Errors) > 0 {
|
|
ae.Field = p.Errors[0].Field
|
|
}
|
|
return ae
|
|
}
|
|
if out != nil {
|
|
return json.NewDecoder(res.Body).Decode(out)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Mount реализует Agent.
|
|
func (a *HTTPAgent) Mount(ctx context.Context, id uuid.UUID, spec storageagent.NFSSpec) (storageagent.MountResult, error) {
|
|
var res storageagent.MountResult
|
|
err := a.do(ctx, http.MethodPost, "/internal/mounts", storageagent.MountRequest{SourceID: id.String(), NFSSpec: spec}, &res)
|
|
return res, err
|
|
}
|
|
|
|
// Unmount реализует Agent.
|
|
func (a *HTTPAgent) Unmount(ctx context.Context, id uuid.UUID) error {
|
|
return a.do(ctx, http.MethodDelete, "/internal/mounts/"+id.String(), nil, nil)
|
|
}
|
|
|
|
// Test реализует Agent.
|
|
func (a *HTTPAgent) Test(ctx context.Context, spec storageagent.NFSSpec) (storageagent.MountResult, error) {
|
|
var res storageagent.MountResult
|
|
err := a.do(ctx, http.MethodPost, "/internal/mounts/test", spec, &res)
|
|
return res, err
|
|
}
|