lens @ 05a8dbf46792adfef007a0ffbcb654026db036fa

feat: Add use based file scanner
diff --git a/cmd/server/main.go b/cmd/server/main.go
index c064d569c75c289e6dd9b3a436666431eb4b1a01..b81b2916115977c566ef4ae741e144368e9e933b 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -24,6 +24,7 @@ 	"git.sr.ht/~gabrielgio/img/pkg/ext"
 	"git.sr.ht/~gabrielgio/img/pkg/service"
 	"git.sr.ht/~gabrielgio/img/pkg/view"
 	"git.sr.ht/~gabrielgio/img/pkg/worker"
+	"git.sr.ht/~gabrielgio/img/pkg/worker/scanner"
 )
 
 func main() {
@@ -34,9 +35,6 @@ 		dbCon          = flag.String("db-con", "main.db", "Database string connection for given database type. Ref: https://gorm.io/docs/connecting_to_the_database.html")
 		logLevel       = flag.String("log-level", "error", "Log level: Choose either trace, debug, info, warning, error, fatal or panic")
 		schedulerCount = flag.Uint("scheduler-count", 10, "How many workers are created to process media files")
 		cachePath      = flag.String("cache-path", "", "Folder to store thumbnail image")
-
-		// TODO: this will later be replaced by user specific root folder
-		root = flag.String("root", "", "root folder for the whole application. All the workers will use it as working directory")
 	)
 
 	flag.Parse()
@@ -76,7 +74,7 @@ 	// repository
 	var (
 		userRepository       = sql.NewUserRepository(db)
 		settingsRepository   = sql.NewSettingsRespository(db)
-		fileSystemRepository = localfs.NewFileSystemRepository(*root)
+		fileSystemRepository = localfs.NewFileSystemRepository()
 		mediaRepository      = sql.NewMediaRepository(db)
 	)
 
@@ -113,9 +111,9 @@ 	}
 
 	// processors
 	var (
-		fileScanner      = worker.NewFileScanner(*root, mediaRepository)
-		exifScanner      = worker.NewEXIFScanner(mediaRepository)
-		thumbnailScanner = worker.NewThumbnailScanner(*cachePath, mediaRepository)
+		fileScanner      = scanner.NewFileScanner(mediaRepository, userRepository)
+		exifScanner      = scanner.NewEXIFScanner(mediaRepository)
+		thumbnailScanner = scanner.NewThumbnailScanner(*cachePath, mediaRepository)
 	)
 
 	// worker
diff --git a/pkg/database/localfs/filesystem.go b/pkg/database/localfs/filesystem.go
index c7c645817235c7c6b2f3fa2e5408bcc9e8caa63d..d516ce9934ae8e951b2e6c3a4c80b6deb646bb43 100644
--- a/pkg/database/localfs/filesystem.go
+++ b/pkg/database/localfs/filesystem.go
@@ -11,10 +11,8 @@ type FileSystemRepository struct {
 	root string
 }
 
