Skip to content

Commit

Permalink
Add new scanner algorithm, can be enabled with DevNewScanner config o…
Browse files Browse the repository at this point in the history
…ption
  • Loading branch information
deluan committed Jul 17, 2020
1 parent de0cc1f commit 51c295d
Show file tree
Hide file tree
Showing 11 changed files with 750 additions and 160 deletions.
2 changes: 2 additions & 0 deletions conf/configuration.go
Expand Up @@ -38,6 +38,7 @@ type configOptions struct {
// DevFlags. These are used to enable/disable debugging and incomplete features
DevLogSourceLine bool
DevAutoCreateAdminPassword string
DevNewScanner bool
}

var Server = &configOptions{}
Expand Down Expand Up @@ -94,6 +95,7 @@ func init() {
// DevFlags. These are used to enable/disable debugging and incomplete features
viper.SetDefault("devlogsourceline", false)
viper.SetDefault("devautocreateadminpassword", "")
viper.SetDefault("devnewscanner", false)
}

func InitConfig(cfgFile string) {
Expand Down
55 changes: 8 additions & 47 deletions scanner/change_detector.go
Expand Up @@ -7,9 +7,7 @@ import (
"path/filepath"
"time"

"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/utils"
)

type dirInfo struct {
Expand All @@ -18,19 +16,19 @@ type dirInfo struct {
}
type dirInfoMap map[string]dirInfo

type ChangeDetector struct {
type changeDetector struct {
rootFolder string
dirMap dirInfoMap
}

func NewChangeDetector(rootFolder string) *ChangeDetector {
return &ChangeDetector{
func newChangeDetector(rootFolder string) *changeDetector {
return &changeDetector{
rootFolder: rootFolder,
dirMap: dirInfoMap{},
}
}

func (s *ChangeDetector) Scan(ctx context.Context, lastModifiedSince time.Time) (changed []string, deleted []string, err error) {
func (s *changeDetector) Scan(ctx context.Context, lastModifiedSince time.Time) (changed []string, deleted []string, err error) {
start := time.Now()
newMap := make(dirInfoMap)
err = s.loadMap(ctx, newMap, s.rootFolder, lastModifiedSince, false)
Expand All @@ -48,7 +46,7 @@ func (s *ChangeDetector) Scan(ctx context.Context, lastModifiedSince time.Time)
return
}

func (s *ChangeDetector) loadDir(ctx context.Context, dirPath string) (children []string, lastUpdated time.Time, err error) {
func (s *changeDetector) loadDir(ctx context.Context, dirPath string) (children []string, lastUpdated time.Time, err error) {
dirInfo, err := os.Stat(dirPath)
if err != nil {
log.Error(ctx, "Error stating dir", "path", dirPath, err)
Expand Down Expand Up @@ -78,44 +76,7 @@ func (s *ChangeDetector) loadDir(ctx context.Context, dirPath string) (children
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
}

func (s *ChangeDetector) loadMap(ctx context.Context, dirMap dirInfoMap, path string, since time.Time, maybe bool) error {
func (s *changeDetector) loadMap(ctx context.Context, dirMap dirInfoMap, path string, since time.Time, maybe bool) error {
children, lastUpdated, err := s.loadDir(ctx, path)
if err != nil {
return err
Expand All @@ -134,15 +95,15 @@ func (s *ChangeDetector) loadMap(ctx context.Context, dirMap dirInfoMap, path st
return nil
}

func (s *ChangeDetector) getRelativePath(subFolder string) string {
func (s *changeDetector) getRelativePath(subFolder string) string {
dir, _ := filepath.Rel(s.rootFolder, subFolder)
if dir == "" {
dir = "."
}
return dir
}

func (s *ChangeDetector) checkForUpdates(lastModifiedSince time.Time, newMap dirInfoMap) (changed []string, deleted []string, err error) {
func (s *changeDetector) checkForUpdates(lastModifiedSince time.Time, newMap dirInfoMap) (changed []string, deleted []string, err error) {
for dir, newEntry := range newMap {
lastUpdated := newEntry.mdate
oldLastUpdated := lastModifiedSince
Expand Down
8 changes: 4 additions & 4 deletions scanner/change_detector_test.go
Expand Up @@ -11,9 +11,9 @@ import (
. "github.com/onsi/gomega"
)

var _ = Describe("ChangeDetector", func() {
var _ = Describe("changeDetector", func() {
var testFolder string
var scanner *ChangeDetector
var scanner *changeDetector

lastModifiedSince := time.Time{}

Expand All @@ -23,7 +23,7 @@ var _ = Describe("ChangeDetector", func() {
if err != nil {
panic(err)
}
scanner = NewChangeDetector(testFolder)
scanner = newChangeDetector(testFolder)
})

It("detects changes recursively", func() {
Expand Down Expand Up @@ -97,7 +97,7 @@ var _ = Describe("ChangeDetector", func() {

// Only returns changes after lastModifiedSince
lastModifiedSince = nowWithDelay()
newScanner := NewChangeDetector(testFolder)
newScanner := newChangeDetector(testFolder)
changed, deleted, err = newScanner.Scan(context.TODO(), lastModifiedSince)
Expect(err).To(BeNil())
Expect(deleted).To(BeEmpty())
Expand Down
58 changes: 58 additions & 0 deletions scanner/flushable_map.go
@@ -0,0 +1,58 @@
package scanner

import (
"context"
"fmt"

"github.com/deluan/navidrome/log"
)

const (
// batchSize used for albums/artists updates
batchSize = 5
)

type refreshCallbackFunc = func(ids ...string) error

type flushableMap struct {
ctx context.Context
flushFunc refreshCallbackFunc
entity string
m map[string]struct{}
}

func newFlushableMap(ctx context.Context, entity string, flushFunc refreshCallbackFunc) *flushableMap {
return &flushableMap{
ctx: ctx,
flushFunc: flushFunc,
entity: entity,
m: map[string]struct{}{},
}
}

func (f *flushableMap) update(id string) error {
f.m[id] = struct{}{}
if len(f.m) >= batchSize {
err := f.flush()
if err != nil {
return err
}
}
return nil
}

func (f *flushableMap) flush() error {
if len(f.m) == 0 {
return nil
}
var ids []string
for id := range f.m {
ids = append(ids, id)
delete(f.m, id)
}
if err := f.flushFunc(ids...); err != nil {
log.Error(f.ctx, fmt.Sprintf("Error writing %ss to the DB", f.entity), err)
return err
}
return nil
}
109 changes: 109 additions & 0 deletions scanner/load_tree.go
@@ -0,0 +1,109 @@
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 dirMap = map[string]time.Time

func loadDirTree(ctx context.Context, rootFolder string) (dirMap, error) {
newMap := make(map[string]time.Time)
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, lastUpdated, 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] = lastUpdated

return nil
}

func loadDir(ctx context.Context, dirPath string) (children []string, lastUpdated time.Time, err error) {
dirInfo, err := os.Stat(dirPath)
if err != nil {
log.Error(ctx, "Error stating dir", "path", dirPath, err)
return
}
lastUpdated = 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(lastUpdated) {
lastUpdated = f.ModTime()
}
}
}
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
}

0 comments on commit 51c295d

Please sign in to comment.