navidrome/scanner/load_tree.go

121 lines
3.2 KiB
Go

package scanner
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/utils"
)
type (
dirMapValue struct {
modTime time.Time
hasImages bool
hasPlaylist bool
hasAudioFiles bool
}
dirMap = map[string]dirMapValue
)
func loadDirTree(ctx context.Context, rootFolder string) (dirMap, error) {
newMap := make(dirMap)
err := loadMap(ctx, rootFolder, rootFolder, newMap)
if err != nil {
log.Error(ctx, "Error loading directory tree", err)
}
return newMap, err
}
func loadMap(ctx context.Context, rootPath string, currentFolder string, dirMap dirMap) error {
children, dirMapValue, err := loadDir(ctx, currentFolder)
if err != nil {
return err
}
for _, c := range children {
err := loadMap(ctx, rootPath, c, dirMap)
if err != nil {
return err
}
}
dir := filepath.Clean(currentFolder)
dirMap[dir] = dirMapValue
return nil
}
func loadDir(ctx context.Context, dirPath string) (children []string, info dirMapValue, err error) {
dirInfo, err := os.Stat(dirPath)
if err != nil {
log.Error(ctx, "Error stating dir", "path", dirPath, err)
return
}
info.modTime = dirInfo.ModTime()
files, err := ioutil.ReadDir(dirPath)
if err != nil {
log.Error(ctx, "Error reading dir", "path", dirPath, err)
return
}
for _, f := range files {
isDir, err := isDirOrSymlinkToDir(dirPath, f)
// Skip invalid symlinks
if err != nil {
continue
}
if isDir && !isDirIgnored(dirPath, f) && isDirReadable(dirPath, f) {
children = append(children, filepath.Join(dirPath, f.Name()))
} else {
if f.ModTime().After(info.modTime) {
info.modTime = f.ModTime()
}
info.hasImages = info.hasImages || utils.IsImageFile(f.Name())
info.hasPlaylist = info.hasPlaylist || utils.IsPlaylist(f.Name())
info.hasAudioFiles = info.hasAudioFiles || utils.IsAudioFile(f.Name())
}
}
return
}
// isDirOrSymlinkToDir returns true if and only if the dirInfo represents a file
// system directory, or a symbolic link to a directory. Note that if the dirInfo
// is not a directory but is a symbolic link, this method will resolve by
// sending a request to the operating system to follow the symbolic link.
// Copied from github.com/karrick/godirwalk
func isDirOrSymlinkToDir(baseDir string, dirInfo os.FileInfo) (bool, error) {
if dirInfo.IsDir() {
return true, nil
}
if dirInfo.Mode()&os.ModeSymlink == 0 {
return false, nil
}
// Does this symlink point to a directory?
dirInfo, err := os.Stat(filepath.Join(baseDir, dirInfo.Name()))
if err != nil {
return false, err
}
return dirInfo.IsDir(), nil
}
// isDirIgnored returns true if the directory represented by dirInfo contains an
// `ignore` file (named after consts.SkipScanFile)
func isDirIgnored(baseDir string, dirInfo os.FileInfo) bool {
_, err := os.Stat(filepath.Join(baseDir, dirInfo.Name(), consts.SkipScanFile))
return err == nil
}
// isDirReadable returns true if the directory represented by dirInfo is readable
func isDirReadable(baseDir string, dirInfo os.FileInfo) bool {
path := filepath.Join(baseDir, dirInfo.Name())
res, err := utils.IsDirReadable(path)
if !res {
log.Debug("Warning: Skipping unreadable directory", "path", path, err)
}
return res
}