-func NewFileSystemRepository(root string) *FileSystemRepository {
-	return &FileSystemRepository{
-		root: root,
-	}
+func NewFileSystemRepository() *FileSystemRepository {
+	return &FileSystemRepository{}
 }
 
 func (self *FileSystemRepository) getFilesFromPath(filepath string) ([]fs.FileInfo, error) {
diff --git a/pkg/worker/exif_scanner.go b/pkg/worker/scanner/exif_scanner.go
rename from pkg/worker/exif_scanner.go
rename to pkg/worker/scanner/exif_scanner.go
index 5ea18104d9be7dec5448e46321bc603d8d142e94..47d717fc3d48eaf99945a504d86a76a9ef21955b 100644
--- a/pkg/worker/exif_scanner.go
+++ b/pkg/worker/scanner/exif_scanner.go
@@ -1,4 +1,4 @@
-package worker
+package scanner
 
 import (
 	"context"
@@ -6,6 +6,7 @@
 	"git.sr.ht/~gabrielgio/img/pkg/coroutine"
 	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
 	"git.sr.ht/~gabrielgio/img/pkg/fileop"
+	"git.sr.ht/~gabrielgio/img/pkg/worker"
 )
 
 type (
@@ -14,7 +15,7 @@ 		repository repository.MediaRepository
 	}
 )
 
-var _ BatchProcessor[*repository.Media] = &EXIFScanner{}
+var _ worker.BatchProcessor[*repository.Media] = &EXIFScanner{}
 
 func NewEXIFScanner(repository repository.MediaRepository) *EXIFScanner {
 	return &EXIFScanner{
diff --git a/pkg/worker/file_scanner.go b/pkg/worker/file_scanner.go
deleted file mode 100644
index b4f907ab682473e1082d0694b690c5a25a75c3a2..0000000000000000000000000000000000000000
--- a/pkg/worker/file_scanner.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package worker
-
-import (
-	"context"
-	"io/fs"
-	"mime"
-	"path/filepath"
-
-	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
-	"git.sr.ht/~gabrielgio/img/pkg/fileop"
-)
-
-type (
-	FileScanner struct {
-		root       string
-		repository repository.MediaRepository
-	}
-)
-
-var _ ChanProcessor[string] = &FileScanner{}
-
-func NewFileScanner(root string, repository repository.MediaRepository) *FileScanner {
-	return &FileScanner{
-		root:       root,
-		repository: repository,
-	}
-}
-
-func (f *FileScanner) Query(ctx context.Context) (<-chan string, error) {
-	c := make(chan string)
-	go func() {
-		defer close(c)
-		_ = filepath.Walk(f.root, func(path string, info fs.FileInfo, err error) error {
-			select {
-			case <-ctx.Done():
-				return filepath.SkipAll
-			default:
-			}
-
-			if info == nil {
-				return nil
-			}
-
-			if info.IsDir() && filepath.Base(info.Name())[0] == '.' {
-				return filepath.SkipDir
-			}
-
-			if info.IsDir() {
-				return nil
-			}
-
-			c <- path
-			return nil
-		})
-	}()
-	return c, nil
-}
-
-func (f *FileScanner) Process(ctx context.Context, path string) error {
-	mimetype := mime.TypeByExtension(filepath.Ext(path))
-	supported := fileop.IsMimeTypeSupported(mimetype)
-	if !supported {
-		return nil
-	}
-
-	hash := fileop.GetHashFromPath(path)
-
-	exists, err := f.repository.Exists(ctx, hash)
-	if err != nil {
-		return err
-	}
-
-	if exists {
-		return nil
-	}
-
-	return f.repository.Create(ctx, &repository.CreateMedia{
-		Name:     filepath.Base(path),
-		Path:     path,
-		PathHash: hash,
-		MIMEType: mimetype,
-	})
-}
diff --git a/pkg/worker/scanner/file_scanner.go b/pkg/worker/scanner/file_scanner.go
new file mode 100644
index 0000000000000000000000000000000000000000..7c19a3dfe9573278aa703c3402d1620f62b53c46
--- /dev/null
+++ b/pkg/worker/scanner/file_scanner.go
@@ -0,0 +1,99 @@
+package scanner
+
+import (
+	"context"
+	"io/fs"
+	"mime"
+	"path/filepath"
+
+	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
+	"git.sr.ht/~gabrielgio/img/pkg/fileop"
+	"git.sr.ht/~gabrielgio/img/pkg/list"
+	"git.sr.ht/~gabrielgio/img/pkg/worker"
+)
+
+type (
+	FileScanner struct {
+		mediaRepository repository.MediaRepository
+		userRepository  repository.UserRepository
+	}
+)
+
+var _ worker.ChanProcessor[string] = &FileScanner{}
+
+func NewFileScanner(
+	mediaRepository repository.MediaRepository,
+	userRepository repository.UserRepository,
+) *FileScanner {
+	return &FileScanner{
+		mediaRepository: mediaRepository,
+		userRepository:  userRepository,
+	}
+}
+
+func (f *FileScanner) Query(ctx context.Context) (<-chan string, error) {
+	c := make(chan string)
+
+	users, err := f.userRepository.List(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	// TODO: de duplicate file paths
+	paths := list.Map(users, func(u *repository.User) string { return u.Path })
+
+	go func(paths []string) {
+		defer close(c)
+		for _, p := range paths {
+			_ = filepath.Walk(p, func(path string, info fs.FileInfo, err error) error {
+				select {
+				case <-ctx.Done():
+					return filepath.SkipAll
+				default:
+				}
+
+				if info == nil {
+					return nil
+				}
+
+				if info.IsDir() && filepath.Base(info.Name())[0] == '.' {
+					return filepath.SkipDir
+				}
+
+				if info.IsDir() {
+					return nil
+				}
+
+				c <- path
+				return nil
+			})
+		}
+	}(paths)
+	return c, nil
+}
+
+func (f *FileScanner) Process(ctx context.Context, path string) error {
+	mimetype := mime.TypeByExtension(filepath.Ext(path))
+	supported := fileop.IsMimeTypeSupported(mimetype)
+	if !supported {
+		return nil
+	}
+
+	hash := fileop.GetHashFromPath(path)
+
+	exists, err := f.mediaRepository.Exists(ctx, hash)
+	if err != nil {
+		return err
+	}
+
+	if exists {
+		return nil
+	}
+
+	return f.mediaRepository.Create(ctx, &repository.CreateMedia{
+		Name:     filepath.Base(path),
+		Path:     path,
+		PathHash: hash,
+		MIMEType: mimetype,
+	})
+}
diff --git a/pkg/worker/thumbnail_scanner.go b/pkg/worker/scanner/thumbnail_scanner.go
rename from pkg/worker/thumbnail_scanner.go
rename to pkg/worker/scanner/thumbnail_scanner.go
index 168abef60b86033d7a8bad7118e361c6b013277d..02fd4dd51d4c215edb4a03d2f2d6ce05ef371afb 100644
--- a/pkg/worker/thumbnail_scanner.go
+++ b/pkg/worker/scanner/thumbnail_scanner.go
@@ -1,4 +1,4 @@
-package worker
+package scanner
 
 import (
 	"context"
@@ -9,6 +9,7 @@ 	"path"
 
 	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
 	"git.sr.ht/~gabrielgio/img/pkg/fileop"
+	"git.sr.ht/~gabrielgio/img/pkg/worker"
 )
 
 type (
@@ -18,7 +19,7 @@ 		cachePath  string
 	}
 )
 
-var _ BatchProcessor[*repository.Media] = &EXIFScanner{}
+var _ worker.BatchProcessor[*repository.Media] = &EXIFScanner{}
 
 func NewThumbnailScanner(cachePath string, repository repository.MediaRepository) *ThumbnailScanner {
 	return &ThumbnailScanner{