🏡 index : quine-global/quintodrome.git

package local

import (
	"fmt"
	"io/fs"
	"net/url"
	"os"
	"path/filepath"
	"sync"
	"sync/atomic"
	"time"

	"github.com/djherbis/times"
	"github.com/navidrome/navidrome/conf"
	"github.com/navidrome/navidrome/consts"
	"github.com/navidrome/navidrome/core/storage"
	"github.com/navidrome/navidrome/log"
	"github.com/navidrome/navidrome/model/metadata"
)

// localStorage implements a Storage that reads the files from the local filesystem and uses registered extractors
// to extract the metadata and tags from the files.
type localStorage struct {
	u            url.URL
	extractor    Extractor
	resolvedPath string
	watching     atomic.Bool
}

func newLocalStorage(u url.URL) storage.Storage {
	newExtractor, ok := extractors[conf.Server.Scanner.Extractor]
	if !ok || newExtractor == nil {
		if conf.Server.Scanner.Extractor != consts.DefaultScannerExtractor {
			log.Warn("Extractor not found, using default", "extractor", conf.Server.Scanner.Extractor, "default", consts.DefaultScannerExtractor)
		}
		newExtractor = extractors[consts.DefaultScannerExtractor]
		if newExtractor == nil {
			log.Fatal("Default extractor not registered", "extractor", consts.DefaultScannerExtractor)
		}
	}
	isWindowsPath := filepath.VolumeName(u.Host) != ""
	if u.Scheme == storage.LocalSchemaID && isWindowsPath {
		u.Path = filepath.Join(u.Host, u.Path)
	}
	resolvedPath, err := filepath.EvalSymlinks(u.Path)
	if err != nil {
		log.Warn("Error resolving path", "path", u.Path, "err", err)
		resolvedPath = u.Path
	}
	return &localStorage{u: u, extractor: newExtractor(os.DirFS(u.Path), u.Path), resolvedPath: resolvedPath}
}

func (s *localStorage) FS() (storage.MusicFS, error) {
	path := s.u.Path
	if _, err := os.Stat(path); err != nil { //nolint:gosec
		return nil, fmt.Errorf("%w: %s", err, path)
	}
	return &localFS{FS: os.DirFS(path), extractor: s.extractor, root: path}, nil
}

type localFS struct {
	fs.FS
	extractor Extractor
	root      string
	// devices whose statx never reports a birth time (NFS, rclone/FUSE), so we ask each only once
	noBirthTime sync.Map
}

// ResolveSymlink implements storage.SymlinkResolverFS. It resolves the whole chain at the
// OS level, so links whose targets live outside the library folder (not reachable through
// the fs.FS abstraction) still resolve to their final target.
func (lfs *localFS) ResolveSymlink(name string) (string, error) {
	if !fs.ValidPath(name) {
		return "", &fs.PathError{Op: "resolvesymlink", Path: name, Err: fs.ErrInvalid}
	}
	return filepath.EvalSymlinks(filepath.Join(lfs.root, filepath.FromSlash(name)))
}

// ReadLink and Lstat implement fs.ReadLinkFS, so callers can detect symlinks without following them.
var _ fs.ReadLinkFS = (*localFS)(nil)

func (lfs *localFS) ReadLink(name string) (string, error) {
	return fs.ReadLink(lfs.FS, name)
}

func (lfs *localFS) Lstat(name string) (fs.FileInfo, error) {
	return fs.Lstat(lfs.FS, name)
}

func (lfs *localFS) ReadTags(path ...string) (map[string]metadata.Info, error) {
	res, err := lfs.extractor.Parse(path...)
	if err != nil {
		return nil, err
	}
	for path, v := range res {
		if v.FileInfo == nil {
			info, err := fs.Stat(lfs, path)
			if err != nil {
				return nil, err
			}
			v.FileInfo = localFileInfo{
				FileInfo:    info,
				path:        filepath.Join(lfs.root, filepath.FromSlash(path)),
				noBirthTime: &lfs.noBirthTime,
			}
			res[path] = v
		}
	}
	return res, nil
}

// localFileInfo is a wrapper around fs.FileInfo that adds a BirthTime method, to make it compatible
// with metadata.FileInfo
type localFileInfo struct {
	fs.FileInfo
	path        string
	noBirthTime *sync.Map
}

func (lfi localFileInfo) BirthTime() time.Time {
	if ts := times.Get(lfi.FileInfo); ts.HasBirthTime() {
		return ts.BirthTime()
	}
	if bt, ok := lfi.statxBirthTime(); ok {
		return bt
	}
	return time.Now()
}

// statxBirthTime reads the birth time from the path, which on Linux is the only way to get it.
// Filesystems that never report one are remembered per device, so a scan asks each only once.
func (lfi localFileInfo) statxBirthTime() (time.Time, bool) {
	if lfi.path == "" {
		return time.Time{}, false
	}
	dev, hasDev := deviceID(lfi.FileInfo)
	memo := lfi.noBirthTime
	if hasDev && memo != nil {
		if _, skip := memo.Load(dev); skip {
			return time.Time{}, false
		}
	}
	ts, err := times.Stat(lfi.path)
	if err != nil {
		return time.Time{}, false
	}
	if ts.HasBirthTime() {
		return ts.BirthTime(), true
	}
	if hasDev && memo != nil {
		memo.Store(dev, struct{}{})
	}
	return time.Time{}, false
}

func init() {
	storage.Register(storage.LocalSchemaID, newLocalStorage)
}