update
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
This commit is contained in:
216
persistence/folder_repository.go
Normal file
216
persistence/folder_repository.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
. "github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils/slice"
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
type folderRepository struct {
|
||||
sqlRepository
|
||||
}
|
||||
|
||||
type dbFolder struct {
|
||||
*model.Folder `structs:",flatten"`
|
||||
ImageFiles string `structs:"-" json:"-"`
|
||||
}
|
||||
|
||||
func (f *dbFolder) PostScan() error {
|
||||
var err error
|
||||
if f.ImageFiles != "" {
|
||||
if err = json.Unmarshal([]byte(f.ImageFiles), &f.Folder.ImageFiles); err != nil {
|
||||
return fmt.Errorf("parsing folder image files from db: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *dbFolder) PostMapArgs(args map[string]any) error {
|
||||
if f.Folder.ImageFiles == nil {
|
||||
args["image_files"] = "[]"
|
||||
} else {
|
||||
imgFiles, err := json.Marshal(f.Folder.ImageFiles)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshalling image files: %w", err)
|
||||
}
|
||||
args["image_files"] = string(imgFiles)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbFolders []dbFolder
|
||||
|
||||
func (fs dbFolders) toModels() []model.Folder {
|
||||
return slice.Map(fs, func(f dbFolder) model.Folder { return *f.Folder })
|
||||
}
|
||||
|
||||
func newFolderRepository(ctx context.Context, db dbx.Builder) model.FolderRepository {
|
||||
r := &folderRepository{}
|
||||
r.ctx = ctx
|
||||
r.db = db
|
||||
r.tableName = "folder"
|
||||
return r
|
||||
}
|
||||
|
||||
func (r folderRepository) selectFolder(options ...model.QueryOptions) SelectBuilder {
|
||||
sql := r.newSelect(options...).Columns("folder.*", "library.path as library_path").
|
||||
Join("library on library.id = folder.library_id")
|
||||
return r.applyLibraryFilter(sql)
|
||||
}
|
||||
|
||||
func (r folderRepository) Get(id string) (*model.Folder, error) {
|
||||
sq := r.selectFolder().Where(Eq{"folder.id": id})
|
||||
var res dbFolder
|
||||
err := r.queryOne(sq, &res)
|
||||
return res.Folder, err
|
||||
}
|
||||
|
||||
func (r folderRepository) GetByPath(lib model.Library, path string) (*model.Folder, error) {
|
||||
id := model.NewFolder(lib, path).ID
|
||||
return r.Get(id)
|
||||
}
|
||||
|
||||
func (r folderRepository) GetAll(opt ...model.QueryOptions) ([]model.Folder, error) {
|
||||
sq := r.selectFolder(opt...)
|
||||
var res dbFolders
|
||||
err := r.queryAll(sq, &res)
|
||||
return res.toModels(), err
|
||||
}
|
||||
|
||||
func (r folderRepository) CountAll(opt ...model.QueryOptions) (int64, error) {
|
||||
query := r.newSelect(opt...).Columns("count(*)")
|
||||
query = r.applyLibraryFilter(query)
|
||||
return r.count(query)
|
||||
}
|
||||
|
||||
func (r folderRepository) GetFolderUpdateInfo(lib model.Library, targetPaths ...string) (map[string]model.FolderUpdateInfo, error) {
|
||||
where := And{
|
||||
Eq{"library_id": lib.ID},
|
||||
Eq{"missing": false},
|
||||
}
|
||||
|
||||
// If specific paths are requested, include those folders and all their descendants
|
||||
if len(targetPaths) > 0 {
|
||||
// Collect folder IDs for exact target folders and path conditions for descendants
|
||||
folderIDs := make([]string, 0, len(targetPaths))
|
||||
pathConditions := make(Or, 0, len(targetPaths)*2)
|
||||
|
||||
for _, targetPath := range targetPaths {
|
||||
if targetPath == "" || targetPath == "." {
|
||||
// Root path - include everything in this library
|
||||
pathConditions = Or{}
|
||||
folderIDs = nil
|
||||
break
|
||||
}
|
||||
// Clean the path to normalize it. Paths stored in the folder table do not have leading/trailing slashes.
|
||||
cleanPath := strings.TrimPrefix(targetPath, string(os.PathSeparator))
|
||||
cleanPath = filepath.Clean(cleanPath)
|
||||
|
||||
// Include the target folder itself by ID
|
||||
folderIDs = append(folderIDs, model.FolderID(lib, cleanPath))
|
||||
|
||||
// Include all descendants: folders whose path field equals or starts with the target path
|
||||
// Note: Folder.Path is the directory path, so children have path = targetPath
|
||||
pathConditions = append(pathConditions, Eq{"path": cleanPath})
|
||||
pathConditions = append(pathConditions, Like{"path": cleanPath + "/%"})
|
||||
}
|
||||
|
||||
// Combine conditions: exact folder IDs OR descendant path patterns
|
||||
if len(folderIDs) > 0 {
|
||||
where = append(where, Or{Eq{"id": folderIDs}, pathConditions})
|
||||
} else if len(pathConditions) > 0 {
|
||||
where = append(where, pathConditions)
|
||||
}
|
||||
}
|
||||
|
||||
sq := r.newSelect().Columns("id", "updated_at", "hash").Where(where)
|
||||
var res []struct {
|
||||
ID string
|
||||
UpdatedAt time.Time
|
||||
Hash string
|
||||
}
|
||||
err := r.queryAll(sq, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]model.FolderUpdateInfo, len(res))
|
||||
for _, f := range res {
|
||||
m[f.ID] = model.FolderUpdateInfo{UpdatedAt: f.UpdatedAt, Hash: f.Hash}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (r folderRepository) Put(f *model.Folder) error {
|
||||
dbf := dbFolder{Folder: f}
|
||||
_, err := r.put(dbf.ID, &dbf)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r folderRepository) MarkMissing(missing bool, ids ...string) error {
|
||||
log.Debug(r.ctx, "Marking folders as missing", "ids", ids, "missing", missing)
|
||||
for chunk := range slices.Chunk(ids, 200) {
|
||||
sq := Update(r.tableName).
|
||||
Set("missing", missing).
|
||||
Set("updated_at", time.Now()).
|
||||
Where(Eq{"id": chunk})
|
||||
_, err := r.executeSQL(sq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r folderRepository) GetTouchedWithPlaylists() (model.FolderCursor, error) {
|
||||
query := r.selectFolder().Where(And{
|
||||
Eq{"missing": false},
|
||||
Gt{"num_playlists": 0},
|
||||
ConcatExpr("folder.updated_at > library.last_scan_at"),
|
||||
})
|
||||
cursor, err := queryWithStableResults[dbFolder](r.sqlRepository, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(yield func(model.Folder, error) bool) {
|
||||
for f, err := range cursor {
|
||||
if !yield(*f.Folder, err) || err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r folderRepository) purgeEmpty(libraryIDs ...int) error {
|
||||
sq := Delete(r.tableName).Where(And{
|
||||
Eq{"num_audio_files": 0},
|
||||
Eq{"num_playlists": 0},
|
||||
Eq{"image_files": "[]"},
|
||||
ConcatExpr("id not in (select parent_id from folder)"),
|
||||
ConcatExpr("id not in (select folder_id from media_file)"),
|
||||
})
|
||||
// If libraryIDs are specified, only purge folders from those libraries
|
||||
if len(libraryIDs) > 0 {
|
||||
sq = sq.Where(Eq{"library_id": libraryIDs})
|
||||
}
|
||||
c, err := r.executeSQL(sq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("purging empty folders: %w", err)
|
||||
}
|
||||
if c > 0 {
|
||||
log.Debug(r.ctx, "Purging empty folders", "totalDeleted", c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ model.FolderRepository = (*folderRepository)(nil)
|
||||
Reference in New Issue
Block a user