Initial commit
This commit is contained in:
commit
ba95e927fe
|
@ -0,0 +1,158 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rwcarlsen/goexif/exif"
|
||||||
|
)
|
||||||
|
|
||||||
|
type processFn func(path, format string) error
|
||||||
|
|
||||||
|
var counters = make(map[string]int)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
srcDir := flag.String("in", ".", "Source directory")
|
||||||
|
format := flag.String("out", "dist/%Y/%m/%d-%H%M%S",
|
||||||
|
"Output format (use strftime symbols %Y, %y, %m, %d, %H, %M, %S)",
|
||||||
|
)
|
||||||
|
dryRun := flag.Bool("dryrun", false, "Dry run")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
formatNorm := toTimeFormat(*format)
|
||||||
|
var fn processFn
|
||||||
|
n := -1
|
||||||
|
if *dryRun {
|
||||||
|
fmt.Println("Dry run. Examples of moved files:")
|
||||||
|
fn = previewFile
|
||||||
|
n = 10
|
||||||
|
} else {
|
||||||
|
fn = processFile
|
||||||
|
}
|
||||||
|
if err := processDir(*srcDir, formatNorm, n, fn); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processDir(dir, format string, max int, fn processFn) error {
|
||||||
|
var i int
|
||||||
|
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if i < 0 || i > max {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
i++
|
||||||
|
return fn(path, format)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func processFile(path, format string) error {
|
||||||
|
if ext := strings.ToLower(filepath.Ext(path)); !isSupportedExt(ext) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
src, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
ts, err := getDate(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newPath := ts.Format(format) + "-" + strconv.Itoa(nextIndex(ts)) + ".jpg"
|
||||||
|
dir := filepath.Dir(newPath)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dest, err := os.Create(newPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dest.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(dest, src); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func previewFile(path, format string) error {
|
||||||
|
if ext := strings.ToLower(filepath.Ext(path)); !isSupportedExt(ext) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
src, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
ts, err := getDate(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newPath := ts.Format(format) + "-" + strconv.Itoa(nextIndex(ts)) + ".jpg"
|
||||||
|
fmt.Printf("%s -> %s\n", path, newPath)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSupportedExt(ext string) bool {
|
||||||
|
switch ext {
|
||||||
|
case ".jpg":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDate(fd io.ReadSeeker) (time.Time, error) {
|
||||||
|
ef, err := exif.Decode(fd)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fd.Seek(0, 0)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ef.DateTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextIndex(ts time.Time) int {
|
||||||
|
key := ts.Format("20060102150405")
|
||||||
|
counters[key]++
|
||||||
|
return counters[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func toTimeFormat(f string) string {
|
||||||
|
f = strings.Replace(f, "%y", "2006", -1)
|
||||||
|
f = strings.Replace(f, "%Y", "06", -1)
|
||||||
|
f = strings.Replace(f, "%m", "01", -1)
|
||||||
|
f = strings.Replace(f, "%d", "02", -1)
|
||||||
|
|
||||||
|
f = strings.Replace(f, "%H", "15", -1)
|
||||||
|
f = strings.Replace(f, "%M", "04", -1)
|
||||||
|
f = strings.Replace(f, "%S", "05", -1)
|
||||||
|
|
||||||
|
return f
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToTimeFormat(t *testing.T) {
|
||||||
|
assertEqualStr(t, "dist/photos/06/01/02-150405", toTimeFormat("dist/photos/%Y/%m/%d-%H%M%S"))
|
||||||
|
assertEqualStr(t, "dist/2006-01-02/150405", toTimeFormat("dist/%y-%m-%d/%H%M%S"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertEqualStr(t *testing.T, exp, out string) {
|
||||||
|
t.Helper()
|
||||||
|
if exp != out {
|
||||||
|
t.Errorf("Expected %q, got %q", exp, out)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue