5 Commits

Author SHA1 Message Date
ff22423496 Refactoring 2025-01-07 04:15:50 +03:00
56e826e580 Refactoring, fix bug 2025-01-07 03:53:28 +03:00
e1670934a1 Refactoring 2025-01-07 03:45:27 +03:00
62daa023f5 Permanent fallback to copy on hardlink error 2025-01-07 03:42:12 +03:00
df4f2ebd99 Refactoring 2025-01-06 23:47:45 +03:00
7 changed files with 86 additions and 45 deletions

View File

@@ -30,7 +30,8 @@ sudo cp ${GOPATH}/bin/photocatalog /usr/local/bin/photocatalog
The tool supports the following organization modes:
- **copy** — Copies files to the target directory. If the filesystem supports it, uses Copy-on-Write (COW) for efficiency (via FICLONE ioctl call).
- **hardlink** — Creates hardlinks to the source files, saving disk space. Ideal if the source and target are on the same partition, though file permissions remain linked to the original.
- **hardlink** — Creates hardlinks to the source files, saving disk space. Ideal (and usable only) if the source and target are on the same partition,
though file permissions remain linked to the original. Fallback to copy on fail.
- **move** — Moves files from the source to the target directory.
- **symlink** — Creates symbolic links at the target pointing to the source files.

View File

@@ -43,24 +43,24 @@ func (a *Application) Start(ctx context.Context, wg *sync.WaitGroup) error {
WithDirMode(os.FileMode(a.config.DirMode)).
WithFileMode(os.FileMode(a.config.FileMode)).
WithErrLogger(func(err error) {
log.Println(err)
log.Println("ERROR:", err.Error())
})
if a.config.Overwrite {
org = org.WithOverwrite()
}
if !a.config.SkipFullSync {
if err := org.FullSync(ctx); err != nil {
return fmt.Errorf("full sync: %w", err)
}
}
if a.config.Watch {
if err := org.Watch(ctx, wg); err != nil {
return fmt.Errorf("initialize watch: %w", err)
}
}
if !a.config.SkipFullSync {
if err := org.FullSync(ctx); err != nil {
return fmt.Errorf("full sync: %w", err)
}
}
return nil
}

View File

