Files
pipeline_backend/watcher/internal/scanner/scanner.go
2026-06-10 17:12:58 +03:00

145 lines
3.2 KiB
Go

package scanner
import (
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"time"
"github.com/oklog/ulid/v2"
)
var allowedExts = map[string]bool{
".mp3": true, ".wav": true, ".m4a": true,
".ogg": true, ".flac": true, ".webm": true,
}
type ClaimedFile struct {
TaskID string
FilePath string
Filename string
Size int64
}
type Config struct {
StorageRoot string
IncomingDir string
ProcessingDir string
FailedDir string
StableWindow time.Duration
StableChecks int
}
type Scanner struct {
cfg Config
}
func New(cfg Config) *Scanner {
return &Scanner{cfg: cfg}
}
func (s *Scanner) EnsureDirs() error {
for _, dir := range []string{s.cfg.IncomingDir, s.cfg.ProcessingDir, s.cfg.FailedDir} {
if err := os.MkdirAll(filepath.Join(s.cfg.StorageRoot, dir), 0o755); err != nil {
return err
}
}
return nil
}
func (s *Scanner) ScanOnce() ([]ClaimedFile, error) {
incoming := filepath.Join(s.cfg.StorageRoot, s.cfg.IncomingDir)
entries, err := os.ReadDir(incoming)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
var claimed []ClaimedFile
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if strings.HasPrefix(name, ".") || strings.HasSuffix(strings.ToLower(name), ".tmp") {
continue
}
ext := strings.ToLower(filepath.Ext(name))
if !allowedExts[ext] {
continue
}
src := filepath.Join(incoming, name)
if !s.isStable(src) {
continue
}
cf, err := s.claim(src, name, ext)
if err != nil {
slog.Warn("claim failed", "file", name, "error", err)
continue
}
claimed = append(claimed, cf)
}
return claimed, nil
}
func (s *Scanner) isStable(path string) bool {
var lastSize int64 = -1
for i := 0; i < s.cfg.StableChecks; i++ {
info, err := os.Stat(path)
if err != nil {
return false
}
size := info.Size()
if lastSize >= 0 && size != lastSize {
return false
}
lastSize = size
if i < s.cfg.StableChecks-1 {
time.Sleep(s.cfg.StableWindow)
}
}
return true
}
func (s *Scanner) claim(src, originalName, ext string) (ClaimedFile, error) {
info, err := os.Stat(src)
if err != nil {
return ClaimedFile{}, err
}
taskID := ulid.Make().String()
processing := filepath.Join(s.cfg.StorageRoot, s.cfg.ProcessingDir)
dst := filepath.Join(processing, taskID+ext)
if err := os.Rename(src, dst); err != nil {
return ClaimedFile{}, fmt.Errorf("rename: %w", err)
}
slog.Info("claimed file", "task_id", taskID, "filename", originalName, "path", dst, "size", info.Size())
return ClaimedFile{
TaskID: taskID,
FilePath: dst,
Filename: originalName,
Size: info.Size(),
}, nil
}
func (s *Scanner) RollbackToIncoming(filePath, originalName string) error {
incoming := filepath.Join(s.cfg.StorageRoot, s.cfg.IncomingDir)
dst := filepath.Join(incoming, originalName)
if err := os.Rename(filePath, dst); err != nil {
return s.MoveToFailed(filePath, originalName)
}
return nil
}
func (s *Scanner) MoveToFailed(filePath, originalName string) error {
failed := filepath.Join(s.cfg.StorageRoot, s.cfg.FailedDir)
if err := os.MkdirAll(failed, 0o755); err != nil {
return err
}
dst := filepath.Join(failed, originalName)
return os.Rename(filePath, dst)
}