145 lines
3.2 KiB
Go
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)
|
|
}
|