mirror of
https://github.com/derfenix/photocatalog.git
synced 2026-03-11 21:35:34 +03:00
Compare commits
5 Commits
bcaeba5ea6
...
ff22423496
| Author | SHA1 | Date | |
|---|---|---|---|
| ff22423496 | |||
| 56e826e580 | |||
| e1670934a1 | |||
| 62daa023f5 | |||
| df4f2ebd99 |
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
62
main.go
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user