added primative support for png, gif others.
This commit is contained in:
parent
126a948fd4
commit
57d3e5fa1f
106
arrange.go
106
arrange.go
@ -3,7 +3,9 @@ package arrange
|
|||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image/gif"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
@ -30,8 +32,13 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type File interface {
|
func mtime(path string) (time.Time, error) {
|
||||||
Move(root string) 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 {
|
func PrepOutput(root string) error {
|
||||||
@ -74,8 +81,8 @@ func Source(root string) <-chan string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func Parse(in <-chan string) <-chan File {
|
func Parse(in <-chan string) <-chan Media {
|
||||||
out := make(chan File)
|
out := make(chan Media)
|
||||||
go func() {
|
go func() {
|
||||||
for path := range in {
|
for path := range in {
|
||||||
f, err := _parse(path)
|
f, err := _parse(path)
|
||||||
@ -97,7 +104,7 @@ func Parse(in <-chan string) <-chan File {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func Move(in <-chan File, root string) <-chan error {
|
func Move(in <-chan Media, root string) <-chan error {
|
||||||
out := make(chan error)
|
out := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
for i := range in {
|
for i := range in {
|
||||||
@ -108,28 +115,30 @@ func Move(in <-chan File, root string) <-chan error {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func _parse(path string) (File, error) {
|
func _parse(path string) (Media, error) {
|
||||||
ext := strings.ToLower(filepath.Ext(path))
|
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 {
|
switch ext {
|
||||||
default:
|
default:
|
||||||
return nil, NotMedia{path}
|
return r, NotMedia{path}
|
||||||
case ".jpg", ".jpeg":
|
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 {
|
if _, err := jpeg.DecodeConfig(f); err != nil {
|
||||||
return nil, NotMedia{path}
|
return r, NotMedia{path}
|
||||||
}
|
}
|
||||||
if _, err := f.Seek(0, 0); err != nil {
|
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
|
// try a few things for a time value
|
||||||
var t time.Time
|
|
||||||
{
|
{
|
||||||
success := false
|
success := false
|
||||||
if t, err = parseExif(f); err == nil {
|
if t, err = parseExif(f); err == nil {
|
||||||
@ -139,34 +148,59 @@ func _parse(path string) (File, error) {
|
|||||||
t, err = mtime(path)
|
t, err = mtime(path)
|
||||||
}
|
}
|
||||||
if err != nil {
|
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":
|
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":
|
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
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Merge(cs []<-chan File) <-chan File {
|
func Merge(cs []<-chan Media) <-chan Media {
|
||||||
out := make(chan File)
|
out := make(chan Media)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
output := func(c <-chan File) {
|
output := func(c <-chan Media) {
|
||||||
for n := range c {
|
for n := range c {
|
||||||
out <- n
|
out <- n
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
work := arrange.Source(in)
|
work := arrange.Source(in)
|
||||||
streams := []<-chan arrange.File{}
|
streams := []<-chan arrange.Media{}
|
||||||
|
|
||||||
workers := runtime.NumCPU()
|
workers := runtime.NumCPU()
|
||||||
if *cores != 0 {
|
if *cores != 0 {
|
||||||
|
71
image.go
71
image.go
@ -1,75 +1,13 @@
|
|||||||
package arrange
|
package arrange
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rwcarlsen/goexif/exif"
|
"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) {
|
func parseExif(f io.Reader) (time.Time, error) {
|
||||||
ti := time.Time{}
|
ti := time.Time{}
|
||||||
x, err := exif.Decode(f)
|
x, err := exif.Decode(f)
|
||||||
@ -84,12 +22,3 @@ func parseExif(f io.Reader) (time.Time, error) {
|
|||||||
}
|
}
|
||||||
return tm, nil
|
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
|
|
||||||
}
|
|
||||||
|
62
media.go
Normal file
62
media.go
Normal file
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user