Initial commit

This commit is contained in:
2019-08-04 23:47:12 +03:00
commit f1716e1fa2
13 changed files with 623 additions and 0 deletions

10
pkg/core/managemode.go Normal file
View File

@@ -0,0 +1,10 @@
package core
//go:generate stringer -type ManageMode
type ManageMode uint8
const (
Copy ManageMode = iota
Hardlink
)

View File

@@ -0,0 +1,24 @@
// Code generated by "stringer -type ManageMode"; DO NOT EDIT.
package core
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[Copy-0]
_ = x[Hardlink-1]
}
const _ManageMode_name = "CopyHardlink"
var _ManageMode_index = [...]uint8{0, 4, 12}
func (i ManageMode) String() string {
if i >= ManageMode(len(_ManageMode_index)-1) {
return "ManageMode(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ManageMode_name[_ManageMode_index[i]:_ManageMode_index[i+1]]
}

134
pkg/core/manager.go Normal file
View File

@@ -0,0 +1,134 @@
package core
import (
"fmt"
"log"
"os"
"os/exec"
"path"
"strings"
"github.com/pkg/errors"
"photocatalog/pkg/metadata"
)
type Manager struct {
TargetPath string
Mode ManageMode
processor func(fp, targetDir string) (string, error)
extractorsCache map[string]metadata.Extractor
}
func NewManager(target string, mode ManageMode) (*Manager, error) {
manager := Manager{
TargetPath: target,
Mode: mode,
processor: nil,
}
if err := manager.initProcessor(); err != nil {
return nil, err
}
manager.extractorsCache = map[string]metadata.Extractor{}
return &manager, nil
}
func (m *Manager) buildTarget(meta *metadata.Metadata) (string, error) {
dir, err := m.dirPathFromMeta(meta)
if err != nil {
return "", err
}
dirPath := path.Join(m.TargetPath, dir)
err = os.MkdirAll(dirPath, os.FileMode(0770))
if err != nil {
return "", err
}
return dirPath, nil
}
func (m *Manager) dirPathFromMeta(meta *metadata.Metadata) (string, error) {
t := meta.Time
year := t.Format("2006")
month := t.Format("01")
day := t.Format("02")
return path.Join(year, month, day), nil
}
func (m *Manager) getMetadataExtractor(fp string) metadata.Extractor {
switch strings.ToLower(path.Ext(fp)) {
case ".jpeg", ".jpg":
if _, ok := m.extractorsCache["jpeg"]; !ok {
m.extractorsCache["jpeg"] = metadata.NewJpegExtractor()
}
return m.extractorsCache["jpeg"]
default:
if _, ok := m.extractorsCache["default"]; !ok {
m.extractorsCache["default"] = metadata.NewDefaultExtractor()
}
return m.extractorsCache["default"]
}
}
func (m *Manager) initProcessor() error {
switch m.Mode {
case Copy:
m.processor = func(fp, targetDir string) (string, error) {
_, fn := path.Split(fp)
target := path.Join(targetDir, fn)
cmd := exec.Command("cp", "-f", "--reflink=auto", fp, target)
return target, cmd.Run()
}
case Hardlink:
m.processor = func(fp, targetDir string) (string, error) {
_, fn := path.Split(fp)
target := path.Join(targetDir, fn)
cmd := exec.Command("ln", "-f", fp, target)
return target, cmd.Run()
}
default:
return fmt.Errorf("failed to init processor: invalid Mode value")
}
return nil
}
func (m *Manager) Manage(fp string) error {
if m.processor == nil {
return fmt.Errorf("no processor initialized")
}
// Skip hidden files
if strings.HasPrefix(path.Base(fp), ".") {
return nil
}
log.Println("processing", fp)
extractor := m.getMetadataExtractor(fp)
if extractor == nil {
return fmt.Errorf("failed to get md extractor for %s", fp)
}
md, err := extractor.Extract(fp)
if err != nil {
return errors.WithMessagef(err, "failed to extract md from %s", fp)
}
targetDir, err := m.buildTarget(&md)
if err != nil {
return errors.WithMessagef(err, "failed to create dir for %s", fp)
}
target, err := m.processor(fp, targetDir)
if err != nil {
return errors.WithMessagef(err, "failed to process %s to %s", fp, targetDir)
}
if m.Mode == Hardlink {
log.Println(fp, "linked to", target)
} else if m.Mode == Copy {
log.Println(fp, "copied to", target)
}
log.Println("success")
return nil
}

15
pkg/metadata/base.go Normal file
View File

@@ -0,0 +1,15 @@
package metadata
import (
"time"
)
// Metadata contains meta data for the files have to be processed
type Metadata struct {
Time time.Time
}
// Extractor interface for Metadata extractors
type Extractor interface {
Extract(string) (Metadata, error)
}

45
pkg/metadata/default.go Normal file
View File

@@ -0,0 +1,45 @@
package metadata
import (
"path"
"strings"
"time"
)
const defaultTimeLayout = "20060102_150405"
// DefaultExtractor extract metadata from all file types, not covered by special extractors
//
// Gets the meta data from the file's name
type DefaultExtractor struct {
Layout string
}
// NewDefaultExtractor returns new DefaultExtractor's instance
func NewDefaultExtractor() *DefaultExtractor {
return &DefaultExtractor{Layout: defaultTimeLayout}
}
// NewDefaultExtractorWithLayout returns DefaultExtractor with custom time layout
func NewDefaultExtractorWithLayout(l string) *DefaultExtractor {
return &DefaultExtractor{Layout: l}
}
// Extract returns Metadata from specified filename using its name to parse Time
func (d *DefaultExtractor) Extract(fp string) (Metadata, error) {
_, fName := path.Split(fp)
// Remove extension
fName = strings.Replace(fName, path.Ext(fName), "", 1)
// If there more than one photo in one second, cameras append ~N to the end of file name (before extension)
if strings.ContainsRune(fName, '~') {
fName = fName[:strings.IndexRune(fName, '~')]
}
t, err := time.ParseInLocation(d.Layout, fName, time.Local)
if err != nil {
return Metadata{}, err
}
return Metadata{Time: t}, nil
}

View File

@@ -0,0 +1,70 @@
package metadata
import (
"reflect"
"testing"
"time"
)
func TestDefaultExtractor_Extract(t *testing.T) {
local, err := time.LoadLocation("Europe/Moscow")
if err != nil {
t.Fatal(err)
}
time.Local = local
type args struct {
fp string
}
tests := []struct {
name string
args args
want Metadata
wantErr bool
}{
{
name: "Normal",
args: args{"20190321_120325.jpg"},
want: Metadata{
Time: func() time.Time {
tm, err := time.Parse(time.RFC3339, "2019-03-21T12:03:25+03:00")
if err != nil {
t.Fatal(err)
}
return tm
}(),
},
wantErr: false,
},
{
name: "Name with tilda",
args: args{"20190321_120325~2.jpg"},
want: Metadata{
Time: func() time.Time {
tm, err := time.Parse(time.RFC3339, "2019-03-21T12:03:25+03:00")
if err != nil {
t.Fatal(err)
}
return tm
}(),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := NewDefaultExtractor()
if err != nil {
t.Fatal(err)
}
got, err := d.Extract(tt.args.fp)
if (err != nil) != tt.wantErr {
t.Errorf("Extract() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Extract() got = %v, want %v", got, tt.want)
}
})
}
}

40
pkg/metadata/jpeg.go Normal file
View File

@@ -0,0 +1,40 @@
package metadata
import (
"os"
"github.com/rwcarlsen/goexif/exif"
)
// JpegExtractor meta data extractor for the jpeg files
type JpegExtractor struct {
}
// NewJpegExtractor returns new JpegExtractor
func NewJpegExtractor() *JpegExtractor {
return &JpegExtractor{}
}
// Extract returns Metadata from specified jpeg file reading its exif data
//
// TODO: Fallback to default extractor on exif reading/parsing error
func (j *JpegExtractor) Extract(fp string) (Metadata, error) {
f, err := os.Open(fp)
if err != nil {
return Metadata{}, err
}
x, err := exif.Decode(f)
if err != nil {
return Metadata{}, err
}
time, err := x.DateTime()
if err != nil {
return Metadata{}, err
}
meta := Metadata{
Time: time,
}
return meta, nil
}