]> code.octet-stream.net Git - photosorter/blob - photosorter.go
Initial git commit
[photosorter] / photosorter.go
1 package main
2
3 import (
4 "bufio"
5 "errors"
6 "flag"
7 "log"
8 "os"
9 "os/signal"
10 "path"
11 "strconv"
12 "strings"
13 "syscall"
14 "time"
15
16 "github.com/fsnotify/fsnotify"
17 )
18
19 var sourcePath string
20 var targetPath string
21
22 func getSeenPath() string {
23 cacheDir, err := os.UserCacheDir()
24 if err != nil {
25 log.Fatal(err, "No cache directory available")
26 }
27 return path.Join(cacheDir, "photosorter", "seen")
28 }
29
30 func prepareStateDir() {
31 parent := path.Dir(getSeenPath())
32 log.Println("Storing seen cache in:", parent)
33 os.MkdirAll(parent, 0700)
34 }
35
36 func getSeen() map[string]bool {
37 seenFiles := make(map[string]bool)
38 file, err := os.Open(getSeenPath())
39 if err != nil {
40 return seenFiles
41 }
42 defer file.Close()
43 scanner := bufio.NewScanner(file)
44 for scanner.Scan() {
45 seenFiles[scanner.Text()] = true
46 }
47 return seenFiles
48 }
49
50 func setSeen(seenFiles map[string]bool) {
51 file, err := os.Create(getSeenPath())
52 if err != nil {
53 return
54 }
55 defer file.Close()
56 w := bufio.NewWriter(file)
57 for key := range seenFiles {
58 w.WriteString(key)
59 w.WriteString("\n")
60 }
61 w.Flush()
62 }
63
64 func processFile(name string, modified time.Time) {
65 sourceFile := path.Join(sourcePath, name)
66 targetDir := path.Join(targetPath, strconv.Itoa(modified.Year()))
67 targetFile := path.Join(targetDir, name)
68 if _, err := os.Stat(targetFile); errors.Is(err, os.ErrNotExist) {
69 log.Println("Copying src:", sourceFile, "target:", targetFile)
70 r, err := os.Open(sourceFile)
71 if err != nil {
72 return
73 }
74 defer r.Close()
75 os.MkdirAll(targetDir, 0700)
76 w, err := os.Create(targetFile)
77 if err != nil {
78 return
79 }
80 defer w.Close()
81 _, err = w.ReadFrom(r)
82 if err != nil {
83 // Try to clean up for next time
84 os.Remove(targetFile)
85 }
86 } else {
87 log.Println("Skipping because it exists, src:", sourceFile, "target:", targetFile)
88 }
89 }
90
91 func doSort() {
92 log.Println("Starting sort...")
93 didProcess := false
94 seenFiles := getSeen()
95 newSeenFiles := make(map[string]bool)
96 files, err := os.ReadDir(sourcePath)
97 if err != nil {
98 log.Fatal(err)
99 }
100 for _, file := range files {
101 if strings.HasPrefix(file.Name(), ".") {
102 continue
103 }
104 if !file.Type().IsRegular() {
105 continue
106 }
107 info, err := file.Info()
108 if err != nil {
109 continue
110 }
111 newSeenFiles[file.Name()] = true
112 if seenFiles[file.Name()] {
113 continue
114 }
115 processFile(file.Name(), info.ModTime())
116 didProcess = true
117 }
118 setSeen(newSeenFiles)
119 if didProcess {
120 log.Println("Sort complete")
121 } else {
122 log.Println("Sort complete (no changes)")
123 }
124 }
125
126 func sortWorker(changeTimes chan time.Time) {
127 // Handle signals cleanly so we avoid stopping mid-execution
128 stopping := make(chan os.Signal)
129 signal.Notify(stopping, syscall.SIGTERM, syscall.SIGHUP)
130
131 // Initial sort to catch any changes before startup
132 lastRun := time.Now()
133 doSort()
134 for {
135 select {
136 case <-stopping:
137 log.Println("Stopping due to signal")
138 os.Exit(0)
139 case t := <-changeTimes:
140 if t.After(lastRun) {
141 log.Println("Source directory did change")
142 lastRun = time.Now()
143 doSort()
144 }
145 }
146 }
147 }
148
149 func watcher(changeTimes chan time.Time) {
150 w, err := fsnotify.NewWatcher()
151 if err != nil {
152 log.Fatal(err)
153 }
154 defer w.Close()
155 w.Add(sourcePath)
156 for range w.Events {
157 changeTimes <- time.Now()
158 }
159 }
160
161 func main() {
162 const req = "REQUIRED"
163 flag.StringVar(&sourcePath, "s", req, "source directory where photos are uploaded")
164 flag.StringVar(&targetPath, "t", req, "target directory in which year directories are created")
165 flag.Parse()
166 if sourcePath == req || targetPath == req {
167 log.Println("Source and Target paths must both be provided")
168 os.Exit(1)
169 }
170 prepareStateDir()
171 changeTimes := make(chan time.Time, 1024)
172 go sortWorker(changeTimes)
173 watcher(changeTimes)
174 }