Backend: Store and index original file names during import #184

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-02-01 20:52:28 +01:00
parent 1c592464bf
commit a4070cf55c
23 changed files with 470 additions and 337 deletions

View file

@ -36,7 +36,7 @@ var rules = LabelRules{
Label: "pumpkin", Label: "pumpkin",
Threshold: 0.400000, Threshold: 0.400000,
Priority: 0, Priority: 0,
Categories: []string{"vegetables"}, Categories: []string{"vegetables", "food"},
}, },
"acoustic guitar": { "acoustic guitar": {
Label: "instrument", Label: "instrument",
@ -270,7 +270,7 @@ var rules = LabelRules{
Label: "vegetables", Label: "vegetables",
Threshold: 0.330000, Threshold: 0.330000,
Priority: 0, Priority: 0,
Categories: []string{"dining"}, Categories: []string{"dining", "food"},
}, },
"ashcan": { "ashcan": {
Label: "", Label: "",
@ -330,7 +330,7 @@ var rules = LabelRules{
Label: "", Label: "",
Threshold: 0.400000, Threshold: 0.400000,
Priority: 0, Priority: 0,
Categories: []string{"bakery"}, Categories: []string{"bakery", "food"},
}, },
"bakery": { "bakery": {
Label: "", Label: "",
@ -378,7 +378,7 @@ var rules = LabelRules{
Label: "", Label: "",
Threshold: 0.300000, Threshold: 0.300000,
Priority: 0, Priority: 0,
Categories: []string{"fruit"}, Categories: []string{"fruits", "food"},
}, },
"band aid": { "band aid": {
Label: "portrait", Label: "portrait",
@ -606,7 +606,7 @@ var rules = LabelRules{
Label: "vegetables", Label: "vegetables",
Threshold: 0.330000, Threshold: 0.330000,
Priority: 0, Priority: 0,
Categories: []string{"dining"}, Categories: []string{"dining", "food"},
}, },
"belt": { "belt": {
Label: "portrait", Label: "portrait",
@ -1005,7 +1005,7 @@ var rules = LabelRules{
Categories: []string{"animal"}, Categories: []string{"animal"},
}, },
"burrito": { "burrito": {
Label: "fast food", Label: "food",
Threshold: 0.330000, Threshold: 0.330000,
Priority: 0, Priority: 0,
Categories: []string{}, Categories: []string{},
@ -1032,7 +1032,7 @@ var rules = LabelRules{
Label: "vegetables", Label: "vegetables",
Threshold: 0.400000, Threshold: 0.400000,
Priority: 0, Priority: 0,
Categories: []string{"dining"}, Categories: []string{"food"},
}, },
"cab": { "cab": {
Label: "", Label: "",
@ -1104,7 +1104,7 @@ var rules = LabelRules{
Label: "pasta", Label: "pasta",
Threshold: 0.200000, Threshold: 0.200000,
Priority: 0, Priority: 0,
Categories: []string{"dining"}, Categories: []string{"dining", "food"},
}, },
"cardigan": { "cardigan": {
Label: "portrait", Label: "portrait",
@ -1245,7 +1245,7 @@ var rules = LabelRules{
Categories: []string{"reptile", "animal", "lizard"}, Categories: []string{"reptile", "animal", "lizard"},
}, },
"cheeseburger": { "cheeseburger": {
Label: "fast food", Label: "food",
Threshold: 0.330000, Threshold: 0.330000,
Priority: 0, Priority: 0,
Categories: []string{}, Categories: []string{},
@ -1314,7 +1314,7 @@ var rules = LabelRules{
Label: "dessert", Label: "dessert",
Threshold: 0.200000, Threshold: 0.200000,
Priority: 0, Priority: 0,
Categories: []string{}, Categories: []string{"food"},
}, },
"chow dog": { "chow dog": {
Label: "dog", Label: "dog",
@ -1500,7 +1500,7 @@ var rules = LabelRules{
Label: "soup", Label: "soup",
Threshold: 0.200000, Threshold: 0.200000,
Priority: 0, Priority: 0,
Categories: []string{"dining"}, Categories: []string{"dining", "food"},
}, },
"container ship": { "container ship": {
Label: "ship", Label: "ship",
@ -1536,7 +1536,7 @@ var rules = LabelRules{
Label: "vegetables", Label: "vegetables",
Threshold: 0.330000, Threshold: 0.330000,
Priority: 0, Priority: 0,
Categories: []string{"dining"}, Categories: []string{"dining", "food"},
}, },
"cornet": { "cornet": {
Label: "instrument", Label: "instrument",
@ -1650,7 +1650,7 @@ var rules = LabelRules{
Label: "vegetables", Label: "vegetables",
Threshold: 0.330000, Threshold: 0.330000,
Priority: 0, Priority: 0,
Categories: []string{"dining"}, Categories: []string{"dining", "food"},
}, },
"cuirass": { "cuirass": {
Label: "portrait", Label: "portrait",
@ -1665,10 +1665,10 @@ var rules = LabelRules{
Categories: []string{"animal"}, Categories: []string{"animal"},
}, },
"custard apple": { "custard apple": {
Label: "fruit", Label: "fruits",
Threshold: 0.200000, Threshold: 0.300000,
Priority: 0, Priority: 0,
Categories: []string{}, Categories: []string{"food"},
}, },
"daisy": { "daisy": {
Label: "flower", Label: "flower",
@ -2048,12 +2048,6 @@ var rules = LabelRules{
Priority: 0, Priority: 0,
Categories: []string{"people"}, Categories: []string{"people"},
}, },
"fast food": {
Label: "fast food",
Threshold: 0.330000,
Priority: 0,
Categories: []string{},
},
"fence": { "fence": {
Label: "outdoor", Label: "outdoor",
Threshold: 0.200000, Threshold: 0.200000,
@ -2067,10 +2061,10 @@ var rules = LabelRules{
Categories: []string{"animal"}, Categories: []string{"animal"},
}, },
"fig": { "fig": {
Label: "fruit", Label: "fruits",
Threshold: 0.300000, Threshold: 0.300000,
Priority: 0, Priority: 0,
Categories: []string{}, Categories: []string{"food"},
}, },
"file": { "file": {
Label: "furniture", Label: "furniture",
@ -2150,6 +2144,12 @@ var rules = LabelRules{
Priority: 0, Priority: 0,
Categories: []string{}, Categories: []string{},
}, },
"food": {
Label: "food",
Threshold: 0.330000,
Priority: 0,
Categories: []string{},
},
"football helmet": { "football helmet": {
Label: "helmet", Label: "helmet",
Threshold: 0.300000, Threshold: 0.300000,
@ -2220,7 +2220,7 @@ var rules = LabelRules{
Label: "", Label: "",
Threshold: 0.400000, Threshold: 0.400000,
Priority: 0, Priority: 0,
Categories: []string{"bakery"}, Categories: []string{"bakery", "food"},
}, },
"frilled lizard": { "frilled lizard": {
Label: "lizard", Label: "lizard",
@ -2234,6 +2234,12 @@ var rules = LabelRules{
Priority: 0, Priority: 0,
Categories: []string{"animal"}, Categories: []string{"animal"},
}, },
"fruits": {
Label: "fruits",
Threshold: 0.300000,
Priority: 0,
Categories: []string{"food"},
},
"frying pan": { "frying pan": {
Label: "cooking", Label: "cooking",
Threshold: 0.400000, Threshold: 0.400000,
@ -2415,10 +2421,10 @@ var rules = LabelRules{
Categories: []string{}, Categories: []string{},
}, },
"granny smith": { "granny smith": {
Label: "apple", Label: "fruits",
Threshold: 0.300000, Threshold: 0.300000,
Priority: 0, Priority: 0,
Categories: []string{"fruit"}, Categories: []string{"food"},
}, },
"grasshopper": { "grasshopper": {
Label: "grasshopper", Label: "grasshopper",
@ -2670,7 +2676,7 @@ var rules = LabelRules{
Label: "vegetables", Label: "vegetables",
Threshold: 0.330000, Threshold: 0.330000,
Priority: 0, Priority: 0,
Categories: []string{"dining"}, Categories: []string{"dining", "food"},
}, },
"helmet": { "helmet": {
Label: "helmet", Label: "helmet",
@ -2784,10 +2790,10 @@ var rules = LabelRules{
Label: "soup", Label: "soup",
Threshold: 0.200000, Threshold: 0.200000,
Priority: 0, Priority: 0,
Categories: []string{"dining"}, Categories: []string{"dining", "food"},
}, },
"hotdog": { "hotdog": {
Label: "fast food", Label: "food",
Threshold: 0.330000, Threshold: 0.330000,
Priority: 0, Priority: 0,
Categories: []string{}, Categories: []string{},
@ -2844,13 +2850,13 @@ var rules = LabelRules{
Label: "dessert", Label: "dessert",
Threshold: 0.300000, Threshold: 0.300000,
Priority: 0, Priority: 0,
Categories: []string{}, Categories: []string{"food"},
}, },
"ice lolly": { "ice lolly": {
Label: "dessert", Label: "dessert",
Threshold: 0.400000, Threshold: 0.400000,
Priority: 0, Priority: 0,
Categories: []string{}, Categories: []string{"food"},
}, },
"ignore": { "ignore": {
Label: "", Label: "",
@ -2961,10 +2967,10 @@ var rules = LabelRules{
Categories: []string{"vegetables"}, Categories: []string{"vegetables"},
}, },
"jackfruit": { "jackfruit": {
Label: "fruit", Label: "fruits",
Threshold: 0.500000, Threshold: 0.300000,
Priority: 0, Priority: 0,
Categories: []string{}, Categories: []string{"food"},
}, },
"jaguar": { "jaguar": {
Label: "", Label: "",
@ -3213,10 +3219,10 @@ var rules = LabelRules{
Categories: []string{"reptile", "animal"}, Categories: []string{"reptile", "animal"},
}, },
"lemon": { "lemon": {
Label: "", Label: "fruits",
Threshold: 0.400000, Threshold: 0.300000,
Priority: 0, Priority: 0,
Categories: []string{"fruit"}, Categories: []string{"food"},
}, },
"lens cap": { "lens cap": {
Label: "photography", Label: "photography",
@ -3498,7 +3504,7 @@ var rules = LabelRules{
Label: "vegetables", Label: "vegetables",
Threshold: 0.330000, Threshold: 0.330000,
Priority: 0, Priority: 0,
Categories: []string{"dining"}, Categories: []string{"dining", "food"},
}, },
"matchstick": { "matchstick": {
Label: "", Label: "",
@ -3528,7 +3534,7 @@ var rules = LabelRules{
Label: "meat", Label: "meat",
Threshold: 0.200000, Threshold: 0.200000,
Priority: 0, Priority: 0,
Categories: []string{"cooking"}, Categories: []string{"cooking", "food"},
}, },
"medicine chest": { "medicine chest": {
Label: "", Label: "",
@ -3879,10 +3885,10 @@ var rules = LabelRules{
Categories: []string{"animal"}, Categories: []string{"animal"},
}, },
"orange": { "orange": {
Label: "", Label: "fruits",
Threshold: 0.400000, Threshold: 0.300000,
Priority: 0, Priority: 0,
Categories: []string{"fruit"}, Categories: []string{"food"},
}, },
"orangutan": { "orangutan": {
Label: "ape", Label: "ape",
@ -4206,7 +4212,7 @@ var rules = LabelRules{
Label: "", Label: "",
Threshold: 0.300000, Threshold: 0.300000,
Priority: 0, Priority: 0,
Categories: []string{"fruit"}, Categories: []string{"fruits", "food"},
}, },
"ping-pong ball": { "ping-pong ball": {
Label: "", Label: "",
@ -4230,7 +4236,7 @@ var rules = LabelRules{
Label: "", Label: "",
Threshold: 0.200000, Threshold: 0.200000,
Priority: 0, Priority: 0,
Categories: []string{"fast food"}, Categories: []string{"food"},
}, },
"plane": { "plane": {
Label: "aircraft", Label: "aircraft",
@ -4299,10 +4305,10 @@ var rules = LabelRules{
Categories: []string{"vehicle"}, Categories: []string{"vehicle"},
}, },
"pomegranate": { "pomegranate": {
Label: "fruit", Label: "fruits",
Threshold: 0.300000, Threshold: 0.300000,
Priority: 0, Priority: 0,
Categories: []string{}, Categories: []string{"food"},
}, },
"pomeranian dog": { "pomeranian dog": {
Label: "dog", Label: "dog",
@ -4350,7 +4356,7 @@ var rules = LabelRules{
Label: "cooking", Label: "cooking",
Threshold: 0.200000, Threshold: 0.200000,
Priority: 0, Priority: 0,
Categories: []string{}, Categories: []string{"food"},
}, },
"potter's wheel": { "potter's wheel": {
Label: "", Label: "",
@ -4380,7 +4386,7 @@ var rules = LabelRules{
Label: "", Label: "",
Threshold: 0.400000, Threshold: 0.400000,
Priority: 0, Priority: 0,
Categories: []string{"bakery"}, Categories: []string{"bakery", "food"},
}, },
"printer": { "printer": {
Label: "office", Label: "office",
@ -5160,7 +5166,7 @@ var rules = LabelRules{
Label: "vegetables", Label: "vegetables",
Threshold: 0.400000, Threshold: 0.400000,
Priority: 0, Priority: 0,
Categories: []string{"dining"}, Categories: []string{"food"},
}, },
"spatula": { "spatula": {
Label: "cooking", Label: "cooking",
@ -5232,7 +5238,7 @@ var rules = LabelRules{
Label: "vegetables", Label: "vegetables",
Threshold: 0.400000, Threshold: 0.400000,
Priority: 0, Priority: 0,
Categories: []string{"dining"}, Categories: []string{"food"},
}, },
"squirrel monkey": { "squirrel monkey": {
Label: "monkey", Label: "monkey",
@ -5337,10 +5343,10 @@ var rules = LabelRules{
Categories: []string{}, Categories: []string{},
}, },
"strawberry": { "strawberry": {
Label: "", Label: "fruits",
Threshold: 0.300000, Threshold: 0.300000,
Priority: 0, Priority: 0,
Categories: []string{"fruit"}, Categories: []string{"food"},
}, },
"streetcar": { "streetcar": {
Label: "", Label: "",
@ -5772,7 +5778,7 @@ var rules = LabelRules{
Label: "dessert", Label: "dessert",
Threshold: 0.200000, Threshold: 0.200000,
Priority: 0, Priority: 0,
Categories: []string{"dining"}, Categories: []string{"dining", "food"},
}, },
"trilobite": { "trilobite": {
Label: "animal", Label: "animal",
@ -5874,7 +5880,7 @@ var rules = LabelRules{
Label: "vegetables", Label: "vegetables",
Threshold: 0.330000, Threshold: 0.330000,
Priority: 0, Priority: 0,
Categories: []string{"dining"}, Categories: []string{"dining", "food"},
}, },
"velvet": { "velvet": {
Label: "nature", Label: "nature",
@ -6246,6 +6252,6 @@ var rules = LabelRules{
Label: "vegetables", Label: "vegetables",
Threshold: 0.330000, Threshold: 0.330000,
Priority: 0, Priority: 0,
Categories: []string{"dining"}, Categories: []string{"dining", "food"},
}, },
} }

View file

@ -3119,61 +3119,74 @@ consomme:
threshold: 0.2 threshold: 0.2
categories: categories:
- dining - dining
- food
hot pot: hot pot:
label: soup label: soup
threshold: 0.2 threshold: 0.2
categories: categories:
- dining - dining
- food
trifle: trifle:
label: dessert label: dessert
threshold: 0.2 threshold: 0.2
categories: categories:
- dining - dining
- food
ice cream: ice cream:
label: dessert label: dessert
threshold: 0.3 threshold: 0.3
categories:
- food
ice lolly: ice lolly:
label: dessert label: dessert
threshold: 0.4 threshold: 0.4
categories:
- food
chocolate sauce: chocolate sauce:
label: dessert label: dessert
threshold: 0.2 threshold: 0.2
categories:
- food
french loaf: french loaf:
threshold: 0.4 threshold: 0.4
categories: categories:
- bakery - bakery
- food
bagel: bagel:
threshold: 0.4 threshold: 0.4
categories: categories:
- bakery - bakery
- food
pretzel: pretzel:
threshold: 0.4 threshold: 0.4
categories: categories:
- bakery - bakery
- food
fast food: food:
label: fast food label: food
threshold: 0.33 threshold: 0.33
cheeseburger: cheeseburger:
see: fast food see: food
hotdog: hotdog:
see: fast food see: food
vegetables: vegetables:
label: vegetables label: vegetables
threshold: 0.33 threshold: 0.33
categories: categories:
- dining - dining
- food
mashed potato: mashed potato:
see: vegetables see: vegetables
@ -3205,7 +3218,7 @@ squash:
label: vegetables label: vegetables
threshold: 0.4 threshold: 0.4
categories: categories:
- dining - food
spaghetti squash: spaghetti squash:
see: squash see: squash
@ -3218,59 +3231,56 @@ acorn squash:
threshold: 0.4 threshold: 0.4
categories: categories:
- vegetables - vegetables
- food
fruits:
label: fruits
threshold: 0.3
categories:
- food
granny smith: granny smith:
label: apple see: fruits
threshold: 0.3
categories:
- fruit
strawberry: strawberry:
threshold: 0.3 see: fruits
categories:
- fruit
orange: orange:
threshold: 0.4 see: fruits
categories:
- fruit
lemon: lemon:
threshold: 0.4 see: fruits
categories:
- fruit
fig: fig:
label: fruit see: fruits
threshold: 0.3
pineapple: pineapple:
threshold: 0.3 threshold: 0.3
categories: categories:
- fruit - fruits
- food
banana: banana:
threshold: 0.3 threshold: 0.3
categories: categories:
- fruit - fruits
- food
jackfruit: jackfruit:
label: fruit see: fruits
threshold: 0.5
custard apple: custard apple:
label: fruit see: fruits
threshold: 0.2
pomegranate: pomegranate:
label: fruit see: fruits
threshold: 0.3
carbonara: carbonara:
label: pasta label: pasta
threshold: 0.2 threshold: 0.2
categories: categories:
- dining - dining
- food
dough: dough:
label: cooking label: cooking
@ -3281,18 +3291,21 @@ meat loaf:
threshold: 0.2 threshold: 0.2
categories: categories:
- cooking - cooking
- food
pizza: pizza:
threshold: 0.2 threshold: 0.2
categories: categories:
- fast food - food
potpie: potpie:
label: cooking label: cooking
threshold: 0.2 threshold: 0.2
categories:
- food
burrito: burrito:
see: fast food see: food
red wine: red wine:
label: wine label: wine

View file

@ -18,8 +18,8 @@ func (c *Config) PublicClientConfig() ClientConfig {
return c.ClientConfig() return c.ClientConfig()
} }
jsHash := fs.Hash(c.HttpStaticBuildPath() + "/app.js") jsHash := fs.Checksum(c.HttpStaticBuildPath() + "/app.js")
cssHash := fs.Hash(c.HttpStaticBuildPath() + "/app.css") cssHash := fs.Checksum(c.HttpStaticBuildPath() + "/app.css")
// Feature Flags // Feature Flags
var flags []string var flags []string
@ -196,8 +196,8 @@ func (c *Config) ClientConfig() ClientConfig {
categories[i].Title = strings.Title(l.LabelName) categories[i].Title = strings.Title(l.LabelName)
} }
jsHash := fs.Hash(c.HttpStaticBuildPath() + "/app.js") jsHash := fs.Checksum(c.HttpStaticBuildPath() + "/app.js")
cssHash := fs.Hash(c.HttpStaticBuildPath() + "/app.css") cssHash := fs.Checksum(c.HttpStaticBuildPath() + "/app.css")
// Feature Flags // Feature Flags
var flags []string var flags []string

View file

@ -12,37 +12,39 @@ import (
// An image or sidecar file that belongs to a photo // An image or sidecar file that belongs to a photo
type File struct { type File struct {
ID uint `gorm:"primary_key"` ID uint `gorm:"primary_key"`
Photo *Photo Photo *Photo
PhotoID uint `gorm:"index;"` PhotoID uint `gorm:"index;"`
PhotoUUID string `gorm:"type:varbinary(36);index;"` PhotoUUID string `gorm:"type:varbinary(36);index;"`
FileUUID string `gorm:"type:varbinary(36);unique_index;"` FileUUID string `gorm:"type:varbinary(36);unique_index;"`
FileName string `gorm:"type:varbinary(600);unique_index"` FileName string `gorm:"type:varbinary(600);unique_index"`
FileHash string `gorm:"type:varbinary(128);unique_index"` OriginalName string `gorm:"type:varbinary(600);"`
FileOriginalName string FileHash string `gorm:"type:varbinary(128);unique_index"`
FileType string `gorm:"type:varbinary(32)"` FileModified time.Time
FileMime string `gorm:"type:varbinary(64)"` FileSize int64
FilePrimary bool FileType string `gorm:"type:varbinary(32)"`
FileSidecar bool FileMime string `gorm:"type:varbinary(64)"`
FileVideo bool FilePrimary bool
FileMissing bool FileSidecar bool
FileDuplicate bool FileVideo bool
FilePortrait bool FileMissing bool
FileWidth int FileDuplicate bool
FileHeight int FilePortrait bool
FileOrientation int FileWidth int
FileAspectRatio float64 FileHeight int
FileMainColor string `gorm:"type:varbinary(16);index;"` FileOrientation int
FileColors string `gorm:"type:binary(9);"` FileAspectRatio float64
FileLuminance string `gorm:"type:binary(9);"` FileMainColor string `gorm:"type:varbinary(16);index;"`
FileChroma uint FileColors string `gorm:"type:binary(9);"`
FileNotes string `gorm:"type:text"` FileLuminance string `gorm:"type:binary(9);"`
FileError string `gorm:"type:varbinary(512)"` FileChroma uint
CreatedAt time.Time FileNotes string `gorm:"type:text"`
CreatedIn int64 FileError string `gorm:"type:varbinary(512)"`
UpdatedAt time.Time CreatedAt time.Time
UpdatedIn int64 CreatedIn int64
DeletedAt *time.Time `sql:"index"` UpdatedAt time.Time
UpdatedIn int64
DeletedAt *time.Time `sql:"index"`
} }
func FindFileByHash(db *gorm.DB, fileHash string) (File, error) { func FindFileByHash(db *gorm.DB, fileHash string) (File, error) {
@ -76,3 +78,15 @@ func (m *File) DownloadFileName() string {
return result return result
} }
func (m File) Changed(fileSize int64, fileModified time.Time) bool {
if m.FileSize != fileSize {
return true
}
if m.FileModified != fileModified {
return true
}
return false
}

View file

@ -7,7 +7,7 @@ import (
// NonCanonical returns true if the file basename is not canonical. // NonCanonical returns true if the file basename is not canonical.
func NonCanonical(basename string) bool { func NonCanonical(basename string) bool {
if len(basename) != 28 { if len(basename) != 24 {
return true return true
} }
@ -22,15 +22,13 @@ func NonCanonical(basename string) bool {
return false return false
} }
// CanonicalName returns a canonical name based on time and hash. // CanonicalName returns a canonical name based on time and CRC32 checksum.
func CanonicalName(date time.Time, hash string) string { func CanonicalName(date time.Time, checksum string) string {
var postfix string if len(checksum) != 8 {
checksum = "ERROR000"
if len(hash) > 12 {
postfix = strings.ToUpper(hash[:12])
} else { } else {
postfix = "NOTFOUND" checksum = strings.ToUpper(checksum)
} }
return date.Format("20060102_150405_") + postfix return date.Format("20060102_150405_") + checksum
} }

View file

@ -48,7 +48,7 @@ func (c *Convert) Start(path string) error {
}() }()
} }
err := filepath.Walk(path, func(filename string, fileInfo os.FileInfo, err error) error { err := filepath.Walk(path, func(fileName string, fileInfo os.FileInfo, err error) error {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
log.Errorf("convert: %s [panic]", err) log.Errorf("convert: %s [panic]", err)
@ -67,7 +67,7 @@ func (c *Convert) Start(path string) error {
return nil return nil
} }
mf, err := NewMediaFile(filename) mf, err := NewMediaFile(fileName)
if err != nil || !(mf.IsRaw() || mf.IsHEIF() || mf.IsImageOther()) { if err != nil || !(mf.IsRaw() || mf.IsHEIF() || mf.IsImageOther()) {
return nil return nil
@ -88,21 +88,21 @@ func (c *Convert) Start(path string) error {
} }
// ConvertCommand returns the command for converting files to JPEG, depending on the format. // ConvertCommand returns the command for converting files to JPEG, depending on the format.
func (c *Convert) ConvertCommand(image *MediaFile, jpegFilename string, xmpFilename string) (result *exec.Cmd, err error) { func (c *Convert) ConvertCommand(image *MediaFile, jpegName string, xmpName string) (result *exec.Cmd, err error) {
if image.IsRaw() { if image.IsRaw() {
if c.conf.SipsBin() != "" { if c.conf.SipsBin() != "" {
result = exec.Command(c.conf.SipsBin(), "-s format jpeg", image.filename, "--out "+jpegFilename) result = exec.Command(c.conf.SipsBin(), "-s format jpeg", image.fileName, "--out "+jpegName)
} else if c.conf.DarktableBin() != "" { } else if c.conf.DarktableBin() != "" {
if xmpFilename != "" { if xmpName != "" {
result = exec.Command(c.conf.DarktableBin(), image.filename, xmpFilename, jpegFilename) result = exec.Command(c.conf.DarktableBin(), image.fileName, xmpName, jpegName)
} else { } else {
result = exec.Command(c.conf.DarktableBin(), image.filename, jpegFilename) result = exec.Command(c.conf.DarktableBin(), image.fileName, jpegName)
} }
} else { } else {
return nil, fmt.Errorf("convert: no binary for raw to jpeg could be found (%s)", image.Filename()) return nil, fmt.Errorf("convert: no binary for raw to jpeg could be found (%s)", image.FileName())
} }
} else if image.IsHEIF() { } else if image.IsHEIF() {
result = exec.Command(c.conf.HeifConvertBin(), image.filename, jpegFilename) result = exec.Command(c.conf.HeifConvertBin(), image.fileName, jpegName)
} else { } else {
return nil, fmt.Errorf("convert: image type not supported for conversion (%s)", image.Type()) return nil, fmt.Errorf("convert: image type not supported for conversion (%s)", image.Type())
} }
@ -113,55 +113,55 @@ func (c *Convert) ConvertCommand(image *MediaFile, jpegFilename string, xmpFilen
// ToJpeg converts a single image file to JPEG if possible. // ToJpeg converts a single image file to JPEG if possible.
func (c *Convert) ToJpeg(image *MediaFile) (*MediaFile, error) { func (c *Convert) ToJpeg(image *MediaFile) (*MediaFile, error) {
if !image.Exists() { if !image.Exists() {
return nil, fmt.Errorf("convert: can not convert to jpeg, file does not exist (%s)", image.Filename()) return nil, fmt.Errorf("convert: can not convert to jpeg, file does not exist (%s)", image.FileName())
} }
if image.IsJpeg() { if image.IsJpeg() {
return image, nil return image, nil
} }
baseFilename := image.DirectoryBasename() base := image.AbsBase()
jpegFilename := baseFilename + ".jpg" jpegName := base + ".jpg"
mediaFile, err := NewMediaFile(jpegFilename) mediaFile, err := NewMediaFile(jpegName)
if err == nil { if err == nil {
return mediaFile, nil return mediaFile, nil
} }
if c.conf.ReadOnly() { if c.conf.ReadOnly() {
return nil, fmt.Errorf("convert: disabled in read only mode (%s)", image.Filename()) return nil, fmt.Errorf("convert: disabled in read only mode (%s)", image.FileName())
} }
log.Infof("convert: \"%s\" -> \"%s\"", image.filename, jpegFilename) fileName := image.RelativeName(c.conf.OriginalsPath())
fileName := image.RelativeFilename(c.conf.OriginalsPath()) log.Infof("convert: %s -> %s", fileName, jpegName)
xmpFilename := baseFilename + ".xmp" xmpName := base + ".xmp"
if _, err := os.Stat(xmpFilename); err != nil { if _, err := os.Stat(xmpName); err != nil {
xmpFilename = "" xmpName = ""
} }
event.Publish("index.converting", event.Data{ event.Publish("index.converting", event.Data{
"fileType": image.Type(), "fileType": image.Type(),
"fileName": fileName, "fileName": fileName,
"baseName": filepath.Base(fileName), "baseName": filepath.Base(fileName),
"xmpName": filepath.Base(xmpFilename), "xmpName": filepath.Base(xmpName),
}) })
if image.IsImageOther() { if image.IsImageOther() {
_, err = thumb.Jpeg(image.Filename(), jpegFilename) _, err = thumb.Jpeg(image.FileName(), jpegName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewMediaFile(jpegFilename) return NewMediaFile(jpegName)
} }
cmd, err := c.ConvertCommand(image, jpegFilename, xmpFilename) cmd, err := c.ConvertCommand(image, jpegName, xmpName)
if err != nil { if err != nil {
return nil, err return nil, err
@ -187,5 +187,5 @@ func (c *Convert) ToJpeg(image *MediaFile) (*MediaFile, error) {
return nil, errors.New(stderr.String()) return nil, errors.New(stderr.String())
} }
return NewMediaFile(jpegFilename) return NewMediaFile(jpegName)
} }

View file

@ -44,13 +44,13 @@ func TestConvert_ToJpeg(t *testing.T) {
infoJpeg, err := imageJpeg.MetaData() infoJpeg, err := imageJpeg.MetaData()
assert.Nilf(t, err, "UpdateExif() failed for "+imageJpeg.Filename()) assert.Nilf(t, err, "UpdateExif() failed for "+imageJpeg.FileName())
if err != nil { if err != nil {
t.Fatalf("%s for %s", err.Error(), imageJpeg.Filename()) t.Fatalf("%s for %s", err.Error(), imageJpeg.FileName())
} }
assert.Equal(t, jpegFilename, imageJpeg.filename) assert.Equal(t, jpegFilename, imageJpeg.fileName)
assert.Equal(t, "Canon EOS 7D", infoJpeg.CameraModel) assert.Equal(t, "Canon EOS 7D", infoJpeg.CameraModel)
@ -76,7 +76,7 @@ func TestConvert_ToJpeg(t *testing.T) {
t.Fatal("imageRaw is nil") t.Fatal("imageRaw is nil")
} }
assert.NotEqual(t, rawFilename, imageRaw.filename) assert.NotEqual(t, rawFilename, imageRaw.fileName)
infoRaw, err := imageRaw.MetaData() infoRaw, err := imageRaw.MetaData()
@ -106,7 +106,7 @@ func TestConvert_Start(t *testing.T) {
t.Fatal(err.Error()) t.Fatal(err.Error())
} }
assert.Equal(t, jpegFilename, image.filename, "FileName must be the same") assert.Equal(t, jpegFilename, image.fileName, "FileName must be the same")
infoRaw, err := image.MetaData() infoRaw, err := image.MetaData()

View file

@ -8,7 +8,7 @@ type ConvertJob struct {
func convertWorker(jobs <-chan ConvertJob) { func convertWorker(jobs <-chan ConvertJob) {
for job := range jobs { for job := range jobs {
if _, err := job.convert.ToJpeg(job.image); err != nil { if _, err := job.convert.ToJpeg(job.image); err != nil {
log.Warnf("convert: %s (%s)", err.Error(), job.image.Filename()) log.Warnf("convert: %s (%s)", err.Error(), job.image.FileName())
} }
} }
} }

View file

@ -78,7 +78,7 @@ func (imp *Import) Start(opt ImportOptions) {
indexOpt := IndexOptionsAll() indexOpt := IndexOptionsAll()
err := filepath.Walk(importPath, func(filename string, fileInfo os.FileInfo, err error) error { err := filepath.Walk(importPath, func(fileName string, fileInfo os.FileInfo, err error) error {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
log.Errorf("import: %s [panic]", err) log.Errorf("import: %s [panic]", err)
@ -89,33 +89,33 @@ func (imp *Import) Start(opt ImportOptions) {
return errors.New("import canceled") return errors.New("import canceled")
} }
if err != nil || done[filename] { if err != nil || done[fileName] {
return nil return nil
} }
if fileInfo.IsDir() { if fileInfo.IsDir() {
if filename != importPath { if fileName != importPath {
directories = append(directories, filename) directories = append(directories, fileName)
} }
return nil return nil
} }
if strings.HasPrefix(filepath.Base(filename), ".") { if strings.HasPrefix(filepath.Base(fileName), ".") {
done[filename] = true done[fileName] = true
if !opt.RemoveDotFiles { if !opt.RemoveDotFiles {
return nil return nil
} }
if err := os.Remove(filename); err != nil { if err := os.Remove(fileName); err != nil {
log.Errorf("import: could not remove \"%s\" (%s)", filename, err.Error()) log.Errorf("import: could not remove \"%s\" (%s)", fileName, err.Error())
} }
return nil return nil
} }
mf, err := NewMediaFile(filename) mf, err := NewMediaFile(fileName)
if err != nil || !mf.IsPhoto() { if err != nil || !mf.IsPhoto() {
return nil return nil
@ -132,20 +132,20 @@ func (imp *Import) Start(opt ImportOptions) {
var files MediaFiles var files MediaFiles
for _, f := range related.files { for _, f := range related.files {
if done[f.Filename()] { if done[f.FileName()] {
continue continue
} }
files = append(files, f) files = append(files, f)
done[f.Filename()] = true done[f.FileName()] = true
} }
done[mf.Filename()] = true done[mf.FileName()] = true
related.files = files related.files = files
jobs <- ImportJob{ jobs <- ImportJob{
filename: filename, fileName: fileName,
related: related, related: related,
indexOpt: indexOpt, indexOpt: indexOpt,
importOpt: opt, importOpt: opt,
@ -167,9 +167,9 @@ func (imp *Import) Start(opt ImportOptions) {
for _, directory := range directories { for _, directory := range directories {
if fs.IsEmpty(directory) { if fs.IsEmpty(directory) {
if err := os.Remove(directory); err != nil { if err := os.Remove(directory); err != nil {
log.Errorf("import: could not deleted empty directory \"%s\" (%s)", directory, err) log.Errorf("import: could not deleted empty directory %s (%s)", directory, err)
} else { } else {
log.Infof("import: deleted empty directory \"%s\"", directory) log.Infof("import: deleted empty directory %s", directory)
} }
} }
} }
@ -191,13 +191,15 @@ func (imp *Import) DestinationFilename(mainFile *MediaFile, mediaFile *MediaFile
fileExtension := mediaFile.Extension() fileExtension := mediaFile.Extension()
dateCreated := mainFile.DateCreated() dateCreated := mainFile.DateCreated()
if f, err := entity.FindFileByHash(imp.conf.Db(), mediaFile.Hash()); err == nil { if !mediaFile.IsSidecar() {
existingFilename := imp.conf.OriginalsPath() + string(os.PathSeparator) + f.FileName if f, err := entity.FindFileByHash(imp.conf.Db(), mediaFile.Hash()); err == nil {
return existingFilename, fmt.Errorf("\"%s\" is identical to \"%s\" (%s)", mediaFile.Filename(), f.FileName, mediaFile.Hash()) existingFilename := imp.conf.OriginalsPath() + string(os.PathSeparator) + f.FileName
return existingFilename, fmt.Errorf("\"%s\" is identical to \"%s\" (%s)", mediaFile.FileName(), f.FileName, mediaFile.Hash())
}
} }
// Mon Jan 2 15:04:05 -0700 MST 2006 // Mon Jan 2 15:04:05 -0700 MST 2006
pathName := imp.originalsPath() + string(os.PathSeparator) + dateCreated.UTC().Format("2006/01") pathName := imp.originalsPath() + string(os.PathSeparator) + dateCreated.Format("2006/01")
iteration := 0 iteration := 0

View file

@ -46,7 +46,7 @@ func TestImport_DestinationFilename(t *testing.T) {
// TODO: Check for errors! // TODO: Check for errors!
assert.Equal(t, conf.OriginalsPath()+"/2019/07/20190705_153230_6E16EB388AD2.cr2", filename) assert.Equal(t, conf.OriginalsPath()+"/2019/07/20190705_153230_C167C6FD.cr2", filename)
} }
func TestImport_Start(t *testing.T) { func TestImport_Start(t *testing.T) {

View file

@ -9,11 +9,10 @@ import (
) )
type ImportJob struct { type ImportJob struct {
filename string fileName string
related RelatedFiles related RelatedFiles
indexOpt IndexOptions indexOpt IndexOptions
importOpt ImportOptions importOpt ImportOptions
path string
imp *Import imp *Import
} }
@ -26,20 +25,27 @@ func importWorker(jobs <-chan ImportJob) {
indexOpt := job.indexOpt indexOpt := job.indexOpt
importPath := job.importOpt.Path importPath := job.importOpt.Path
if related.main == nil {
log.Warnf("import: no main file found for %s", job.fileName)
continue
}
originalName := related.main.RelativeName(importPath)
event.Publish("import.file", event.Data{ event.Publish("import.file", event.Data{
"fileName": related.main.Filename(), "fileName": originalName,
"baseName": filepath.Base(related.main.Filename()), "baseName": filepath.Base(related.main.FileName()),
}) })
for _, f := range related.files { for _, f := range related.files {
relativeFilename := f.RelativeFilename(importPath) relativeFilename := f.RelativeName(importPath)
if destinationFilename, err := imp.DestinationFilename(related.main, f); err == nil { if destinationFilename, err := imp.DestinationFilename(related.main, f); err == nil {
if err := os.MkdirAll(path.Dir(destinationFilename), os.ModePerm); err != nil { if err := os.MkdirAll(path.Dir(destinationFilename), os.ModePerm); err != nil {
log.Errorf("import: could not create directories (%s)", err.Error()) log.Errorf("import: could not create directories (%s)", err.Error())
} }
if related.main.HasSameFilename(f) { if related.main.HasSameName(f) {
destinationMainFilename = destinationFilename destinationMainFilename = destinationFilename
log.Infof("import: moving main %s file \"%s\" to \"%s\"", f.Type(), relativeFilename, destinationFilename) log.Infof("import: moving main %s file \"%s\" to \"%s\"", f.Type(), relativeFilename, destinationFilename)
} else { } else {
@ -48,18 +54,18 @@ func importWorker(jobs <-chan ImportJob) {
if opt.Move { if opt.Move {
if err := f.Move(destinationFilename); err != nil { if err := f.Move(destinationFilename); err != nil {
log.Errorf("import: could not move file to \"%s\" (%s)", destinationMainFilename, err.Error()) log.Errorf("import: could not move file to %s (%s)", destinationMainFilename, err.Error())
} }
} else { } else {
if err := f.Copy(destinationFilename); err != nil { if err := f.Copy(destinationFilename); err != nil {
log.Errorf("import: could not copy file to \"%s\" (%s)", destinationMainFilename, err.Error()) log.Errorf("import: could not copy file to %s (%s)", destinationMainFilename, err.Error())
} }
} }
} else if opt.RemoveExistingFiles { } else if opt.RemoveExistingFiles {
if err := f.Remove(); err != nil { if err := f.Remove(); err != nil {
log.Errorf("import: could not delete file \"%s\" (%s)", f.Filename(), err.Error()) log.Errorf("import: could not delete %s (%s)", f.FileName(), err.Error())
} else { } else {
log.Infof("import: deleted \"%s\" (already exists)", relativeFilename) log.Infof("import: deleted %s (already exists)", relativeFilename)
} }
} }
} }
@ -99,9 +105,9 @@ func importWorker(jobs <-chan ImportJob) {
ind := imp.index ind := imp.index
if related.main != nil { if related.main != nil {
res := ind.MediaFile(related.main, indexOpt) res := ind.MediaFile(related.main, indexOpt, originalName)
log.Infof("import: %s main %s file \"%s\"", res, related.main.Type(), related.main.RelativeFilename(ind.originalsPath())) log.Infof("import: %s main %s file \"%s\"", res, related.main.Type(), related.main.RelativeName(ind.originalsPath()))
done[related.main.Filename()] = true done[related.main.FileName()] = true
} else { } else {
log.Warnf("import: no main file for %s (conversion to jpeg failed?)", destinationMainFilename) log.Warnf("import: no main file for %s (conversion to jpeg failed?)", destinationMainFilename)
} }
@ -111,14 +117,14 @@ func importWorker(jobs <-chan ImportJob) {
continue continue
} }
if done[f.Filename()] { if done[f.FileName()] {
continue continue
} }
res := ind.MediaFile(f, indexOpt) res := ind.MediaFile(f, indexOpt, "")
done[f.Filename()] = true done[f.FileName()] = true
log.Infof("import: %s related %s file \"%s\"", res, f.Type(), f.RelativeFilename(ind.originalsPath())) log.Infof("import: %s related %s file \"%s\"", res, f.Type(), f.RelativeName(ind.originalsPath()))
} }
} }
} }

View file

@ -86,7 +86,7 @@ func (ind *Index) Start(options IndexOptions) map[string]bool {
}() }()
} }
err := filepath.Walk(originalsPath, func(filename string, fileInfo os.FileInfo, err error) error { err := filepath.Walk(originalsPath, func(fileName string, fileInfo os.FileInfo, err error) error {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
log.Errorf("index: %s [panic]", err) log.Errorf("index: %s [panic]", err)
@ -97,15 +97,15 @@ func (ind *Index) Start(options IndexOptions) map[string]bool {
return errors.New("indexing canceled") return errors.New("indexing canceled")
} }
if err != nil || done[filename] { if err != nil || done[fileName] {
return nil return nil
} }
if fileInfo.IsDir() || strings.HasPrefix(filepath.Base(filename), ".") { if fileInfo.IsDir() || strings.HasPrefix(filepath.Base(fileName), ".") {
return nil return nil
} }
mf, err := NewMediaFile(filename) mf, err := NewMediaFile(fileName)
if err != nil || !mf.IsPhoto() { if err != nil || !mf.IsPhoto() {
return nil return nil
@ -122,20 +122,20 @@ func (ind *Index) Start(options IndexOptions) map[string]bool {
var files MediaFiles var files MediaFiles
for _, f := range related.files { for _, f := range related.files {
if done[f.Filename()] { if done[f.FileName()] {
continue continue
} }
files = append(files, f) files = append(files, f)
done[f.Filename()] = true done[f.FileName()] = true
} }
done[mf.Filename()] = true done[mf.FileName()] = true
related.files = files related.files = files
jobs <- IndexJob{ jobs <- IndexJob{
filename: mf.Filename(), filename: mf.FileName(),
related: related, related: related,
opt: options, opt: options,
ind: ind, ind: ind,

View file

@ -24,7 +24,7 @@ const (
type IndexResult string type IndexResult string
func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult { func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) IndexResult {
if m == nil { if m == nil {
log.Error("index: media file is nil - you might have found a bug") log.Error("index: media file is nil - you might have found a bug")
return indexResultFailed return indexResultFailed
@ -39,23 +39,31 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult {
var keywords []string var keywords []string
labels := classify.Labels{} labels := classify.Labels{}
fileBase := m.Basename() fileBase := m.Base()
filePath := m.RelativePath(ind.originalsPath()) filePath := m.RelativePath(ind.originalsPath())
fileName := m.RelativeFilename(ind.originalsPath()) fileName := m.RelativeName(ind.originalsPath())
fileHash := m.Hash() fileHash := ""
fileSize, fileModified := m.Stat()
fileChanged := true fileChanged := true
fileExists := false fileExists := false
photoExists := false photoExists := false
event.Publish("index.indexing", event.Data{ event.Publish("index.indexing", event.Data{
"fileHash": fileHash, "fileHash": fileHash,
"fileSize": fileSize,
"fileName": fileName, "fileName": fileName,
"baseName": filepath.Base(fileName), "baseName": filepath.Base(fileName),
}) })
fileQuery = ind.db.Unscoped().First(&file, "file_hash = ? OR file_name = ?", fileHash, fileName) fileQuery = ind.db.Unscoped().First(&file, "file_name = ?", fileName)
fileExists = fileQuery.Error == nil fileExists = fileQuery.Error == nil
if !fileExists && !m.IsSidecar() {
fileHash = m.Hash()
fileQuery = ind.db.Unscoped().First(&file, "file_hash = ?", fileHash)
fileExists = fileQuery.Error == nil
}
if !fileExists { if !fileExists {
photoQuery = ind.db.Unscoped().First(&photo, "photo_path = ? AND photo_name = ?", filePath, fileBase) photoQuery = ind.db.Unscoped().First(&photo, "photo_path = ? AND photo_name = ?", filePath, fileBase)
@ -65,7 +73,8 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult {
} }
} else { } else {
photoQuery = ind.db.Unscoped().First(&photo, "id = ?", file.PhotoID) photoQuery = ind.db.Unscoped().First(&photo, "id = ?", file.PhotoID)
fileChanged = file.FileHash != fileHash
fileChanged = file.Changed(fileSize, fileModified)
} }
photoExists = photoQuery.Error == nil photoExists = photoQuery.Error == nil
@ -74,6 +83,15 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult {
return indexResultSkipped return indexResultSkipped
} }
if fileHash == "" {
fileHash = m.Hash()
}
if !photoExists {
photo.PhotoPath = filePath
photo.PhotoName = fileBase
}
if !file.FilePrimary { if !file.FilePrimary {
if photoExists { if photoExists {
if q := ind.db.Where("file_type = 'jpg' AND file_primary = 1 AND photo_id = ?", photo.ID).First(&primaryFile); q.Error != nil { if q := ind.db.Where("file_type = 'jpg' AND file_primary = 1 AND photo_id = ?", photo.ID).First(&primaryFile); q.Error != nil {
@ -161,7 +179,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult {
} }
} else if m.IsXMP() { } else if m.IsXMP() {
// TODO: Proof-of-concept for indexing XMP sidecar files // TODO: Proof-of-concept for indexing XMP sidecar files
if data, err := meta.XMP(m.Filename()); err == nil { if data, err := meta.XMP(m.FileName()); err == nil {
if data.Title != "" && !photo.ModifiedTitle { if data.Title != "" && !photo.ModifiedTitle {
photo.PhotoTitle = data.Title photo.PhotoTitle = data.Title
} }
@ -211,6 +229,10 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult {
ind.addLabels(photo.ID, labels) ind.addLabels(photo.ID, labels)
} }
if originalName != "" {
file.OriginalName = originalName
}
file.PhotoID = photo.ID file.PhotoID = photo.ID
file.PhotoUUID = photo.PhotoUUID file.PhotoUUID = photo.PhotoUUID
file.FileSidecar = m.IsSidecar() file.FileSidecar = m.IsSidecar()
@ -218,6 +240,8 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult {
file.FileMissing = false file.FileMissing = false
file.FileName = fileName file.FileName = fileName
file.FileHash = fileHash file.FileHash = fileHash
file.FileSize = fileSize
file.FileModified = fileModified
file.FileType = string(m.Type()) file.FileType = string(m.Type())
file.FileMime = m.MimeType() file.FileMime = m.MimeType()
file.FileOrientation = m.Orientation() file.FileOrientation = m.Orientation()
@ -248,6 +272,11 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult {
keywords = append(keywords, txt.Keywords(fileBase)...) keywords = append(keywords, txt.Keywords(fileBase)...)
} }
if file.OriginalName != "" {
log.Debugf("index: extracting keywords from original file name (%s)", file.OriginalName)
keywords = append(keywords, txt.Keywords(file.OriginalName)...)
}
keywords = append(keywords, file.FileMainColor) keywords = append(keywords, file.FileMainColor)
keywords = append(keywords, labels.Keywords()...) keywords = append(keywords, labels.Keywords()...)
photo.IndexKeywords(keywords, ind.db) photo.IndexKeywords(keywords, ind.db)
@ -292,7 +321,7 @@ func (ind *Index) isNSFW(jpeg *MediaFile) bool {
return false return false
} else { } else {
if nsfwLabels.NSFW() { if nsfwLabels.NSFW() {
log.Warnf("index: \"%s\" might contain offensive content", jpeg.Filename()) log.Warnf("index: \"%s\" might contain offensive content", jpeg.FileName())
return true return true
} }
} }
@ -459,7 +488,7 @@ func (ind *Index) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label
} }
if photo.NoTitle() { if photo.NoTitle() {
log.Warnf("index: could not set photo title based on location or labels for \"%s\"", filepath.Base(mediaFile.Filename())) log.Warnf("index: could not set photo title based on location or labels for \"%s\"", filepath.Base(mediaFile.FileName()))
} else { } else {
log.Infof("index: new photo title is \"%s\"", photo.PhotoTitle) log.Infof("index: new photo title is \"%s\"", photo.PhotoTitle)
} }

View file

@ -15,23 +15,23 @@ func indexWorker(jobs <-chan IndexJob) {
ind := job.ind ind := job.ind
if related.main != nil { if related.main != nil {
res := ind.MediaFile(related.main, opt) res := ind.MediaFile(related.main, opt, "")
done[related.main.Filename()] = true done[related.main.FileName()] = true
log.Infof("index: %s main %s file \"%s\"", res, related.main.Type(), related.main.RelativeFilename(ind.originalsPath())) log.Infof("index: %s main %s file \"%s\"", res, related.main.Type(), related.main.RelativeName(ind.originalsPath()))
} else { } else {
log.Warnf("index: no main file for %s (conversion to jpeg failed?)", job.filename) log.Warnf("index: no main file for %s (conversion to jpeg failed?)", job.filename)
} }
for _, f := range related.files { for _, f := range related.files {
if done[f.Filename()] { if done[f.FileName()] {
continue continue
} }
res := ind.MediaFile(f, opt) res := ind.MediaFile(f, opt, "")
done[f.Filename()] = true done[f.FileName()] = true
log.Infof("index: %s related %s file \"%s\"", res, f.Type(), f.RelativeFilename(ind.originalsPath())) log.Infof("index: %s related %s file \"%s\"", res, f.Type(), f.RelativeName(ind.originalsPath()))
} }
} }
} }

View file

@ -23,53 +23,64 @@ import (
// MediaFile represents a single photo, video or sidecar file. // MediaFile represents a single photo, video or sidecar file.
type MediaFile struct { type MediaFile struct {
filename string fileName string
dateCreated time.Time fileType fs.Type
timeZone string mimeType string
hash string dateCreated time.Time
fileType fs.Type hash string
mimeType string checksum string
perceptualHash string width int
width int height int
height int once sync.Once
once sync.Once metaData meta.Data
metaData meta.Data location *entity.Location
location *entity.Location
} }
// NewMediaFile returns a new MediaFile. // NewMediaFile returns a new media file.
func NewMediaFile(filename string) (*MediaFile, error) { func NewMediaFile(fileName string) (*MediaFile, error) {
if !fs.FileExists(filename) { if !fs.FileExists(fileName) {
return nil, fmt.Errorf("file does not exist: %s", filename) return nil, fmt.Errorf("file does not exist: %s", fileName)
} }
instance := &MediaFile{ instance := &MediaFile{
filename: filename, fileName: fileName,
fileType: fs.TypeOther, fileType: fs.TypeOther,
} }
return instance, nil return instance, nil
} }
// DateCreated returns the date on which the media file was created. // Stat returns the media file size and modification time.
func (m MediaFile) Stat() (size int64, mod time.Time) {
s, err := os.Stat(m.FileName())
if err != nil {
log.Errorf("mediafile: unknown size (%s)", err)
return -1, time.Now()
}
return s.Size(), s.ModTime()
}
// DateCreated returns the date on which the media file was created in UTC.
func (m *MediaFile) DateCreated() time.Time { func (m *MediaFile) DateCreated() time.Time {
if !m.dateCreated.IsZero() { if !m.dateCreated.IsZero() {
return m.dateCreated return m.dateCreated
} }
m.dateCreated = time.Now() m.dateCreated = time.Now().UTC()
info, err := m.MetaData() info, err := m.MetaData()
if err == nil && !info.TakenAt.IsZero() && info.TakenAt.Year() > 1000 { if err == nil && !info.TakenAt.IsZero() && info.TakenAt.Year() > 1000 {
m.dateCreated = info.TakenAt m.dateCreated = info.TakenAt.UTC()
log.Infof("exif: taken at %s", m.dateCreated.String()) log.Infof("exif: taken at %s", m.dateCreated.String())
return m.dateCreated return m.dateCreated
} }
t, err := times.Stat(m.Filename()) t, err := times.Stat(m.FileName())
if err != nil { if err != nil {
log.Debug(err.Error()) log.Debug(err.Error())
@ -78,9 +89,9 @@ func (m *MediaFile) DateCreated() time.Time {
} }
if t.HasBirthTime() { if t.HasBirthTime() {
m.dateCreated = t.BirthTime() m.dateCreated = t.BirthTime().UTC()
} else { } else {
m.dateCreated = t.ModTime() m.dateCreated = t.ModTime().UTC()
} }
log.Infof("mediafile: taken at %s", m.dateCreated.String()) log.Infof("mediafile: taken at %s", m.dateCreated.String())
@ -206,12 +217,12 @@ func (m *MediaFile) Exposure() string {
// CanonicalName returns the canonical name of a media file. // CanonicalName returns the canonical name of a media file.
func (m *MediaFile) CanonicalName() string { func (m *MediaFile) CanonicalName() string {
return CanonicalName(m.DateCreated().UTC(), m.Hash()) return CanonicalName(m.DateCreated(), m.Checksum())
} }
// CanonicalNameFromFile returns the canonical name of a file derived from the image name. // CanonicalNameFromFile returns the canonical name of a file derived from the image name.
func (m *MediaFile) CanonicalNameFromFile() string { func (m *MediaFile) CanonicalNameFromFile() string {
basename := filepath.Base(m.Filename()) basename := filepath.Base(m.FileName())
if end := strings.Index(basename, "."); end != -1 { if end := strings.Index(basename, "."); end != -1 {
return basename[:end] // Length of canonical name: 16 + 12 return basename[:end] // Length of canonical name: 16 + 12
@ -226,21 +237,30 @@ func (m *MediaFile) CanonicalNameFromFileWithDirectory() string {
return m.Directory() + string(os.PathSeparator) + m.CanonicalNameFromFile() return m.Directory() + string(os.PathSeparator) + m.CanonicalNameFromFile()
} }
// Hash return a sha1 hash of a MediaFile based on the filename. // Hash returns the SHA1 hash of a media file.
func (m *MediaFile) Hash() string { func (m *MediaFile) Hash() string {
if len(m.hash) == 0 { if len(m.hash) == 0 {
m.hash = fs.Hash(m.Filename()) m.hash = fs.Hash(m.FileName())
} }
return m.hash return m.hash
} }
// EditedFilename When editing photos, iPhones create additional files like IMG_E12345.JPG // Checksum returns the CRC32 checksum of a media file.
func (m *MediaFile) EditedFilename() string { func (m *MediaFile) Checksum() string {
basename := filepath.Base(m.filename) if len(m.checksum) == 0 {
m.checksum = fs.Checksum(m.FileName())
}
return m.checksum
}
// EditedName When editing photos, iPhones create additional files like IMG_E12345.JPG
func (m *MediaFile) EditedName() string {
basename := filepath.Base(m.fileName)
if strings.ToUpper(basename[:4]) == "IMG_" && strings.ToUpper(basename[:5]) != "IMG_E" { if strings.ToUpper(basename[:4]) == "IMG_" && strings.ToUpper(basename[:5]) != "IMG_E" {
if filename := filepath.Dir(m.filename) + string(os.PathSeparator) + basename[:4] + "E" + basename[4:]; fs.FileExists(filename) { if filename := filepath.Dir(m.fileName) + string(os.PathSeparator) + basename[:4] + "E" + basename[4:]; fs.FileExists(filename) {
return filename return filename
} }
} }
@ -250,7 +270,7 @@ func (m *MediaFile) EditedFilename() string {
// RelatedFiles returns files which are related to this file. // RelatedFiles returns files which are related to this file.
func (m *MediaFile) RelatedFiles() (result RelatedFiles, err error) { func (m *MediaFile) RelatedFiles() (result RelatedFiles, err error) {
baseFilename := m.DirectoryBasename() baseFilename := m.AbsBase()
// escape any meta characters in the file name // escape any meta characters in the file name
baseFilename = regexp.QuoteMeta(baseFilename) baseFilename = regexp.QuoteMeta(baseFilename)
matches, err := filepath.Glob(baseFilename + "*") matches, err := filepath.Glob(baseFilename + "*")
@ -259,7 +279,7 @@ func (m *MediaFile) RelatedFiles() (result RelatedFiles, err error) {
return result, err return result, err
} }
if filename := m.EditedFilename(); filename != "" { if filename := m.EditedName(); filename != "" {
matches = append(matches, filename) matches = append(matches, filename)
} }
@ -276,7 +296,7 @@ func (m *MediaFile) RelatedFiles() (result RelatedFiles, err error) {
result.main = resultFile result.main = resultFile
} else if resultFile.IsHEIF() { } else if resultFile.IsHEIF() {
result.main = resultFile result.main = resultFile
} else if resultFile.IsJpeg() && len(result.main.Filename()) > len(resultFile.Filename()) { } else if resultFile.IsJpeg() && len(result.main.FileName()) > len(resultFile.FileName()) {
result.main = resultFile result.main = resultFile
} else if resultFile.IsImageOther() { } else if resultFile.IsImageOther() {
result.main = resultFile result.main = resultFile
@ -290,34 +310,34 @@ func (m *MediaFile) RelatedFiles() (result RelatedFiles, err error) {
return result, nil return result, nil
} }
// Filename returns the filename. // FileName returns the filename.
func (m MediaFile) Filename() string { func (m MediaFile) FileName() string {
return m.filename return m.fileName
} }
// SetFilename sets the filename to the given string. // SetFileName sets the filename to the given string.
func (m *MediaFile) SetFilename(filename string) { func (m *MediaFile) SetFileName(fileName string) {
m.filename = filename m.fileName = fileName
} }
// RelativeFilename returns the relative filename. // RelativeName returns the relative filename.
func (m MediaFile) RelativeFilename(directory string) string { func (m MediaFile) RelativeName(directory string) string {
if index := strings.Index(m.filename, directory); index == 0 { if index := strings.Index(m.fileName, directory); index == 0 {
if index := strings.LastIndex(directory, string(os.PathSeparator)); index == len(directory)-1 { if index := strings.LastIndex(directory, string(os.PathSeparator)); index == len(directory)-1 {
pos := len(directory) pos := len(directory)
return m.filename[pos:] return m.fileName[pos:]
} else if index := strings.LastIndex(directory, string(os.PathSeparator)); index != len(directory) { } else if index := strings.LastIndex(directory, string(os.PathSeparator)); index != len(directory) {
pos := len(directory) + 1 pos := len(directory) + 1
return m.filename[pos:] return m.fileName[pos:]
} }
} }
return m.filename return m.fileName
} }
// RelativePath returns the relative path without filename. // RelativePath returns the relative path without filename.
func (m MediaFile) RelativePath(directory string) string { func (m MediaFile) RelativePath(directory string) string {
pathname := m.filename pathname := m.fileName
if i := strings.Index(pathname, directory); i == 0 { if i := strings.Index(pathname, directory); i == 0 {
if i := strings.LastIndex(directory, string(os.PathSeparator)); i == len(directory)-1 { if i := strings.LastIndex(directory, string(os.PathSeparator)); i == len(directory)-1 {
@ -336,23 +356,23 @@ func (m MediaFile) RelativePath(directory string) string {
return pathname return pathname
} }
// RelativeBasename returns the relative filename. // RelativeBase returns the relative filename.
func (m MediaFile) RelativeBasename(directory string) string { func (m MediaFile) RelativeBase(directory string) string {
if relativePath := m.RelativePath(directory); relativePath != "" { if relativePath := m.RelativePath(directory); relativePath != "" {
return relativePath + string(os.PathSeparator) + m.Basename() return relativePath + string(os.PathSeparator) + m.Base()
} }
return m.Basename() return m.Base()
} }
// Directory returns the directory // Directory returns the directory
func (m MediaFile) Directory() string { func (m MediaFile) Directory() string {
return filepath.Dir(m.filename) return filepath.Dir(m.fileName)
} }
// Basename returns the filename base without any extensions and path. // Base returns the filename base without any extensions and path.
func (m MediaFile) Basename() string { func (m MediaFile) Base() string {
basename := filepath.Base(m.Filename()) basename := filepath.Base(m.FileName())
if end := strings.Index(basename, "."); end != -1 { if end := strings.Index(basename, "."); end != -1 {
// ignore everything behind the first dot in the file name // ignore everything behind the first dot in the file name
@ -370,24 +390,24 @@ func (m MediaFile) Basename() string {
return basename return basename
} }
// DirectoryBasename returns the directory and base filename without any extensions. // AbsBase returns the directory and base filename without any extensions.
func (m MediaFile) DirectoryBasename() string { func (m MediaFile) AbsBase() string {
return m.Directory() + string(os.PathSeparator) + m.Basename() return m.Directory() + string(os.PathSeparator) + m.Base()
} }
// MimeType returns the mimetype. // MimeType returns the mime type.
func (m *MediaFile) MimeType() string { func (m *MediaFile) MimeType() string {
if m.mimeType != "" { if m.mimeType != "" {
return m.mimeType return m.mimeType
} }
m.mimeType = fs.MimeType(m.Filename()) m.mimeType = fs.MimeType(m.FileName())
return m.mimeType return m.mimeType
} }
func (m *MediaFile) openFile() (*os.File, error) { func (m *MediaFile) openFile() (*os.File, error) {
handle, err := os.Open(m.filename) handle, err := os.Open(m.fileName)
if err != nil { if err != nil {
log.Error(err.Error()) log.Error(err.Error())
return nil, err return nil, err
@ -397,26 +417,30 @@ func (m *MediaFile) openFile() (*os.File, error) {
// Exists checks if a media file exists by filename. // Exists checks if a media file exists by filename.
func (m MediaFile) Exists() bool { func (m MediaFile) Exists() bool {
return fs.FileExists(m.Filename()) return fs.FileExists(m.FileName())
} }
// Remove a media file. // Remove a media file.
func (m MediaFile) Remove() error { func (m MediaFile) Remove() error {
return os.Remove(m.Filename()) return os.Remove(m.FileName())
} }
// HasSameFilename compares a media file with another media file and returns if // HasSameName compares a media file with another media file and returns if
// their filenames are matching or not. // their filenames are matching or not.
func (m MediaFile) HasSameFilename(other *MediaFile) bool { func (m MediaFile) HasSameName(f *MediaFile) bool {
return m.Filename() == other.Filename() if f == nil {
return false
}
return m.FileName() == f.FileName()
} }
// Move file to a new destination with the filename provided in parameter. // Move file to a new destination with the filename provided in parameter.
func (m *MediaFile) Move(newFilename string) error { func (m *MediaFile) Move(newFilename string) error {
if err := os.Rename(m.filename, newFilename); err != nil { if err := os.Rename(m.fileName, newFilename); err != nil {
log.Debugf("could not rename file, falling back to copy and delete: %s", err.Error()) log.Debugf("could not rename file, falling back to copy and delete: %s", err.Error())
} else { } else {
m.filename = newFilename m.fileName = newFilename
return nil return nil
} }
@ -425,11 +449,11 @@ func (m *MediaFile) Move(newFilename string) error {
return err return err
} }
if err := os.Remove(m.filename); err != nil { if err := os.Remove(m.fileName); err != nil {
return err return err
} }
m.filename = newFilename m.fileName = newFilename
return nil return nil
} }
@ -466,7 +490,7 @@ func (m *MediaFile) Copy(destinationFilename string) error {
// Extension returns the filename extension of this media file. // Extension returns the filename extension of this media file.
func (m MediaFile) Extension() string { func (m MediaFile) Extension() string {
return strings.ToLower(filepath.Ext(m.filename)) return strings.ToLower(filepath.Ext(m.fileName))
} }
// IsJpeg return true if this media file is a JPEG image. // IsJpeg return true if this media file is a JPEG image.
@ -545,6 +569,8 @@ func (m MediaFile) IsSidecar() bool {
return true return true
case fs.TypeYaml: case fs.TypeYaml:
return true return true
case fs.TypeJson:
return true
case fs.TypeText: case fs.TypeText:
return true return true
case fs.TypeMarkdown: case fs.TypeMarkdown:
@ -572,14 +598,14 @@ func (m MediaFile) IsPhoto() bool {
// Jpeg returns a the JPEG version of an image or sidecar file (if exists). // Jpeg returns a the JPEG version of an image or sidecar file (if exists).
func (m *MediaFile) Jpeg() (*MediaFile, error) { func (m *MediaFile) Jpeg() (*MediaFile, error) {
if m.IsJpeg() { if m.IsJpeg() {
if !fs.FileExists(m.Filename()) { if !fs.FileExists(m.FileName()) {
return nil, fmt.Errorf("jpeg file should exist, but does not: %s", m.Filename()) return nil, fmt.Errorf("jpeg file should exist, but does not: %s", m.FileName())
} }
return m, nil return m, nil
} }
jpegFilename := fmt.Sprintf("%s.%s", m.DirectoryBasename(), fs.TypeJpeg) jpegFilename := fmt.Sprintf("%s.%s", m.AbsBase(), fs.TypeJpeg)
if !fs.FileExists(jpegFilename) { if !fs.FileExists(jpegFilename) {
return nil, fmt.Errorf("jpeg file does not exist: %s", jpegFilename) return nil, fmt.Errorf("jpeg file does not exist: %s", jpegFilename)
@ -590,7 +616,7 @@ func (m *MediaFile) Jpeg() (*MediaFile, error) {
func (m *MediaFile) decodeDimensions() error { func (m *MediaFile) decodeDimensions() error {
if !m.IsPhoto() { if !m.IsPhoto() {
return fmt.Errorf("not a photo: %s", m.Filename()) return fmt.Errorf("not a photo: %s", m.FileName())
} }
var width, height int var width, height int
@ -603,7 +629,7 @@ func (m *MediaFile) decodeDimensions() error {
} }
if m.IsJpeg() { if m.IsJpeg() {
file, err := os.Open(m.Filename()) file, err := os.Open(m.FileName())
defer file.Close() defer file.Close()
@ -690,7 +716,7 @@ func (m *MediaFile) Thumbnail(path string, typeName string) (filename string, er
return "", fmt.Errorf("mediafile: invalid type %s", typeName) return "", fmt.Errorf("mediafile: invalid type %s", typeName)
} }
thumbnail, err := thumb.FromFile(m.Filename(), m.Hash(), path, thumbType.Width, thumbType.Height, thumbType.Options...) thumbnail, err := thumb.FromFile(m.FileName(), m.Hash(), path, thumbType.Width, thumbType.Height, thumbType.Options...)
if err != nil { if err != nil {
log.Errorf("mediafile: could not create thumbnail (%s)", err) log.Errorf("mediafile: could not create thumbnail (%s)", err)
@ -717,9 +743,12 @@ func (m *MediaFile) ResampleDefault(thumbPath string, force bool) (err error) {
defer func() { defer func() {
switch count { switch count {
case 0: log.Debug(capture.Time(start, fmt.Sprintf("mediafile: no new thumbnails created for %s", m.Basename()))) case 0:
case 1: log.Debug(capture.Time(start, fmt.Sprintf("mediafile: one thumbnail created for %s", m.Basename()))) log.Debug(capture.Time(start, fmt.Sprintf("mediafile: no new thumbnails created for %s", m.Base())))
default: log.Debug(capture.Time(start, fmt.Sprintf("mediafile: %d thumbnails created for %s", count, m.Basename()))) case 1:
log.Debug(capture.Time(start, fmt.Sprintf("mediafile: one thumbnail created for %s", m.Base())))
default:
log.Debug(capture.Time(start, fmt.Sprintf("mediafile: %d thumbnails created for %s", count, m.Base())))
} }
}() }()
@ -747,10 +776,10 @@ func (m *MediaFile) ResampleDefault(thumbPath string, force bool) (err error) {
} }
if originalImg == nil { if originalImg == nil {
img, err := imaging.Open(m.Filename(), imaging.AutoOrientation(true)) img, err := imaging.Open(m.FileName(), imaging.AutoOrientation(true))
if err != nil { if err != nil {
log.Errorf("mediafile: can't open \"%s\" (%s)", m.Filename(), err.Error()) log.Errorf("mediafile: can't open \"%s\" (%s)", m.FileName(), err.Error())
return err return err
} }

View file

@ -201,7 +201,7 @@ func TestMediaFileCanonicalName(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/beach_wood.jpg") mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/beach_wood.jpg")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "20180111_110938_EB4B2A989C20", mediaFile.CanonicalName()) assert.Equal(t, "20180111_110938_B6B8AB4F", mediaFile.CanonicalName())
} }
func TestMediaFileCanonicalNameFromFile(t *testing.T) { func TestMediaFileCanonicalNameFromFile(t *testing.T) {
@ -236,14 +236,14 @@ func TestMediaFile_EditedFilename(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120.JPG") mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120.JPG")
assert.Nil(t, err) assert.Nil(t, err)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, conf.ExamplesPath()+"/IMG_E4120.JPG", mediaFile.EditedFilename()) assert.Equal(t, conf.ExamplesPath()+"/IMG_E4120.JPG", mediaFile.EditedName())
}) })
t.Run("fern_green.jpg", func(t *testing.T) { t.Run("fern_green.jpg", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/fern_green.jpg") mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/fern_green.jpg")
assert.Nil(t, err) assert.Nil(t, err)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "", mediaFile.EditedFilename()) assert.Equal(t, "", mediaFile.EditedName())
}) })
} }
@ -264,9 +264,9 @@ func TestMediaFile_RelatedFiles(t *testing.T) {
assert.Len(t, related.files, 3) assert.Len(t, related.files, 3)
for _, result := range related.files { for _, result := range related.files {
t.Logf("Filename: %s", result.Filename()) t.Logf("FileName: %s", result.FileName())
filename := result.Filename() filename := result.FileName()
extension := result.Extension() extension := result.Extension()
@ -290,9 +290,9 @@ func TestMediaFile_RelatedFiles(t *testing.T) {
assert.Len(t, related.files, 3) assert.Len(t, related.files, 3)
for _, result := range related.files { for _, result := range related.files {
t.Logf("Filename: %s", result.Filename()) t.Logf("FileName: %s", result.FileName())
filename := result.Filename() filename := result.FileName()
extension := result.Extension() extension := result.Extension()
@ -316,12 +316,12 @@ func TestMediaFile_RelatedFiles_Ordering(t *testing.T) {
assert.Len(t, related.files, 5) assert.Len(t, related.files, 5)
assert.Equal(t, conf.ExamplesPath()+"/IMG_4120.AAE", related.files[0].Filename()) assert.Equal(t, conf.ExamplesPath()+"/IMG_4120.AAE", related.files[0].FileName())
assert.Equal(t, conf.ExamplesPath()+"/IMG_4120.JPG", related.files[1].Filename()) assert.Equal(t, conf.ExamplesPath()+"/IMG_4120.JPG", related.files[1].FileName())
for _, result := range related.files { for _, result := range related.files {
filename := result.Filename() filename := result.FileName()
t.Logf("Filename: %s", filename) t.Logf("FileName: %s", filename)
} }
} }
@ -330,10 +330,10 @@ func TestMediaFile_SetFilename(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/turtle_brown_blue.jpg") mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/turtle_brown_blue.jpg")
assert.Nil(t, err) assert.Nil(t, err)
mediaFile.SetFilename("newFilename") mediaFile.SetFileName("newFilename")
assert.Equal(t, "newFilename", mediaFile.filename) assert.Equal(t, "newFilename", mediaFile.fileName)
mediaFile.SetFilename("turtle_brown_blue") mediaFile.SetFileName("turtle_brown_blue")
assert.Equal(t, "turtle_brown_blue", mediaFile.filename) assert.Equal(t, "turtle_brown_blue", mediaFile.fileName)
} }
func TestMediaFile_RelativeFilename(t *testing.T) { func TestMediaFile_RelativeFilename(t *testing.T) {
@ -343,20 +343,20 @@ func TestMediaFile_RelativeFilename(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
t.Run("directory with end slash", func(t *testing.T) { t.Run("directory with end slash", func(t *testing.T) {
filename := mediaFile.RelativeFilename("/go/src/github.com/photoprism/photoprism/assets/resources/") filename := mediaFile.RelativeName("/go/src/github.com/photoprism/photoprism/assets/resources/")
assert.Equal(t, "examples/tree_white.jpg", filename) assert.Equal(t, "examples/tree_white.jpg", filename)
}) })
t.Run("directory without end slash", func(t *testing.T) { t.Run("directory without end slash", func(t *testing.T) {
filename := mediaFile.RelativeFilename("/go/src/github.com/photoprism/photoprism/assets/resources") filename := mediaFile.RelativeName("/go/src/github.com/photoprism/photoprism/assets/resources")
assert.Equal(t, "examples/tree_white.jpg", filename) assert.Equal(t, "examples/tree_white.jpg", filename)
}) })
t.Run("directory not part of filename", func(t *testing.T) { t.Run("directory not part of filename", func(t *testing.T) {
filename := mediaFile.RelativeFilename("xxx/") filename := mediaFile.RelativeName("xxx/")
assert.Equal(t, conf.ExamplesPath()+"/tree_white.jpg", filename) assert.Equal(t, conf.ExamplesPath()+"/tree_white.jpg", filename)
}) })
t.Run("directory equals example path", func(t *testing.T) { t.Run("directory equals example path", func(t *testing.T) {
filename := mediaFile.RelativeFilename("/go/src/github.com/photoprism/photoprism/assets/resources/examples") filename := mediaFile.RelativeName("/go/src/github.com/photoprism/photoprism/assets/resources/examples")
assert.Equal(t, "tree_white.jpg", filename) assert.Equal(t, "tree_white.jpg", filename)
}) })
} }
@ -393,15 +393,15 @@ func TestMediaFile_RelativeBasename(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
t.Run("directory with end slash", func(t *testing.T) { t.Run("directory with end slash", func(t *testing.T) {
basename := mediaFile.RelativeBasename("/go/src/github.com/photoprism/photoprism/assets/resources/") basename := mediaFile.RelativeBase("/go/src/github.com/photoprism/photoprism/assets/resources/")
assert.Equal(t, "examples/tree_white", basename) assert.Equal(t, "examples/tree_white", basename)
}) })
t.Run("directory without end slash", func(t *testing.T) { t.Run("directory without end slash", func(t *testing.T) {
basename := mediaFile.RelativeBasename("/go/src/github.com/photoprism/photoprism/assets/resources") basename := mediaFile.RelativeBase("/go/src/github.com/photoprism/photoprism/assets/resources")
assert.Equal(t, "examples/tree_white", basename) assert.Equal(t, "examples/tree_white", basename)
}) })
t.Run("directory equals example path", func(t *testing.T) { t.Run("directory equals example path", func(t *testing.T) {
basename := mediaFile.RelativeBasename("/go/src/github.com/photoprism/photoprism/assets/resources/examples/") basename := mediaFile.RelativeBase("/go/src/github.com/photoprism/photoprism/assets/resources/examples/")
assert.Equal(t, "tree_white", basename) assert.Equal(t, "tree_white", basename)
}) })
@ -423,21 +423,21 @@ func TestMediaFile_Basename(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/limes.jpg") mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/limes.jpg")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "limes", mediaFile.Basename()) assert.Equal(t, "limes", mediaFile.Base())
}) })
t.Run("/IMG_4120 copy.JPG", func(t *testing.T) { t.Run("/IMG_4120 copy.JPG", func(t *testing.T) {
conf := config.TestConfig() conf := config.TestConfig()
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120 copy.JPG") mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120 copy.JPG")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "IMG_4120", mediaFile.Basename()) assert.Equal(t, "IMG_4120", mediaFile.Base())
}) })
t.Run("/IMG_4120 (1).JPG", func(t *testing.T) { t.Run("/IMG_4120 (1).JPG", func(t *testing.T) {
conf := config.TestConfig() conf := config.TestConfig()
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120 (1).JPG") mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120 (1).JPG")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "IMG_4120", mediaFile.Basename()) assert.Equal(t, "IMG_4120", mediaFile.Base())
}) })
} }
@ -525,7 +525,7 @@ func TestMediaFile_Move(t *testing.T) {
} }
assert.True(t, fs.FileExists(destName)) assert.True(t, fs.FileExists(destName))
assert.Equal(t, destName, m.Filename()) assert.Equal(t, destName, m.FileName())
} }
func TestMediaFile_Copy(t *testing.T) { func TestMediaFile_Copy(t *testing.T) {
@ -892,7 +892,7 @@ func TestMediaFile_Jpeg(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
file, err := mediaFile.Jpeg() file, err := mediaFile.Jpeg()
assert.Nil(t, err) assert.Nil(t, err)
assert.FileExists(t, file.filename) assert.FileExists(t, file.fileName)
}) })
t.Run("/iphone_7.json", func(t *testing.T) { t.Run("/iphone_7.json", func(t *testing.T) {
conf := config.TestConfig() conf := config.TestConfig()

View file

@ -10,8 +10,8 @@ func (f MediaFiles) Len() int {
// Less compares two files based on the filename. // Less compares two files based on the filename.
func (f MediaFiles) Less(i, j int) bool { func (f MediaFiles) Less(i, j int) bool {
fileName1 := f[i].Filename() fileName1 := f[i].FileName()
fileName2 := f[j].Filename() fileName2 := f[j].FileName()
if len(fileName1) == len(fileName2) { if len(fileName1) == len(fileName2) {
return fileName1 < fileName2 return fileName1 < fileName2

View file

@ -6,6 +6,6 @@ import (
// MetaData returns exif meta data of a media file. // MetaData returns exif meta data of a media file.
func (m *MediaFile) MetaData() (result meta.Data, err error) { func (m *MediaFile) MetaData() (result meta.Data, err error) {
m.once.Do(func() { m.metaData, err = meta.Exif(m.Filename()) }) m.once.Do(func() { m.metaData, err = meta.Exif(m.FileName()) })
return m.metaData, err return m.metaData, err
} }

View file

@ -67,7 +67,7 @@ func (rs *Resample) Start(force bool) error {
return nil return nil
} }
fileName := mf.RelativeFilename(originalsPath) fileName := mf.RelativeName(originalsPath)
event.Publish("index.thumbnails", event.Data{ event.Publish("index.thumbnails", event.Data{
"fileName": fileName, "fileName": fileName,

View file

@ -176,7 +176,7 @@ func TestThumb_Create(t *testing.T) {
res, err := thumb.Create(&img, expectedFilename, 150, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor) res, err := thumb.Create(&img, expectedFilename, 150, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
if err != nil || res == nil{ if err != nil || res == nil {
t.Fatal("err should be nil and res should NOT be nil") t.Fatal("err should be nil and res should NOT be nil")
} }
@ -203,7 +203,7 @@ func TestThumb_Create(t *testing.T) {
res, err := thumb.Create(&img, expectedFilename, -1, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor) res, err := thumb.Create(&img, expectedFilename, -1, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
if err == nil || res == nil{ if err == nil || res == nil {
t.Fatal("err and res should NOT be nil") t.Fatal("err and res should NOT be nil")
} }

View file

@ -3,11 +3,12 @@ package fs
import ( import (
"crypto/sha1" "crypto/sha1"
"encoding/hex" "encoding/hex"
"hash/crc32"
"io" "io"
"os" "os"
) )
// Hash returns the sha1 hash of file as string. // Hash returns the SHA1 hash of a file as string.
func Hash(filename string) string { func Hash(filename string) string {
var result []byte var result []byte
@ -27,3 +28,24 @@ func Hash(filename string) string {
return hex.EncodeToString(hash.Sum(result)) return hex.EncodeToString(hash.Sum(result))
} }
// Checksum returns the CRC32 checksum of a file as string.
func Checksum(filename string) string {
var result []byte
file, err := os.Open(filename)
if err != nil {
return ""
}
defer file.Close()
hash := crc32.New(crc32.MakeTable(crc32.Castagnoli))
if _, err := io.Copy(hash, file); err != nil {
return ""
}
return hex.EncodeToString(hash.Sum(result))
}

View file

@ -16,3 +16,14 @@ func TestHash(t *testing.T) {
assert.Equal(t, "", hash) assert.Equal(t, "", hash)
}) })
} }
func TestChecksum(t *testing.T) {
t.Run("existing image", func(t *testing.T) {
hash := Checksum("testdata/test.jpg")
assert.Equal(t, "5239d867", hash)
})
t.Run("not existing image", func(t *testing.T) {
hash := Checksum("testdata/xxx.jpg")
assert.Equal(t, "", hash)
})
}

View file

@ -33,6 +33,8 @@ const (
TypeXML Type = "xml" TypeXML Type = "xml"
// YAML metadata / config / sidecar file. // YAML metadata / config / sidecar file.
TypeYaml Type = "yml" TypeYaml Type = "yml"
// JSON metadata / config / sidecar file.
TypeJson Type = "json"
// Text config / sidecar file. // Text config / sidecar file.
TypeText Type = "txt" TypeText Type = "txt"
// Markdown text sidecar file. // Markdown text sidecar file.
@ -106,4 +108,5 @@ var Ext = map[string]Type{
".xml": TypeXML, ".xml": TypeXML,
".txt": TypeText, ".txt": TypeText,
".md": TypeMarkdown, ".md": TypeMarkdown,
".json": TypeJson,
} }