lens @ 05a8dbf46792adfef007a0ffbcb654026db036fa

feat: Add use based file scanner
  1diff --git a/cmd/server/main.go b/cmd/server/main.go
  2index c064d569c75c289e6dd9b3a436666431eb4b1a01..b81b2916115977c566ef4ae741e144368e9e933b 100644
  3--- a/cmd/server/main.go
  4+++ b/cmd/server/main.go
  5@@ -24,6 +24,7 @@ 	"git.sr.ht/~gabrielgio/img/pkg/ext"
  6 	"git.sr.ht/~gabrielgio/img/pkg/service"
  7 	"git.sr.ht/~gabrielgio/img/pkg/view"
  8 	"git.sr.ht/~gabrielgio/img/pkg/worker"
  9+	"git.sr.ht/~gabrielgio/img/pkg/worker/scanner"
 10 )
 11 
 12 func main() {
 13@@ -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")
 14 		logLevel       = flag.String("log-level", "error", "Log level: Choose either trace, debug, info, warning, error, fatal or panic")
 15 		schedulerCount = flag.Uint("scheduler-count", 10, "How many workers are created to process media files")
 16 		cachePath      = flag.String("cache-path", "", "Folder to store thumbnail image")
 17-
 18-		// TODO: this will later be replaced by user specific root folder
 19-		root = flag.String("root", "", "root folder for the whole application. All the workers will use it as working directory")
 20 	)
 21 
 22 	flag.Parse()
 23@@ -76,7 +74,7 @@ 	// repository
 24 	var (
 25 		userRepository       = sql.NewUserRepository(db)
 26 		settingsRepository   = sql.NewSettingsRespository(db)
 27-		fileSystemRepository = localfs.NewFileSystemRepository(*root)
 28+		fileSystemRepository = localfs.NewFileSystemRepository()
 29 		mediaRepository      = sql.NewMediaRepository(db)
 30 	)
 31 
 32@@ -113,9 +111,9 @@ 	}
 33 
 34 	// processors
 35 	var (
 36-		fileScanner      = worker.NewFileScanner(*root, mediaRepository)
 37-		exifScanner      = worker.NewEXIFScanner(mediaRepository)
 38-		thumbnailScanner = worker.NewThumbnailScanner(*cachePath, mediaRepository)
 39+		fileScanner      = scanner.NewFileScanner(mediaRepository, userRepository)
 40+		exifScanner      = scanner.NewEXIFScanner(mediaRepository)
 41+		thumbnailScanner = scanner.NewThumbnailScanner(*cachePath, mediaRepository)
 42 	)
 43 
 44 	// worker
 45diff --git a/pkg/database/localfs/filesystem.go b/pkg/database/localfs/filesystem.go
 46index c7c645817235c7c6b2f3fa2e5408bcc9e8caa63d..d516ce9934ae8e951b2e6c3a4c80b6deb646bb43 100644
 47--- a/pkg/database/localfs/filesystem.go
 48+++ b/pkg/database/localfs/filesystem.go
 49@@ -11,10 +11,8 @@ type FileSystemRepository struct {
 50 	root string
 51 }
 52 
 53-func NewFileSystemRepository(root string) *FileSystemRepository {
 54-	return &FileSystemRepository{
 55-		root: root,
 56-	}
 57+func NewFileSystemRepository() *FileSystemRepository {
 58+	return &FileSystemRepository{}
 59 }
 60 
 61 func (self *FileSystemRepository) getFilesFromPath(filepath string) ([]fs.FileInfo, error) {
 62diff --git a/pkg/worker/exif_scanner.go b/pkg/worker/scanner/exif_scanner.go
 63rename from pkg/worker/exif_scanner.go
 64rename to pkg/worker/scanner/exif_scanner.go
 65index 5ea18104d9be7dec5448e46321bc603d8d142e94..47d717fc3d48eaf99945a504d86a76a9ef21955b 100644
 66--- a/pkg/worker/exif_scanner.go
 67+++ b/pkg/worker/scanner/exif_scanner.go
 68@@ -1,4 +1,4 @@
 69-package worker
 70+package scanner
 71 
 72 import (
 73 	"context"
 74@@ -6,6 +6,7 @@
 75 	"git.sr.ht/~gabrielgio/img/pkg/coroutine"
 76 	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
 77 	"git.sr.ht/~gabrielgio/img/pkg/fileop"
 78+	"git.sr.ht/~gabrielgio/img/pkg/worker"
 79 )
 80 
 81 type (
 82@@ -14,7 +15,7 @@ 		repository repository.MediaRepository
 83 	}
 84 )
 85 
 86-var _ BatchProcessor[*repository.Media] = &EXIFScanner{}
 87+var _ worker.BatchProcessor[*repository.Media] = &EXIFScanner{}
 88 
 89 func NewEXIFScanner(repository repository.MediaRepository) *EXIFScanner {
 90 	return &EXIFScanner{
 91diff --git a/pkg/worker/file_scanner.go b/pkg/worker/file_scanner.go
 92deleted file mode 100644
 93index b4f907ab682473e1082d0694b690c5a25a75c3a2..0000000000000000000000000000000000000000
 94--- a/pkg/worker/file_scanner.go
 95+++ /dev/null
 96@@ -1,83 +0,0 @@
 97-package worker
 98-
 99-import (
100-	"context"
101-	"io/fs"
102-	"mime"
103-	"path/filepath"
104-
105-	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
106-	"git.sr.ht/~gabrielgio/img/pkg/fileop"
107-)
108-
109-type (
110-	FileScanner struct {
111-		root       string
112-		repository repository.MediaRepository
113-	}
114-)
115-
116-var _ ChanProcessor[string] = &FileScanner{}
117-
118-func NewFileScanner(root string, repository repository.MediaRepository) *FileScanner {
119-	return &FileScanner{
120-		root:       root,
121-		repository: repository,
122-	}
123-}
124-
125-func (f *FileScanner) Query(ctx context.Context) (<-chan string, error) {
126-	c := make(chan string)
127-	go func() {
128-		defer close(c)
129-		_ = filepath.Walk(f.root, func(path string, info fs.FileInfo, err error) error {
130-			select {
131-			case <-ctx.Done():
132-				return filepath.SkipAll
133-			default:
134-			}
135-
136-			if info == nil {
137-				return nil
138-			}
139-
140-			if info.IsDir() && filepath.Base(info.Name())[0] == '.' {
141-				return filepath.SkipDir
142-			}
143-
144-			if info.IsDir() {
145-				return nil
146-			}
147-
148-			c <- path
149-			return nil
150-		})
151-	}()
152-	return c, nil
153-}
154-
155-func (f *FileScanner) Process(ctx context.Context, path string) error {
156-	mimetype := mime.TypeByExtension(filepath.Ext(path))
157-	supported := fileop.IsMimeTypeSupported(mimetype)
158-	if !supported {
159-		return nil
160-	}
161-
162-	hash := fileop.GetHashFromPath(path)
163-
164-	exists, err := f.repository.Exists(ctx, hash)
165-	if err != nil {
166-		return err
167-	}
168-
169-	if exists {
170-		return nil
171-	}
172-
173-	return f.repository.Create(ctx, &repository.CreateMedia{
174-		Name:     filepath.Base(path),
175-		Path:     path,
176-		PathHash: hash,
177-		MIMEType: mimetype,
178-	})
179-}
180diff --git a/pkg/worker/scanner/file_scanner.go b/pkg/worker/scanner/file_scanner.go
181new file mode 100644
182index 0000000000000000000000000000000000000000..7c19a3dfe9573278aa703c3402d1620f62b53c46
183--- /dev/null
184+++ b/pkg/worker/scanner/file_scanner.go
185@@ -0,0 +1,99 @@
186+package scanner
187+
188+import (
189+	"context"
190+	"io/fs"
191+	"mime"
192+	"path/filepath"
193+
194+	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
195+	"git.sr.ht/~gabrielgio/img/pkg/fileop"
196+	"git.sr.ht/~gabrielgio/img/pkg/list"
197+	"git.sr.ht/~gabrielgio/img/pkg/worker"
198+)
199+
200+type (
201+	FileScanner struct {
202+		mediaRepository repository.MediaRepository
203+		userRepository  repository.UserRepository
204+	}
205+)
206+
207+var _ worker.ChanProcessor[string] = &FileScanner{}
208+
209+func NewFileScanner(
210+	mediaRepository repository.MediaRepository,
211+	userRepository repository.UserRepository,
212+) *FileScanner {
213+	return &FileScanner{
214+		mediaRepository: mediaRepository,
215+		userRepository:  userRepository,
216+	}
217+}
218+
219+func (f *FileScanner) Query(ctx context.Context) (<-chan string, error) {
220+	c := make(chan string)
221+
222+	users, err := f.userRepository.List(ctx)
223+	if err != nil {
224+		return nil, err
225+	}
226+
227+	// TODO: de duplicate file paths
228+	paths := list.Map(users, func(u *repository.User) string { return u.Path })
229+
230+	go func(paths []string) {
231+		defer close(c)
232+		for _, p := range paths {
233+			_ = filepath.Walk(p, func(path string, info fs.FileInfo, err error) error {
234+				select {
235+				case <-ctx.Done():
236+					return filepath.SkipAll
237+				default:
238+				}
239+
240+				if info == nil {
241+					return nil
242+				}
243+
244+				if info.IsDir() && filepath.Base(info.Name())[0] == '.' {
245+					return filepath.SkipDir
246+				}
247+
248+				if info.IsDir() {
249+					return nil
250+				}
251+
252+				c <- path
253+				return nil
254+			})
255+		}
256+	}(paths)
257+	return c, nil
258+}
259+
260+func (f *FileScanner) Process(ctx context.Context, path string) error {
261+	mimetype := mime.TypeByExtension(filepath.Ext(path))
262+	supported := fileop.IsMimeTypeSupported(mimetype)
263+	if !supported {
264+		return nil
265+	}
266+
267+	hash := fileop.GetHashFromPath(path)
268+
269+	exists, err := f.mediaRepository.Exists(ctx, hash)
270+	if err != nil {
271+		return err
272+	}
273+
274+	if exists {
275+		return nil
276+	}
277+
278+	return f.mediaRepository.Create(ctx, &repository.CreateMedia{
279+		Name:     filepath.Base(path),
280+		Path:     path,
281+		PathHash: hash,
282+		MIMEType: mimetype,
283+	})
284+}
285diff --git a/pkg/worker/thumbnail_scanner.go b/pkg/worker/scanner/thumbnail_scanner.go
286rename from pkg/worker/thumbnail_scanner.go
287rename to pkg/worker/scanner/thumbnail_scanner.go
288index 168abef60b86033d7a8bad7118e361c6b013277d..02fd4dd51d4c215edb4a03d2f2d6ce05ef371afb 100644
289--- a/pkg/worker/thumbnail_scanner.go
290+++ b/pkg/worker/scanner/thumbnail_scanner.go
291@@ -1,4 +1,4 @@
292-package worker
293+package scanner
294 
295 import (
296 	"context"
297@@ -9,6 +9,7 @@ 	"path"
298 
299 	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
300 	"git.sr.ht/~gabrielgio/img/pkg/fileop"
301+	"git.sr.ht/~gabrielgio/img/pkg/worker"
302 )
303 
304 type (
305@@ -18,7 +19,7 @@ 		cachePath  string
306 	}
307 )
308 
309-var _ BatchProcessor[*repository.Media] = &EXIFScanner{}
310+var _ worker.BatchProcessor[*repository.Media] = &EXIFScanner{}
311 
312 func NewThumbnailScanner(cachePath string, repository repository.MediaRepository) *ThumbnailScanner {
313 	return &ThumbnailScanner{