diff --git a/arrange.go b/arrange.go index aee3f8d..7a025a5 100644 --- a/arrange.go +++ b/arrange.go @@ -3,7 +3,9 @@ package arrange import ( "crypto/md5" "fmt" + "image/gif" "image/jpeg" + "image/png" "io" "log" "os" @@ -30,8 +32,13 @@ func init() { } } -type File interface { - Move(root string) error +func mtime(path string) (time.Time, error) { + ti := time.Time{} + s, err := os.Stat(path) + if err != nil { + return ti, fmt.Errorf("failure to collect times from stat: %v", err) + } + return s.ModTime(), nil } func PrepOutput(root string) error { @@ -74,8 +81,8 @@ func Source(root string) <-chan string { return out } -func Parse(in <-chan string) <-chan File { - out := make(chan File) +func Parse(in <-chan string) <-chan Media { + out := make(chan Media) go func() { for path := range in { f, err := _parse(path) @@ -97,7 +104,7 @@ func Parse(in <-chan string) <-chan File { return out } -func Move(in <-chan File, root string) <-chan error { +func Move(in <-chan Media, root string) <-chan error { out := make(chan error) go func() { for i := range in { @@ -108,28 +115,30 @@ func Move(in <-chan File, root string) <-chan error { return out } -func _parse(path string) (File, error) { +func _parse(path string) (Media, error) { ext := strings.ToLower(filepath.Ext(path)) - var r File + var r Media + hash := md5.New() + var t time.Time + + f, err := os.Open(path) + if err != nil { + return r, fmt.Errorf("problem opening file: %v", err) + } + defer f.Close() + switch ext { default: - return nil, NotMedia{path} + return r, NotMedia{path} case ".jpg", ".jpeg": - f, err := os.Open(path) - if err != nil { - return nil, fmt.Errorf("problem opening file: %v", err) - } - defer f.Close() - if _, err := jpeg.DecodeConfig(f); err != nil { - return nil, NotMedia{path} + return r, NotMedia{path} } if _, err := f.Seek(0, 0); err != nil { - return nil, fmt.Errorf("couldn't seek back in file: %v", err) + return r, fmt.Errorf("couldn't seek back in file: %v", err) } // try a few things for a time value - var t time.Time { success := false if t, err = parseExif(f); err == nil { @@ -139,34 +148,59 @@ func _parse(path string) (File, error) { t, err = mtime(path) } if err != nil { - return nil, fmt.Errorf("unable to calculate reasonble time for jpg %q: %v", path, err) + return r, fmt.Errorf("unable to calculate reasonble time for jpg %q: %v", path, err) } } - - if _, err := f.Seek(0, 0); err != nil { - return nil, fmt.Errorf("couldn't seek back in file: %v", err) - } - hash := md5.New() - if _, err := io.Copy(hash, f); err != nil { - return nil, fmt.Errorf("problem calculating checksum on %q: %v", path, err) - } - r = Image{ - Path: path, - Hash: fmt.Sprintf("%x", hash.Sum(nil)), - Time: t, - } case ".png": - return nil, fmt.Errorf("NYI: %q", path) + if _, err := png.DecodeConfig(f); err != nil { + return r, NotMedia{path} + } + if _, err := f.Seek(0, 0); err != nil { + return r, fmt.Errorf("couldn't seek back in file: %v", err) + } + + t, err = mtime(path) + if err != nil { + return r, fmt.Errorf("unable to calculate reasonble time for media %q: %v", path, err) + } + case ".gif": + if _, err := gif.DecodeConfig(f); err != nil { + return r, NotMedia{path} + } + if _, err := f.Seek(0, 0); err != nil { + return r, fmt.Errorf("couldn't seek back in file: %v", err) + } + + t, err = mtime(path) + if err != nil { + return r, fmt.Errorf("unable to calculate reasonble time for media %q: %v", path, err) + } case ".mov", ".mp4", ".m4v": - return nil, fmt.Errorf("NYI: %q", path) + t, err = mtime(path) + if err != nil { + return r, fmt.Errorf("unable to calculate reasonble time for media %q: %v", path, err) + } + } + + if _, err := f.Seek(0, 0); err != nil { + return r, fmt.Errorf("couldn't seek back in file: %v", err) + } + if _, err := io.Copy(hash, f); err != nil { + return r, fmt.Errorf("problem calculating checksum on %q: %v", path, err) + } + r = Media{ + Path: path, + Hash: fmt.Sprintf("%x", hash.Sum(nil)), + Extension: ext, + Time: t, } return r, nil } -func Merge(cs []<-chan File) <-chan File { - out := make(chan File) +func Merge(cs []<-chan Media) <-chan Media { + out := make(chan Media) var wg sync.WaitGroup - output := func(c <-chan File) { + output := func(c <-chan Media) { for n := range c { out <- n } diff --git a/cmd/am/main.go b/cmd/am/main.go index fe2c5fc..d51dfc2 100644 --- a/cmd/am/main.go +++ b/cmd/am/main.go @@ -35,7 +35,7 @@ func main() { } work := arrange.Source(in) - streams := []<-chan arrange.File{} + streams := []<-chan arrange.Media{} workers := runtime.NumCPU() if *cores != 0 { diff --git a/image.go b/image.go index 77f98e6..8a67e17 100644 --- a/image.go +++ b/image.go @@ -1,75 +1,13 @@ package arrange import ( - "errors" "fmt" "io" - "os" - "path/filepath" "time" "github.com/rwcarlsen/goexif/exif" ) -type Media struct { - Path string -} - -func (m Media) Move(root string) error { - return errors.New("NYI") -} - -type Image struct { - Path string - Hash string - Time time.Time -} - -func (im Image) Move(root string) error { - f, err := os.Open(im.Path) - if err != nil { - return fmt.Errorf("problem opening jpg file: %v", err) - } - defer f.Close() - - content := filepath.Join(root, "content", im.Hash[:2], im.Hash[2:]+".jpg") - - if _, err := os.Stat(content); !os.IsNotExist(err) { - return Dup{content} - } - - out, err := os.Create(content) - if err != nil { - return fmt.Errorf("could not create output file: %v", err) - } - defer out.Close() - - if _, err := io.Copy(out, f); err != nil { - return fmt.Errorf("trouble copying file: %v", err) - } - - year := fmt.Sprintf("%04d", im.Time.Year()) - month := fmt.Sprintf("%02d", im.Time.Month()) - ts := fmt.Sprintf("%d", im.Time.UnixNano()) - - if err := os.MkdirAll(filepath.Join(root, "date", year, month), 0755); err != nil { - return fmt.Errorf("problem creating date directory: %v", err) - } - - date := filepath.Join(root, "date", year, month, ts) - name := date + ".jpg" - for i := 0; i < 10000; i++ { - if _, err := os.Stat(name); os.IsNotExist(err) { - break - } - name = fmt.Sprintf("%s_%04d.jpg", date, i) - } - - // TODO: or maybe symlinking? (issue #2) - // rel := filepath.Join("..", "..", "..", "content", j.hash[:2], j.hash[2:]+".jpg") - // return os.Symlink(rel, name) - return os.Link(content, name) -} func parseExif(f io.Reader) (time.Time, error) { ti := time.Time{} x, err := exif.Decode(f) @@ -84,12 +22,3 @@ func parseExif(f io.Reader) (time.Time, error) { } return tm, nil } - -func mtime(path string) (time.Time, error) { - ti := time.Time{} - s, err := os.Stat(path) - if err != nil { - return ti, fmt.Errorf("failure to collect times from stat: %v", err) - } - return s.ModTime(), nil -} diff --git a/media.go b/media.go new file mode 100644 index 0000000..25f0fbe --- /dev/null +++ b/media.go @@ -0,0 +1,62 @@ +package arrange + +import ( + "fmt" + "io" + "os" + "path/filepath" + "time" +) + +type Media struct { + Path string + Hash string + Extension string + Time time.Time +} + +func (m Media) Move(root string) error { + f, err := os.Open(m.Path) + if err != nil { + return fmt.Errorf("problem opening file %q: %v", m.Path, err) + } + defer f.Close() + + content := filepath.Join(root, "content", m.Hash[:2], m.Hash[2:]+m.Extension) + + if _, err := os.Stat(content); !os.IsNotExist(err) { + return Dup{content} + } + + out, err := os.Create(content) + if err != nil { + return fmt.Errorf("could not create output file: %v", err) + } + defer out.Close() + + if _, err := io.Copy(out, f); err != nil { + return fmt.Errorf("trouble copying file: %v", err) + } + + year := fmt.Sprintf("%04d", m.Time.Year()) + month := fmt.Sprintf("%02d", m.Time.Month()) + ts := fmt.Sprintf("%d", m.Time.UnixNano()) + + if err := os.MkdirAll(filepath.Join(root, "date", year, month), 0755); err != nil { + return fmt.Errorf("problem creating date directory: %v", err) + } + + date := filepath.Join(root, "date", year, month, ts) + name := date + m.Extension + for i := 0; i < 10000; i++ { + if _, err := os.Stat(name); os.IsNotExist(err) { + break + } + name = fmt.Sprintf("%s_%04d%s", date, i, m.Extension) + } + + // TODO: or maybe symlinking? (issue #2) + // rel := filepath.Join("..", "..", "..", "content", j.hash[:2], j.hash[2:]+m.Extension) + // return os.Symlink(rel, name) + return os.Link(content, name) +}