@@ -14,6 +14,8 @@ const (
ModeMove Mode = "move"
)
var SupportedModes = []Mode{ModeHardlink, ModeSymlink, ModeMove, ModeCopy}
type Config struct {
SourceDir string
TargetDir string
@@ -34,8 +36,8 @@ func (c *Config) Validate() error {
return fmt.Errorf("target dir is required")
}
if !slices.Contains([]Mode{ModeHardlink, ModeSymlink, ModeMove, ModeCopy}, c.Mode) {
return fmt.Errorf("invalid mode %s", c.Mode)
if !slices.Contains(SupportedModes, c.Mode) {
return fmt.Errorf("invalid mode %s, supported modes: %s", c.Mode, SupportedModes)
}
if c.SkipFullSync && !c.Watch {

View File

@@ -7,5 +7,5 @@ WantedBy=default.target
[Service]
Type=simple
EnvironmentFile=/home/%u/.config/photocatalog
ExecStart=photocatalog -mode $MODE -target $TARGET -monitor $MONITOR -update_mtime $UPDATECTIME
ExecStartPre=photocatalog -mode $MODE -target $TARGET ${MONITOR}
ExecStart=photocatalog -mode $MODE -target $TARGET -watch -source $MONITOR -skip-full-sync
ExecStartPre=photocatalog -mode $MODE -target $TARGET -source ${MONITOR}

View File

@@ -1,21 +1,38 @@
package modes
import (
"fmt"
"log"
"os"
"sync/atomic"
)
var hardLinkNotSupported = atomic.Bool{}
type HardLink struct {
}
func (h HardLink) PlaceIt(sourcePath, targetPath string, mode os.FileMode) error {
if hardLinkNotSupported.Load() {
return h.fallBack(sourcePath, targetPath, mode)
}
if err := os.Link(sourcePath, targetPath); err != nil {
if os.IsExist(err) {
return nil
}
return fmt.Errorf("create hard link: %w", err)
log.Println("Create hardlink failed:", err.Error())
hardLinkNotSupported.Store(true)
return h.fallBack(sourcePath, targetPath, mode)
}
return nil
}
func (h HardLink) fallBack(sourcePath string, targetPath string, mode os.FileMode) error {
if copyErr := (Copy{}).PlaceIt(sourcePath, targetPath, mode); copyErr != nil {
return copyErr
}
return nil
}

View File

@@ -11,6 +11,7 @@ import (
"strconv"
"strings"
"sync"
"syscall"
"github.com/fsnotify/fsnotify"
@@ -130,6 +131,8 @@ func (o *Organizer) Watch(ctx context.Context, wg *sync.WaitGroup) error {
if err := watcher.Close(); err != nil {
o.logErr(fmt.Errorf("close watcher: %w", err))
}
syscall.Sync()
}()
go func() {
@@ -149,15 +152,17 @@ func (o *Organizer) Watch(ctx context.Context, wg *sync.WaitGroup) error {
// Add new directories to the watcher.
if stat.IsDir() {
if err := watcher.Add(event.Name); err != nil {
o.logErr(fmt.Errorf("watch dir: %w", err))
o.logErr(fmt.Errorf("add the directory %s to watcher: %w", event.Name, err))
}
continue
}
go func() {
if err := o.processFile(event.Name); err != nil {
o.logErr(fmt.Errorf("process file %s: %w", event.Name, err))
}
}()
}
case <-ctx.Done():
@@ -248,7 +253,7 @@ func (o *Organizer) processFile(sourcePath string) error {
return fmt.Errorf("build target path: %w", err)
}
if pathExists(targetPath) && !o.overwrite {
if o.pathExists(targetPath) && !o.overwrite {
return nil
}
@@ -283,7 +288,7 @@ func (o *Organizer) BuildTargetPath(sourcePath string, meta metadata.Metadata) (
}
func (o *Organizer) ensureTargetPath(targetPath string) error {
if pathExists(targetPath) {
if o.pathExists(targetPath) {
return nil
}
@@ -305,13 +310,15 @@ func (o *Organizer) ensureTargetPath(targetPath string) error {
return nil
}
func pathExists(path string) bool {
func (o *Organizer) pathExists(path string) bool {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false
}
o.logErr(fmt.Errorf("pathExists stat %s: %w", path, err))
return true
}

62
main.go
View File

@@ -3,8 +3,10 @@ package main
import (
"context"
"flag"
"fmt"
"log"
"os/signal"
"slices"
"strconv"
"sync"
"syscall"
@@ -38,37 +40,49 @@ func loadCfg() application.Config {
flag.StringVar(&cfg.SourceDir, "source", "", "Source directory")
flag.StringVar(&cfg.TargetDir, "target", "", "Target directory")
flag.BoolVar(&cfg.Overwrite, "overwrite", false, "Overwrite existing files")
flag.BoolVar(&cfg.Watch, "watch", true, "Watch for changes in the source directory")
flag.BoolVar(&cfg.Watch, "watch", false, "Watch for changes in the source directory")
flag.BoolVar(&cfg.Watch, "monitor", false, "Watch for changes in the source directory") // Legacy option
flag.BoolVar(&cfg.SkipFullSync, "skip-full-sync", false, "Skip full sync at startup")
var (
dirMode string
fileMode string
mode string
)
flag.Func("dir-mode", "Mode bits for directories can be created while syncing", func(s string) error {
var err error
flag.StringVar(&dirMode, "dir-mode", "0777", "Mode bits for directories can be created while syncing")
flag.StringVar(&fileMode, "file-mode", "0644", "Mode bits for files created while syncing (not applicable for hardlink mode)")
flag.StringVar(&mode, "mode", "hardlink", "Organizing mode")
cfg.DirMode, err = strconv.ParseUint(s, 8, 32)
if err != nil {
return err
}
return nil
})
flag.Func("file-mode", "Mode bits for files created while syncing (not applicable for hardlink mode)", func(s string) error {
var err error
cfg.FileMode, err = strconv.ParseUint(s, 8, 32)
if err != nil {
return err
}
return nil
})
flag.Func("mode", "Organizing mode", func(s string) error {
cfg.Mode = application.Mode(s)
if !slices.Contains(application.SupportedModes, cfg.Mode) {
return fmt.Errorf("invalid mode, supported modes: %s", application.SupportedModes)
}
return nil
})
flag.Parse()
cfg.Mode = application.Mode(mode)
// Legacy fallback
if cfg.SourceDir == "" {
log.Println("Source directory not specified. May be using old systemd unit file.")
var err error
cfg.DirMode, err = strconv.ParseUint(dirMode, 8, 32)
if err != nil {
log.Println("Parse -dir-mode failed:", err)
cfg.DirMode = 0o777
}
cfg.FileMode, err = strconv.ParseUint(fileMode, 8, 32)
if err != nil {
log.Println("Parse -file-mode failed:", err)
cfg.DirMode = 0o644
cfg.SourceDir = flag.Arg(0)
}
return cfg