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

View file

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

View file

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

View file

@ -18,8 +18,10 @@ type File struct {
PhotoUUID string `gorm:"type:varbinary(36);index;"`
FileUUID string `gorm:"type:varbinary(36);unique_index;"`
FileName string `gorm:"type:varbinary(600);unique_index"`
OriginalName string `gorm:"type:varbinary(600);"`
FileHash string `gorm:"type:varbinary(128);unique_index"`
FileOriginalName string
FileModified time.Time
FileSize int64
FileType string `gorm:"type:varbinary(32)"`
FileMime string `gorm:"type:varbinary(64)"`
FilePrimary bool
@ -76,3 +78,15 @@ func (m *File) DownloadFileName() string {
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.
func NonCanonical(basename string) bool {
if len(basename) != 28 {
if len(basename) != 24 {
return true
}
@ -22,15 +22,13 @@ func NonCanonical(basename string) bool {
return false
}
// CanonicalName returns a canonical name based on time and hash.
func CanonicalName(date time.Time, hash string) string {
var postfix string
if len(hash) > 12 {
postfix = strings.ToUpper(hash[:12])
// CanonicalName returns a canonical name based on time and CRC32 checksum.
func CanonicalName(date time.Time, checksum string) string {
if len(checksum) != 8 {
checksum = "ERROR000"
} 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() {
if err := recover(); err != nil {
log.Errorf("convert: %s [panic]", err)
@ -67,7 +67,7 @@ func (c *Convert) Start(path string) error {
return nil
}
mf, err := NewMediaFile(filename)
mf, err := NewMediaFile(fileName)
if err != nil || !(mf.IsRaw() || mf.IsHEIF() || mf.IsImageOther()) {
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.
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 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() != "" {
if xmpFilename != "" {
result = exec.Command(c.conf.DarktableBin(), image.filename, xmpFilename, jpegFilename)
if xmpName != "" {
result = exec.Command(c.conf.DarktableBin(), image.fileName, xmpName, jpegName)
} else {
result = exec.Command(c.conf.DarktableBin(), image.filename, jpegFilename)
result = exec.Command(c.conf.DarktableBin(), image.fileName, jpegName)
}
} 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() {
result = exec.Command(c.conf.HeifConvertBin(), image.filename, jpegFilename)
result = exec.Command(c.conf.HeifConvertBin(), image.fileName, jpegName)
} else {
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.
func (c *Convert) ToJpeg(image *MediaFile) (*MediaFile, error) {
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() {
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 {
return mediaFile, nil
}
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 {
xmpFilename = ""
if _, err := os.Stat(xmpName); err != nil {
xmpName = ""
}
event.Publish("index.converting", event.Data{
"fileType": image.Type(),
"fileName": fileName,
"baseName": filepath.Base(fileName),
"xmpName": filepath.Base(xmpFilename),
"xmpName": filepath.Base(xmpName),
})
if image.IsImageOther() {
_, err = thumb.Jpeg(image.Filename(), jpegFilename)
_, err = thumb.Jpeg(image.FileName(), jpegName)
if err != nil {
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 {
return nil, err
@ -187,5 +187,5 @@ func (c *Convert) ToJpeg(image *MediaFile) (*MediaFile, error) {
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()
assert.Nilf(t, err, "UpdateExif() failed for "+imageJpeg.Filename())
assert.Nilf(t, err, "UpdateExif() failed for "+imageJpeg.FileName())
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)
@ -76,7 +76,7 @@ func TestConvert_ToJpeg(t *testing.T) {
t.Fatal("imageRaw is nil")
}
assert.NotEqual(t, rawFilename, imageRaw.filename)
assert.NotEqual(t, rawFilename, imageRaw.fileName)
infoRaw, err := imageRaw.MetaData()
@ -106,7 +106,7 @@ func TestConvert_Start(t *testing.T) {
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()

View file

@ -8,7 +8,7 @@ type ConvertJob struct {
func convertWorker(jobs <-chan ConvertJob) {
for job := range jobs {
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()
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() {
if err := recover(); err != nil {
log.Errorf("import: %s [panic]", err)
@ -89,33 +89,33 @@ func (imp *Import) Start(opt ImportOptions) {
return errors.New("import canceled")
}
if err != nil || done[filename] {
if err != nil || done[fileName] {
return nil
}
if fileInfo.IsDir() {
if filename != importPath {
directories = append(directories, filename)
if fileName != importPath {
directories = append(directories, fileName)
}
return nil
}
if strings.HasPrefix(filepath.Base(filename), ".") {
done[filename] = true
if strings.HasPrefix(filepath.Base(fileName), ".") {
done[fileName] = true
if !opt.RemoveDotFiles {
return nil
}
if err := os.Remove(filename); err != nil {
log.Errorf("import: could not remove \"%s\" (%s)", filename, err.Error())
if err := os.Remove(fileName); err != nil {
log.Errorf("import: could not remove \"%s\" (%s)", fileName, err.Error())
}
return nil
}
mf, err := NewMediaFile(filename)
mf, err := NewMediaFile(fileName)
if err != nil || !mf.IsPhoto() {
return nil
@ -132,20 +132,20 @@ func (imp *Import) Start(opt ImportOptions) {
var files MediaFiles
for _, f := range related.files {
if done[f.Filename()] {
if done[f.FileName()] {
continue
}
files = append(files, f)
done[f.Filename()] = true
done[f.FileName()] = true
}
done[mf.Filename()] = true
done[mf.FileName()] = true
related.files = files
jobs <- ImportJob{
filename: filename,
fileName: fileName,
related: related,
indexOpt: indexOpt,
importOpt: opt,
@ -167,9 +167,9 @@ func (imp *Import) Start(opt ImportOptions) {
for _, directory := range directories {
if fs.IsEmpty(directory) {
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 {
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()
dateCreated := mainFile.DateCreated()
if !mediaFile.IsSidecar() {
if f, err := entity.FindFileByHash(imp.conf.Db(), mediaFile.Hash()); err == nil {
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())
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
pathName := imp.originalsPath() + string(os.PathSeparator) + dateCreated.UTC().Format("2006/01")
pathName := imp.originalsPath() + string(os.PathSeparator) + dateCreated.Format("2006/01")
iteration := 0

View file

@ -46,7 +46,7 @@ func TestImport_DestinationFilename(t *testing.T) {
// 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) {

View file

@ -9,11 +9,10 @@ import (
)
type ImportJob struct {
filename string
fileName string
related RelatedFiles
indexOpt IndexOptions
importOpt ImportOptions
path string
imp *Import
}
@ -26,20 +25,27 @@ func importWorker(jobs <-chan ImportJob) {
indexOpt := job.indexOpt
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{
"fileName": related.main.Filename(),
"baseName": filepath.Base(related.main.Filename()),
"fileName": originalName,
"baseName": filepath.Base(related.main.FileName()),
})
for _, f := range related.files {
relativeFilename := f.RelativeFilename(importPath)
relativeFilename := f.RelativeName(importPath)
if destinationFilename, err := imp.DestinationFilename(related.main, f); err == nil {
if err := os.MkdirAll(path.Dir(destinationFilename), os.ModePerm); err != nil {
log.Errorf("import: could not create directories (%s)", err.Error())
}
if related.main.HasSameFilename(f) {
if related.main.HasSameName(f) {
destinationMainFilename = destinationFilename
log.Infof("import: moving main %s file \"%s\" to \"%s\"", f.Type(), relativeFilename, destinationFilename)
} else {
@ -48,18 +54,18 @@ func importWorker(jobs <-chan ImportJob) {
if opt.Move {
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 {
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 {
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 {
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
if related.main != nil {
res := ind.MediaFile(related.main, indexOpt)
log.Infof("import: %s main %s file \"%s\"", res, related.main.Type(), related.main.RelativeFilename(ind.originalsPath()))
done[related.main.Filename()] = true
res := ind.MediaFile(related.main, indexOpt, originalName)
log.Infof("import: %s main %s file \"%s\"", res, related.main.Type(), related.main.RelativeName(ind.originalsPath()))
done[related.main.FileName()] = true
} else {
log.Warnf("import: no main file for %s (conversion to jpeg failed?)", destinationMainFilename)
}
@ -111,14 +117,14 @@ func importWorker(jobs <-chan ImportJob) {
continue
}
if done[f.Filename()] {
if done[f.FileName()] {
continue
}
res := ind.MediaFile(f, indexOpt)
done[f.Filename()] = true
res := ind.MediaFile(f, indexOpt, "")
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() {
if err := recover(); err != nil {
log.Errorf("index: %s [panic]", err)
@ -97,15 +97,15 @@ func (ind *Index) Start(options IndexOptions) map[string]bool {
return errors.New("indexing canceled")
}
if err != nil || done[filename] {
if err != nil || done[fileName] {
return nil
}
if fileInfo.IsDir() || strings.HasPrefix(filepath.Base(filename), ".") {
if fileInfo.IsDir() || strings.HasPrefix(filepath.Base(fileName), ".") {
return nil
}
mf, err := NewMediaFile(filename)
mf, err := NewMediaFile(fileName)
if err != nil || !mf.IsPhoto() {
return nil
@ -122,20 +122,20 @@ func (ind *Index) Start(options IndexOptions) map[string]bool {
var files MediaFiles
for _, f := range related.files {
if done[f.Filename()] {
if done[f.FileName()] {
continue
}
files = append(files, f)
done[f.Filename()] = true
done[f.FileName()] = true
}
done[mf.Filename()] = true
done[mf.FileName()] = true
related.files = files
jobs <- IndexJob{
filename: mf.Filename(),
filename: mf.FileName(),
related: related,
opt: options,
ind: ind,

View file

@ -24,7 +24,7 @@ const (
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 {
log.Error("index: media file is nil - you might have found a bug")
return indexResultFailed
@ -39,23 +39,31 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult {
var keywords []string
labels := classify.Labels{}
fileBase := m.Basename()
fileBase := m.Base()
filePath := m.RelativePath(ind.originalsPath())
fileName := m.RelativeFilename(ind.originalsPath())
fileHash := m.Hash()
fileName := m.RelativeName(ind.originalsPath())
fileHash := ""
fileSize, fileModified := m.Stat()
fileChanged := true
fileExists := false
photoExists := false
event.Publish("index.indexing", event.Data{
"fileHash": fileHash,
"fileSize": fileSize,
"fileName": 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
if !fileExists && !m.IsSidecar() {
fileHash = m.Hash()
fileQuery = ind.db.Unscoped().First(&file, "file_hash = ?", fileHash)
fileExists = fileQuery.Error == nil
}
if !fileExists {
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 {
photoQuery = ind.db.Unscoped().First(&photo, "id = ?", file.PhotoID)
fileChanged = file.FileHash != fileHash
fileChanged = file.Changed(fileSize, fileModified)
}
photoExists = photoQuery.Error == nil
@ -74,6 +83,15 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult {
return indexResultSkipped
}
if fileHash == "" {
fileHash = m.Hash()
}
if !photoExists {
photo.PhotoPath = filePath
photo.PhotoName = fileBase
}
if !file.FilePrimary {
if photoExists {
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() {
// 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 {
photo.PhotoTitle = data.Title
}
@ -211,6 +229,10 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult {
ind.addLabels(photo.ID, labels)
}
if originalName != "" {
file.OriginalName = originalName
}
file.PhotoID = photo.ID
file.PhotoUUID = photo.PhotoUUID
file.FileSidecar = m.IsSidecar()
@ -218,6 +240,8 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult {
file.FileMissing = false
file.FileName = fileName
file.FileHash = fileHash
file.FileSize = fileSize
file.FileModified = fileModified
file.FileType = string(m.Type())
file.FileMime = m.MimeType()
file.FileOrientation = m.Orientation()
@ -248,6 +272,11 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult {
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, labels.Keywords()...)
photo.IndexKeywords(keywords, ind.db)
@ -292,7 +321,7 @@ func (ind *Index) isNSFW(jpeg *MediaFile) bool {
return false
} else {
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
}
}
@ -459,7 +488,7 @@ func (ind *Index) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label
}
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 {
log.Infof("index: new photo title is \"%s\"", photo.PhotoTitle)
}

View file

@ -15,23 +15,23 @@ func indexWorker(jobs <-chan IndexJob) {
ind := job.ind
if related.main != nil {
res := ind.MediaFile(related.main, opt)
done[related.main.Filename()] = true
res := ind.MediaFile(related.main, opt, "")
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 {
log.Warnf("index: no main file for %s (conversion to jpeg failed?)", job.filename)
}
for _, f := range related.files {
if done[f.Filename()] {
if done[f.FileName()] {
continue
}
res := ind.MediaFile(f, opt)
done[f.Filename()] = true
res := ind.MediaFile(f, opt, "")
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,13 +23,12 @@ import (
// MediaFile represents a single photo, video or sidecar file.
type MediaFile struct {
filename string
dateCreated time.Time
timeZone string
hash string
fileName string
fileType fs.Type
mimeType string
perceptualHash string
dateCreated time.Time
hash string
checksum string
width int
height int
once sync.Once
@ -37,39 +36,51 @@ type MediaFile struct {
location *entity.Location
}
// NewMediaFile returns a new MediaFile.
func NewMediaFile(filename string) (*MediaFile, error) {
if !fs.FileExists(filename) {
return nil, fmt.Errorf("file does not exist: %s", filename)
// NewMediaFile returns a new media file.
func NewMediaFile(fileName string) (*MediaFile, error) {
if !fs.FileExists(fileName) {
return nil, fmt.Errorf("file does not exist: %s", fileName)
}
instance := &MediaFile{
filename: filename,
fileName: fileName,
fileType: fs.TypeOther,
}
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 {
if !m.dateCreated.IsZero() {
return m.dateCreated
}
m.dateCreated = time.Now()
m.dateCreated = time.Now().UTC()
info, err := m.MetaData()
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())
return m.dateCreated
}
t, err := times.Stat(m.Filename())
t, err := times.Stat(m.FileName())
if err != nil {
log.Debug(err.Error())
@ -78,9 +89,9 @@ func (m *MediaFile) DateCreated() time.Time {
}
if t.HasBirthTime() {
m.dateCreated = t.BirthTime()
m.dateCreated = t.BirthTime().UTC()
} else {
m.dateCreated = t.ModTime()
m.dateCreated = t.ModTime().UTC()
}
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.
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.
func (m *MediaFile) CanonicalNameFromFile() string {
basename := filepath.Base(m.Filename())
basename := filepath.Base(m.FileName())
if end := strings.Index(basename, "."); end != -1 {
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()
}
// 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 {
if len(m.hash) == 0 {
m.hash = fs.Hash(m.Filename())
m.hash = fs.Hash(m.FileName())
}
return m.hash
}
// EditedFilename When editing photos, iPhones create additional files like IMG_E12345.JPG
func (m *MediaFile) EditedFilename() string {
basename := filepath.Base(m.filename)
// Checksum returns the CRC32 checksum of a media file.
func (m *MediaFile) Checksum() string {
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 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
}
}
@ -250,7 +270,7 @@ func (m *MediaFile) EditedFilename() string {
// RelatedFiles returns files which are related to this file.
func (m *MediaFile) RelatedFiles() (result RelatedFiles, err error) {
baseFilename := m.DirectoryBasename()
baseFilename := m.AbsBase()
// escape any meta characters in the file name
baseFilename = regexp.QuoteMeta(baseFilename)
matches, err := filepath.Glob(baseFilename + "*")
@ -259,7 +279,7 @@ func (m *MediaFile) RelatedFiles() (result RelatedFiles, err error) {
return result, err
}
if filename := m.EditedFilename(); filename != "" {
if filename := m.EditedName(); filename != "" {
matches = append(matches, filename)
}
@ -276,7 +296,7 @@ func (m *MediaFile) RelatedFiles() (result RelatedFiles, err error) {
result.main = resultFile
} else if resultFile.IsHEIF() {
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
} else if resultFile.IsImageOther() {
result.main = resultFile
@ -290,34 +310,34 @@ func (m *MediaFile) RelatedFiles() (result RelatedFiles, err error) {
return result, nil
}
// Filename returns the filename.
func (m MediaFile) Filename() string {
return m.filename
// FileName returns the filename.
func (m MediaFile) FileName() string {
return m.fileName
}
// SetFilename sets the filename to the given string.
func (m *MediaFile) SetFilename(filename string) {
m.filename = filename
// SetFileName sets the filename to the given string.
func (m *MediaFile) SetFileName(fileName string) {
m.fileName = fileName
}
// RelativeFilename returns the relative filename.
func (m MediaFile) RelativeFilename(directory string) string {
if index := strings.Index(m.filename, directory); index == 0 {
// RelativeName returns the relative filename.
func (m MediaFile) RelativeName(directory string) string {
if index := strings.Index(m.fileName, directory); index == 0 {
if index := strings.LastIndex(directory, string(os.PathSeparator)); index == len(directory)-1 {
pos := len(directory)
return m.filename[pos:]
return m.fileName[pos:]
} else if index := strings.LastIndex(directory, string(os.PathSeparator)); index != len(directory) {
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.
func (m MediaFile) RelativePath(directory string) string {
pathname := m.filename
pathname := m.fileName
if i := strings.Index(pathname, directory); i == 0 {
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
}
// RelativeBasename returns the relative filename.
func (m MediaFile) RelativeBasename(directory string) string {
// RelativeBase returns the relative filename.
func (m MediaFile) RelativeBase(directory string) string {
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
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.
func (m MediaFile) Basename() string {
basename := filepath.Base(m.Filename())
// Base returns the filename base without any extensions and path.
func (m MediaFile) Base() string {
basename := filepath.Base(m.FileName())
if end := strings.Index(basename, "."); end != -1 {
// ignore everything behind the first dot in the file name
@ -370,9 +390,9 @@ func (m MediaFile) Basename() string {
return basename
}
// DirectoryBasename returns the directory and base filename without any extensions.
func (m MediaFile) DirectoryBasename() string {
return m.Directory() + string(os.PathSeparator) + m.Basename()
// AbsBase returns the directory and base filename without any extensions.
func (m MediaFile) AbsBase() string {
return m.Directory() + string(os.PathSeparator) + m.Base()
}
// MimeType returns the mime type.
@ -381,13 +401,13 @@ func (m *MediaFile) MimeType() string {
return m.mimeType
}
m.mimeType = fs.MimeType(m.Filename())
m.mimeType = fs.MimeType(m.FileName())
return m.mimeType
}
func (m *MediaFile) openFile() (*os.File, error) {
handle, err := os.Open(m.filename)
handle, err := os.Open(m.fileName)
if err != nil {
log.Error(err.Error())
return nil, err
@ -397,26 +417,30 @@ func (m *MediaFile) openFile() (*os.File, error) {
// Exists checks if a media file exists by filename.
func (m MediaFile) Exists() bool {
return fs.FileExists(m.Filename())
return fs.FileExists(m.FileName())
}
// Remove a media file.
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.
func (m MediaFile) HasSameFilename(other *MediaFile) bool {
return m.Filename() == other.Filename()
func (m MediaFile) HasSameName(f *MediaFile) bool {
if f == nil {
return false
}
return m.FileName() == f.FileName()
}
// Move file to a new destination with the filename provided in parameter.
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())
} else {
m.filename = newFilename
m.fileName = newFilename
return nil
}
@ -425,11 +449,11 @@ func (m *MediaFile) Move(newFilename string) error {
return err
}
if err := os.Remove(m.filename); err != nil {
if err := os.Remove(m.fileName); err != nil {
return err
}
m.filename = newFilename
m.fileName = newFilename
return nil
}
@ -466,7 +490,7 @@ func (m *MediaFile) Copy(destinationFilename string) error {
// Extension returns the filename extension of this media file.
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.
@ -545,6 +569,8 @@ func (m MediaFile) IsSidecar() bool {
return true
case fs.TypeYaml:
return true
case fs.TypeJson:
return true
case fs.TypeText:
return true
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).
func (m *MediaFile) Jpeg() (*MediaFile, error) {
if m.IsJpeg() {
if !fs.FileExists(m.Filename()) {
return nil, fmt.Errorf("jpeg file should exist, but does not: %s", m.Filename())
if !fs.FileExists(m.FileName()) {
return nil, fmt.Errorf("jpeg file should exist, but does not: %s", m.FileName())
}
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) {
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 {
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
@ -603,7 +629,7 @@ func (m *MediaFile) decodeDimensions() error {
}
if m.IsJpeg() {
file, err := os.Open(m.Filename())
file, err := os.Open(m.FileName())
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)
}
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 {
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() {
switch count {
case 0: log.Debug(capture.Time(start, fmt.Sprintf("mediafile: no new thumbnails created for %s", m.Basename())))
case 1: log.Debug(capture.Time(start, fmt.Sprintf("mediafile: one thumbnail created for %s", m.Basename())))
default: log.Debug(capture.Time(start, fmt.Sprintf("mediafile: %d thumbnails created for %s", count, m.Basename())))
case 0:
log.Debug(capture.Time(start, fmt.Sprintf("mediafile: no new thumbnails created for %s", m.Base())))
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 {
img, err := imaging.Open(m.Filename(), imaging.AutoOrientation(true))
img, err := imaging.Open(m.FileName(), imaging.AutoOrientation(true))
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
}

View file

@ -201,7 +201,7 @@ func TestMediaFileCanonicalName(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/beach_wood.jpg")
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) {
@ -236,14 +236,14 @@ func TestMediaFile_EditedFilename(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120.JPG")
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) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/fern_green.jpg")
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)
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()
@ -290,9 +290,9 @@ func TestMediaFile_RelatedFiles(t *testing.T) {
assert.Len(t, related.files, 3)
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()
@ -316,12 +316,12 @@ func TestMediaFile_RelatedFiles_Ordering(t *testing.T) {
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.JPG", related.files[1].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())
for _, result := range related.files {
filename := result.Filename()
t.Logf("Filename: %s", filename)
filename := result.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")
assert.Nil(t, err)
mediaFile.SetFilename("newFilename")
assert.Equal(t, "newFilename", mediaFile.filename)
mediaFile.SetFilename("turtle_brown_blue")
assert.Equal(t, "turtle_brown_blue", mediaFile.filename)
mediaFile.SetFileName("newFilename")
assert.Equal(t, "newFilename", mediaFile.fileName)
mediaFile.SetFileName("turtle_brown_blue")
assert.Equal(t, "turtle_brown_blue", mediaFile.fileName)
}
func TestMediaFile_RelativeFilename(t *testing.T) {
@ -343,20 +343,20 @@ func TestMediaFile_RelativeFilename(t *testing.T) {
assert.Nil(t, err)
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)
})
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)
})
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)
})
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)
})
}
@ -393,15 +393,15 @@ func TestMediaFile_RelativeBasename(t *testing.T) {
assert.Nil(t, err)
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)
})
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)
})
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)
})
@ -423,21 +423,21 @@ func TestMediaFile_Basename(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/limes.jpg")
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) {
conf := config.TestConfig()
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120 copy.JPG")
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) {
conf := config.TestConfig()
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120 (1).JPG")
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.Equal(t, destName, m.Filename())
assert.Equal(t, destName, m.FileName())
}
func TestMediaFile_Copy(t *testing.T) {
@ -892,7 +892,7 @@ func TestMediaFile_Jpeg(t *testing.T) {
assert.Nil(t, err)
file, err := mediaFile.Jpeg()
assert.Nil(t, err)
assert.FileExists(t, file.filename)
assert.FileExists(t, file.fileName)
})
t.Run("/iphone_7.json", func(t *testing.T) {
conf := config.TestConfig()

View file

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

View file

@ -6,6 +6,6 @@ import (
// MetaData returns exif meta data of a media file.
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
}

View file

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

View file

@ -3,11 +3,12 @@ package fs
import (
"crypto/sha1"
"encoding/hex"
"hash/crc32"
"io"
"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 {
var result []byte
@ -27,3 +28,24 @@ func Hash(filename string) string {
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)
})
}
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"
// YAML metadata / config / sidecar file.
TypeYaml Type = "yml"
// JSON metadata / config / sidecar file.
TypeJson Type = "json"
// Text config / sidecar file.
TypeText Type = "txt"
// Markdown text sidecar file.
@ -106,4 +108,5 @@ var Ext = map[string]Type{
".xml": TypeXML,
".txt": TypeText,
".md": TypeMarkdown,
".json": TypeJson,
}