Unstack all types, except primary and sidecar files #394
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
0cb609fc87
commit
8989c987a2
|
@ -41,13 +41,13 @@
|
|||
</v-img>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="file.Type === 'jpg' && !file.Primary">
|
||||
<tr v-if="!file.Sidecar && !file.Primary && !file.Root">
|
||||
<td>
|
||||
<translate>Actions</translate>
|
||||
</td>
|
||||
<td>
|
||||
<v-btn small depressed dark color="secondary-dark" class="ma-0 action-primary"
|
||||
@click.stop.prevent="primary(file)">
|
||||
@click.stop.prevent="primary(file)" v-if="file.Type === 'jpg' && !file.Primary">
|
||||
<translate>Primary</translate>
|
||||
</v-btn>
|
||||
<v-btn small depressed dark color="secondary-dark" class="ma-0 action-unstack"
|
||||
|
|
|
@ -28,7 +28,7 @@ func SavePhotoAsYaml(p entity.Photo) {
|
|||
if err := p.SaveAsYaml(yamlFile); err != nil {
|
||||
log.Errorf("photo: %s (update yaml)", err)
|
||||
} else {
|
||||
log.Infof("photo: updated yaml file %s", txt.Quote(fs.Rel(yamlFile, conf.OriginalsPath())))
|
||||
log.Infof("photo: updated yaml file %s", txt.Quote(fs.RelName(yamlFile, conf.OriginalsPath())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -297,7 +297,7 @@ func DislikePhoto(router *gin.RouterGroup) {
|
|||
// Parameters:
|
||||
// uid: string PhotoUID as returned by the API
|
||||
// file_uid: string File UID as returned by the API
|
||||
func PhotoFilePrimary(router *gin.RouterGroup) {
|
||||
func PhotoPrimary(router *gin.RouterGroup) {
|
||||
router.POST("/photos/:uid/files/:file_uid/primary", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
|
||||
|
||||
|
|
|
@ -112,10 +112,10 @@ func TestDislikePhoto(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestSetPhotoPrimary(t *testing.T) {
|
||||
func TestPhotoPrimary(t *testing.T) {
|
||||
t.Run("existing photo", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
PhotoFilePrimary(router)
|
||||
PhotoPrimary(router)
|
||||
r := PerformRequest(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh8/files/ft1es39w45bnlqdw/primary")
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
GetFile(router)
|
||||
|
@ -129,7 +129,7 @@ func TestSetPhotoPrimary(t *testing.T) {
|
|||
|
||||
t.Run("wrong photo uid", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
PhotoFilePrimary(router)
|
||||
PhotoPrimary(router)
|
||||
r := PerformRequest(app, "POST", "/api/v1/photos/xxx/files/ft1es39w45bnlqdw/primary")
|
||||
val := gjson.Get(r.Body.String(), "error")
|
||||
assert.Equal(t, i18n.Msg(i18n.ErrEntityNotFound), val.String())
|
||||
|
|
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
|
@ -11,6 +12,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// POST /api/v1/photos/:uid/files/:file_uid/unstack
|
||||
|
@ -18,7 +20,7 @@ import (
|
|||
// Parameters:
|
||||
// uid: string Photo UID as returned by the API
|
||||
// file_uid: string File UID as returned by the API
|
||||
func PhotoFileUnstack(router *gin.RouterGroup) {
|
||||
func PhotoUnstack(router *gin.RouterGroup) {
|
||||
router.POST("/photos/:uid/files/:file_uid/unstack", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
|
||||
|
||||
|
@ -27,6 +29,8 @@ func PhotoFileUnstack(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
conf := service.Config()
|
||||
|
||||
photoUID := c.Param("uid")
|
||||
fileUID := c.Param("file_uid")
|
||||
|
||||
|
@ -42,13 +46,68 @@ func PhotoFileUnstack(router *gin.RouterGroup) {
|
|||
log.Errorf("photo: can't unstack primary file")
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
} else if file.FileSidecar {
|
||||
log.Errorf("photo: can't unstack sidecar files")
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
} else if file.FileRoot != entity.RootOriginals {
|
||||
log.Errorf("photo: only originals can be unstacked")
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := photoprism.FileName(file.FileRoot, file.FileName)
|
||||
baseName := filepath.Base(fileName)
|
||||
|
||||
mediaFile, err := photoprism.NewMediaFile(fileName)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("photo: %s (unstack %s)", err, txt.Quote(baseName))
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
related, err := mediaFile.RelatedFiles(true)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("photo: %s (unstack %s)", err, txt.Quote(baseName))
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
} else if related.Len() == 0 {
|
||||
log.Errorf("photo: no related files found (unstack %s)", txt.Quote(baseName))
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
if related.Len() > 1 {
|
||||
if conf.ReadOnly() {
|
||||
log.Errorf("photo: can't rename files in read only mode (unstack %s)", txt.Quote(baseName))
|
||||
AbortFeatureDisabled(c)
|
||||
return
|
||||
}
|
||||
|
||||
newName := mediaFile.AbsPrefix(true) + "_" + mediaFile.Checksum() + mediaFile.Extension()
|
||||
|
||||
if err := mediaFile.Move(newName); err != nil {
|
||||
log.Errorf("photo: can't rename %s to %s (unstack)", txt.Quote(baseName), txt.Quote(filepath.Base(newName)))
|
||||
AbortUnexpected(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
oldPhoto := *file.Photo
|
||||
oldPrimary, err := oldPhoto.PrimaryFile()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("photo: can't find primary file for existing photo (unstack %s)", txt.Quote(baseName))
|
||||
AbortUnexpected(c)
|
||||
return
|
||||
}
|
||||
|
||||
existingPhoto := *file.Photo
|
||||
newPhoto := entity.NewPhoto()
|
||||
|
||||
if err := newPhoto.Create(); err != nil {
|
||||
log.Errorf("photo: %s (unstack)", err.Error())
|
||||
log.Errorf("photo: %s (unstack %s)", err.Error(), txt.Quote(baseName))
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
@ -56,29 +115,46 @@ func PhotoFileUnstack(router *gin.RouterGroup) {
|
|||
file.Photo = &newPhoto
|
||||
file.PhotoID = newPhoto.ID
|
||||
file.PhotoUID = newPhoto.PhotoUID
|
||||
file.FileName = mediaFile.RelName(conf.OriginalsPath())
|
||||
|
||||
if err := file.Save(); err != nil {
|
||||
log.Errorf("photo: %s (unstack)", err.Error())
|
||||
log.Errorf("photo: %s (unstack %s)", err.Error(), txt.Quote(baseName))
|
||||
|
||||
if err := newPhoto.Delete(true); err != nil {
|
||||
log.Errorf("photo: %s (unstack %s)", err.Error(), txt.Quote(baseName))
|
||||
}
|
||||
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := photoprism.FileName(file.FileRoot, file.FileName)
|
||||
ind := service.Index()
|
||||
|
||||
f, err := photoprism.NewMediaFile(fileName)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("photo: %s (unstack)", err)
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := service.Index().MediaFile(f, photoprism.IndexOptions{Rescan: true}, existingPhoto.OriginalName).Error; err != nil {
|
||||
log.Errorf("photo: %s (unstack)", err)
|
||||
// Index new, unstacked file.
|
||||
if res := ind.File(mediaFile.FileName()); res.Failed() {
|
||||
log.Errorf("photo: %s (unstack %s)", res.Err, txt.Quote(baseName))
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Reset type for old, existing photo to image.
|
||||
if err := oldPhoto.Update("PhotoType", entity.TypeImage); err != nil {
|
||||
log.Errorf("photo: %s (unstack %s)", err, txt.Quote(baseName))
|
||||
AbortUnexpected(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Get name of old, existing primary file.
|
||||
oldPrimaryName := photoprism.FileName(oldPrimary.FileRoot, oldPrimary.FileName)
|
||||
|
||||
// Re-index old, existing primary file.
|
||||
if res := ind.File(oldPrimaryName); res.Failed() {
|
||||
log.Errorf("photo: %s (unstack %s)", res.Err, txt.Quote(baseName))
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Notify clients by publishing events.
|
||||
PublishPhotoEvent(EntityCreated, file.PhotoUID, c)
|
||||
PublishPhotoEvent(EntityUpdated, photoUID, c)
|
||||
|
||||
|
|
28
internal/api/photo_unstack_test.go
Normal file
28
internal/api/photo_unstack_test.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPhotoUnstack(t *testing.T) {
|
||||
t.Run("unstack xmp sidecar file", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
PhotoUnstack(router)
|
||||
r := PerformRequest(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/files/ft1es39w45bnlqdw/unstack")
|
||||
// Sidecar files can not be unstacked.
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
// t.Logf("RESP: %s", r.Body.String())
|
||||
})
|
||||
|
||||
t.Run("unstack bridge3.jpg", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
PhotoUnstack(router)
|
||||
r := PerformRequest(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/files/ft2es49whhbnlqdn/unstack")
|
||||
// TODO: Have a real file in place for testing the success case. This file does not exist, so it can't be unstacked.
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
// t.Logf("RESP: %s", r.Body.String())
|
||||
})
|
||||
}
|
|
@ -54,7 +54,7 @@ func purgeAction(ctx *cli.Context) error {
|
|||
if subPath == "" {
|
||||
log.Infof("removing missing files in %s", txt.Quote(filepath.Base(conf.OriginalsPath())))
|
||||
} else {
|
||||
log.Infof("removing missing files in %s", txt.Quote(fs.Rel(filepath.Join(conf.OriginalsPath(), subPath), filepath.Dir(conf.OriginalsPath()))))
|
||||
log.Infof("removing missing files in %s", txt.Quote(fs.RelName(filepath.Join(conf.OriginalsPath(), subPath), filepath.Dir(conf.OriginalsPath()))))
|
||||
}
|
||||
|
||||
if conf.ReadOnly() {
|
||||
|
|
|
@ -78,6 +78,15 @@ func FirstFileByHash(fileHash string) (File, error) {
|
|||
return file, q.Error
|
||||
}
|
||||
|
||||
// PrimaryFile returns the primary file for a photo uid.
|
||||
func PrimaryFile(photoUID string) (File, error) {
|
||||
var file File
|
||||
|
||||
q := Db().Unscoped().First(&file, "file_primary = 1 AND photo_uid = ?", photoUID)
|
||||
|
||||
return file, q.Error
|
||||
}
|
||||
|
||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||
func (m *File) BeforeCreate(scope *gorm.Scope) error {
|
||||
if rnd.IsUID(m.FileUID, 'f') {
|
||||
|
|
|
@ -902,6 +902,7 @@ func (m *Photo) Delete(permanently bool) error {
|
|||
// Delete permanently deletes the entity from the database.
|
||||
func (m *Photo) DeletePermanently() error {
|
||||
Db().Unscoped().Delete(File{}, "photo_id = ?", m.ID)
|
||||
Db().Unscoped().Delete(Details{}, "photo_id = ?", m.ID)
|
||||
Db().Unscoped().Delete(PhotoKeyword{}, "photo_id = ?", m.ID)
|
||||
Db().Unscoped().Delete(PhotoLabel{}, "photo_id = ?", m.ID)
|
||||
Db().Unscoped().Delete(PhotoAlbum{}, "photo_uid = ?", m.PhotoUID)
|
||||
|
@ -980,3 +981,8 @@ func (m *Photo) Approve() error {
|
|||
func (m *Photo) Links() Links {
|
||||
return FindLinks("", m.PhotoUID)
|
||||
}
|
||||
|
||||
// PrimaryFile returns the primary file for this photo.
|
||||
func (m *Photo) PrimaryFile() (File, error) {
|
||||
return PrimaryFile(m.PhotoUID)
|
||||
}
|
||||
|
|
|
@ -59,10 +59,12 @@ func (data *Data) Exif(fileName string) (err error) {
|
|||
_, rawExif, err = sl.Exif()
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "no exif header" {
|
||||
if strings.HasPrefix(err.Error(), "no exif header") {
|
||||
return fmt.Errorf("metadata: no exif header in %s", logName)
|
||||
} else if strings.HasPrefix(err.Error(), "no exif data") {
|
||||
log.Debugf("metadata: failed parsing %s, starting brute-force search (exif)", logName)
|
||||
} else {
|
||||
log.Warnf("metadata: %s in %s (parse jpeg)", err, logName)
|
||||
log.Warnf("metadata: %s in %s, starting brute-force search (exif)", err, logName)
|
||||
}
|
||||
} else {
|
||||
parsed = true
|
||||
|
@ -133,7 +135,7 @@ func (data *Data) Exif(fileName string) (err error) {
|
|||
valueString, err = ite.FormatFirst()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("metadata: %s in %s (exif)", err, logName)
|
||||
log.Errorf("metadata: %s in %s (find exif tags)", err, logName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -292,7 +294,8 @@ func (data *Data) Exif(fileName string) (err error) {
|
|||
data.Lng = float32(gi.Longitude.Decimal())
|
||||
data.Altitude = gi.Altitude
|
||||
} else {
|
||||
log.Warnf("metadata: %s in %s (exif)", err, logName)
|
||||
log.Debugf("exif: %s in %s", err, logName)
|
||||
log.Warnf("metadata: failed parsing gps coordinates in %s (exif)", logName)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -332,7 +335,7 @@ func (data *Data) Exif(fileName string) (err error) {
|
|||
} else if tl, err := time.ParseInLocation("2006:01:02 15:04:05", takenAt, loc); err == nil {
|
||||
data.TakenAt = tl.Round(time.Second).UTC()
|
||||
} else {
|
||||
log.Errorf("metadata: %s in %s (exif)", err.Error(), logName) // this should never happen
|
||||
log.Errorf("metadata: %s in %s (exif time)", err.Error(), logName) // this should never happen
|
||||
}
|
||||
} else {
|
||||
log.Warnf("metadata: invalid time %s in %s (exif)", takenAt, logName)
|
||||
|
|
|
@ -266,4 +266,25 @@ func TestExif(t *testing.T) {
|
|||
|
||||
assert.EqualError(t, err, "metadata: no exif header in gopher-preview.jpg (search and extract)")
|
||||
})
|
||||
|
||||
t.Run("huawei-gps-error.jpg", func(t *testing.T) {
|
||||
data, err := Exif("testdata/huawei-gps-error.jpg")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "2020-06-16T18:52:46Z", data.TakenAt.Format("2006-01-02T15:04:05Z"))
|
||||
assert.Equal(t, "2020-06-16T18:52:46Z", data.TakenAtLocal.Format("2006-01-02T15:04:05Z"))
|
||||
assert.Equal(t, float32(0), data.Lat)
|
||||
assert.Equal(t, float32(0), data.Lng)
|
||||
assert.Equal(t, 0, data.Altitude)
|
||||
assert.Equal(t, "1/110", data.Exposure)
|
||||
assert.Equal(t, "HUAWEI", data.CameraMake)
|
||||
assert.Equal(t, "ELE-L29", data.CameraModel)
|
||||
assert.Equal(t, 27, data.FocalLength)
|
||||
assert.Equal(t, 0, data.Orientation)
|
||||
assert.Equal(t, "", data.LensMake)
|
||||
assert.Equal(t, "", data.LensModel)
|
||||
})
|
||||
}
|
||||
|
|
BIN
internal/meta/testdata/huawei-gps-error.jpg
vendored
Normal file
BIN
internal/meta/testdata/huawei-gps-error.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 267 KiB |
|
@ -60,7 +60,7 @@ func (c *Convert) Start(path string) error {
|
|||
}
|
||||
|
||||
ignore.Log = func(fileName string) {
|
||||
log.Infof(`convert: ignored "%s"`, fs.Rel(fileName, path))
|
||||
log.Infof(`convert: ignored "%s"`, fs.RelName(fileName, path))
|
||||
}
|
||||
|
||||
err := godirwalk.Walk(path, &godirwalk.Options{
|
||||
|
@ -118,12 +118,12 @@ func (c *Convert) ToJson(mf *MediaFile) (*MediaFile, error) {
|
|||
}
|
||||
|
||||
if !c.conf.SidecarWritable() {
|
||||
return nil, fmt.Errorf("convert: metadata export to json disabled in read only mode (%s)", mf.RelativeName(c.conf.OriginalsPath()))
|
||||
return nil, fmt.Errorf("convert: metadata export to json disabled in read only mode (%s)", mf.RelName(c.conf.OriginalsPath()))
|
||||
}
|
||||
|
||||
jsonName = fs.FileName(mf.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), ".json", c.conf.Settings().Index.Sequences)
|
||||
|
||||
fileName := mf.RelativeName(c.conf.OriginalsPath())
|
||||
fileName := mf.RelName(c.conf.OriginalsPath())
|
||||
|
||||
log.Infof("convert: %s -> %s", fileName, filepath.Base(jsonName))
|
||||
|
||||
|
@ -184,7 +184,7 @@ func (c *Convert) JpegConvertCommand(mf *MediaFile, jpegName string, xmpName str
|
|||
|
||||
result = exec.Command(c.conf.DarktableBin(), args...)
|
||||
} else {
|
||||
return nil, useMutex, fmt.Errorf("convert: no raw to jpeg converter installed (%s)", mf.Base(c.conf.Settings().Index.Sequences))
|
||||
return nil, useMutex, fmt.Errorf("convert: no raw to jpeg converter installed (%s)", mf.BasePrefix(c.conf.Settings().Index.Sequences))
|
||||
}
|
||||
} else if mf.IsVideo() {
|
||||
result = exec.Command(c.conf.FFmpegBin(), "-i", mf.FileName(), "-ss", "00:00:00.001", "-vframes", "1", jpegName)
|
||||
|
@ -200,7 +200,7 @@ func (c *Convert) JpegConvertCommand(mf *MediaFile, jpegName string, xmpName str
|
|||
// 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.RelativeName(c.conf.OriginalsPath()))
|
||||
return nil, fmt.Errorf("convert: can not convert to jpeg, file does not exist (%s)", image.RelName(c.conf.OriginalsPath()))
|
||||
}
|
||||
|
||||
if image.IsJpeg() {
|
||||
|
@ -216,11 +216,11 @@ func (c *Convert) ToJpeg(image *MediaFile) (*MediaFile, error) {
|
|||
}
|
||||
|
||||
if !c.conf.SidecarWritable() {
|
||||
return nil, fmt.Errorf("convert: disabled in read only mode (%s)", image.RelativeName(c.conf.OriginalsPath()))
|
||||
return nil, fmt.Errorf("convert: disabled in read only mode (%s)", image.RelName(c.conf.OriginalsPath()))
|
||||
}
|
||||
|
||||
jpegName = fs.FileName(image.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.JpegExt, c.conf.Settings().Index.Sequences)
|
||||
fileName := image.RelativeName(c.conf.OriginalsPath())
|
||||
fileName := image.RelName(c.conf.OriginalsPath())
|
||||
|
||||
log.Infof("convert: %s -> %s", fileName, filepath.Base(jpegName))
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ type ConvertJob struct {
|
|||
func ConvertWorker(jobs <-chan ConvertJob) {
|
||||
for job := range jobs {
|
||||
if _, err := job.convert.ToJpeg(job.image); err != nil {
|
||||
fileName := job.image.RelativeName(job.convert.conf.OriginalsPath())
|
||||
fileName := job.image.RelName(job.convert.conf.OriginalsPath())
|
||||
log.Errorf("convert: could not create jpeg for %s (%s)", fileName, strings.TrimSpace(err.Error()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,7 +92,7 @@ func (imp *Import) Start(opt ImportOptions) map[string]bool {
|
|||
}
|
||||
|
||||
ignore.Log = func(fileName string) {
|
||||
log.Infof(`import: ignored "%s"`, fs.Rel(fileName, importPath))
|
||||
log.Infof(`import: ignored "%s"`, fs.RelName(fileName, importPath))
|
||||
}
|
||||
|
||||
err := godirwalk.Walk(importPath, &godirwalk.Options{
|
||||
|
@ -118,7 +118,7 @@ func (imp *Import) Start(opt ImportOptions) map[string]bool {
|
|||
|
||||
if skip, result := fs.SkipWalk(fileName, isDir, isSymlink, done, ignore); skip {
|
||||
if isDir && result != filepath.SkipDir {
|
||||
folder := entity.NewFolder(entity.RootImport, fs.Rel(fileName, imp.conf.ImportPath()), nil)
|
||||
folder := entity.NewFolder(entity.RootImport, fs.RelName(fileName, imp.conf.ImportPath()), nil)
|
||||
|
||||
if err := folder.Create(); err == nil {
|
||||
log.Infof("import: added folder /%s", folder.Path)
|
||||
|
@ -183,9 +183,9 @@ func (imp *Import) Start(opt ImportOptions) map[string]bool {
|
|||
for _, directory := range directories {
|
||||
if fs.IsEmpty(directory) {
|
||||
if err := os.Remove(directory); err != nil {
|
||||
log.Errorf("import: could not delete empty folder %s (%s)", txt.Quote(fs.Rel(directory, importPath)), err)
|
||||
log.Errorf("import: could not delete empty folder %s (%s)", txt.Quote(fs.RelName(directory, importPath)), err)
|
||||
} else {
|
||||
log.Infof("import: deleted empty folder %s", txt.Quote(fs.Rel(directory, importPath)))
|
||||
log.Infof("import: deleted empty folder %s", txt.Quote(fs.RelName(directory, importPath)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -199,7 +199,7 @@ func (imp *Import) Start(opt ImportOptions) map[string]bool {
|
|||
}
|
||||
|
||||
if err := os.Remove(file); err != nil {
|
||||
log.Errorf("import: could not remove %s (%s)", txt.Quote(fs.Rel(file, importPath)), err.Error())
|
||||
log.Errorf("import: could not remove %s (%s)", txt.Quote(fs.RelName(file, importPath)), err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -250,7 +250,7 @@ func (imp *Import) DestinationFilename(mainFile *MediaFile, mediaFile *MediaFile
|
|||
|
||||
for fs.FileExists(result) {
|
||||
if mediaFile.Hash() == fs.Hash(result) {
|
||||
return result, fmt.Errorf("%s already exists", txt.Quote(fs.Rel(result, imp.originalsPath())))
|
||||
return result, fmt.Errorf("%s already exists", txt.Quote(fs.RelName(result, imp.originalsPath())))
|
||||
}
|
||||
|
||||
iteration++
|
||||
|
|
|
@ -29,11 +29,11 @@ func ImportWorker(jobs <-chan ImportJob) {
|
|||
importPath := job.ImportOpt.Path
|
||||
|
||||
if related.Main == nil {
|
||||
log.Warnf("import: no media file found for %s", txt.Quote(fs.Rel(job.FileName, importPath)))
|
||||
log.Warnf("import: no media file found for %s", txt.Quote(fs.RelName(job.FileName, importPath)))
|
||||
continue
|
||||
}
|
||||
|
||||
originalName := related.Main.RelativeName(importPath)
|
||||
originalName := related.Main.RelName(importPath)
|
||||
|
||||
event.Publish("import.file", event.Data{
|
||||
"fileName": originalName,
|
||||
|
@ -41,7 +41,7 @@ func ImportWorker(jobs <-chan ImportJob) {
|
|||
})
|
||||
|
||||
for _, f := range related.Files {
|
||||
relativeFilename := f.RelativeName(importPath)
|
||||
relativeFilename := f.RelName(importPath)
|
||||
|
||||
if destinationFilename, err := imp.DestinationFilename(related.Main, f); err == nil {
|
||||
if err := os.MkdirAll(path.Dir(destinationFilename), os.ModePerm); err != nil {
|
||||
|
@ -50,18 +50,18 @@ func ImportWorker(jobs <-chan ImportJob) {
|
|||
|
||||
if related.Main.HasSameName(f) {
|
||||
destinationMainFilename = destinationFilename
|
||||
log.Infof("import: moving main %s file %s to %s", f.FileType(), txt.Quote(relativeFilename), txt.Quote(fs.Rel(destinationFilename, imp.originalsPath())))
|
||||
log.Infof("import: moving main %s file %s to %s", f.FileType(), txt.Quote(relativeFilename), txt.Quote(fs.RelName(destinationFilename, imp.originalsPath())))
|
||||
} else {
|
||||
log.Infof("import: moving related %s file %s to %s", f.FileType(), txt.Quote(relativeFilename), txt.Quote(fs.Rel(destinationFilename, imp.originalsPath())))
|
||||
log.Infof("import: moving related %s file %s to %s", f.FileType(), txt.Quote(relativeFilename), txt.Quote(fs.RelName(destinationFilename, imp.originalsPath())))
|
||||
}
|
||||
|
||||
if opt.Move {
|
||||
if err := f.Move(destinationFilename); err != nil {
|
||||
log.Errorf("import: could not move file to %s (%s)", txt.Quote(fs.Rel(destinationMainFilename, imp.originalsPath())), err.Error())
|
||||
log.Errorf("import: could not move file to %s (%s)", txt.Quote(fs.RelName(destinationMainFilename, imp.originalsPath())), err.Error())
|
||||
}
|
||||
} else {
|
||||
if err := f.Copy(destinationFilename); err != nil {
|
||||
log.Errorf("import: could not copy file to %s (%s)", txt.Quote(fs.Rel(destinationMainFilename, imp.originalsPath())), err.Error())
|
||||
log.Errorf("import: could not copy file to %s (%s)", txt.Quote(fs.RelName(destinationMainFilename, imp.originalsPath())), err.Error())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -69,7 +69,7 @@ func ImportWorker(jobs <-chan ImportJob) {
|
|||
|
||||
if opt.RemoveExistingFiles {
|
||||
if err := f.Remove(); err != nil {
|
||||
log.Errorf("import: could not delete %s (%s)", txt.Quote(fs.Rel(f.FileName(), importPath)), err.Error())
|
||||
log.Errorf("import: could not delete %s (%s)", txt.Quote(fs.RelName(f.FileName(), importPath)), err.Error())
|
||||
} else {
|
||||
log.Infof("import: deleted %s (already exists)", txt.Quote(relativeFilename))
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ func ImportWorker(jobs <-chan ImportJob) {
|
|||
f, err := NewMediaFile(destinationMainFilename)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("import: could not import %s (%s)", txt.Quote(fs.Rel(destinationMainFilename, imp.originalsPath())), err.Error())
|
||||
log.Errorf("import: could not import %s (%s)", txt.Quote(fs.RelName(destinationMainFilename, imp.originalsPath())), err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -90,7 +90,7 @@ func ImportWorker(jobs <-chan ImportJob) {
|
|||
log.Errorf("import: creating jpeg failed (%s)", err.Error())
|
||||
continue
|
||||
} else {
|
||||
log.Infof("import: %s created", fs.Rel(jpegFile.FileName(), imp.originalsPath()))
|
||||
log.Infof("import: %s created", fs.RelName(jpegFile.FileName(), imp.originalsPath()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,14 +107,14 @@ func ImportWorker(jobs <-chan ImportJob) {
|
|||
if jsonFile, err := imp.convert.ToJson(f); err != nil {
|
||||
log.Errorf("import: creating json sidecar file failed (%s)", err.Error())
|
||||
} else {
|
||||
log.Infof("import: %s created", fs.Rel(jsonFile.FileName(), imp.originalsPath()))
|
||||
log.Infof("import: %s created", fs.RelName(jsonFile.FileName(), imp.originalsPath()))
|
||||
}
|
||||
}
|
||||
|
||||
related, err := f.RelatedFiles(imp.conf.Settings().Index.Sequences)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("import: could not index %s (%s)", txt.Quote(fs.Rel(destinationMainFilename, imp.originalsPath())), err.Error())
|
||||
log.Errorf("import: could not index %s (%s)", txt.Quote(fs.RelName(destinationMainFilename, imp.originalsPath())), err.Error())
|
||||
|
||||
continue
|
||||
}
|
||||
|
@ -131,7 +131,7 @@ func ImportWorker(jobs <-chan ImportJob) {
|
|||
|
||||
res := ind.MediaFile(related.Main, indexOpt, originalName)
|
||||
|
||||
log.Infof("import: %s main %s file %s", res, related.Main.FileType(), txt.Quote(related.Main.RelativeName(ind.originalsPath())))
|
||||
log.Infof("import: %s main %s file %s", res, related.Main.FileType(), txt.Quote(related.Main.RelName(ind.originalsPath())))
|
||||
done[related.Main.FileName()] = true
|
||||
|
||||
if res.Success() {
|
||||
|
@ -142,7 +142,7 @@ func ImportWorker(jobs <-chan ImportJob) {
|
|||
continue
|
||||
}
|
||||
} else {
|
||||
log.Warnf("import: no main file for %s (conversion to jpeg failed?)", fs.Rel(destinationMainFilename, imp.originalsPath()))
|
||||
log.Warnf("import: no main file for %s (conversion to jpeg failed?)", fs.RelName(destinationMainFilename, imp.originalsPath()))
|
||||
}
|
||||
|
||||
for _, f := range related.Files {
|
||||
|
@ -157,7 +157,7 @@ func ImportWorker(jobs <-chan ImportJob) {
|
|||
res := ind.MediaFile(f, indexOpt, "")
|
||||
done[f.FileName()] = true
|
||||
|
||||
log.Infof("import: %s related %s file %s", res, f.FileType(), txt.Quote(f.RelativeName(ind.originalsPath())))
|
||||
log.Infof("import: %s related %s file %s", res, f.FileType(), txt.Quote(f.RelName(ind.originalsPath())))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@ func (ind *Index) Start(opt IndexOptions) map[string]bool {
|
|||
}
|
||||
|
||||
ignore.Log = func(fileName string) {
|
||||
log.Infof(`index: ignored "%s"`, fs.Rel(fileName, originalsPath))
|
||||
log.Infof(`index: ignored "%s"`, fs.RelName(fileName, originalsPath))
|
||||
}
|
||||
|
||||
err := godirwalk.Walk(optionsPath, &godirwalk.Options{
|
||||
|
@ -122,7 +122,7 @@ func (ind *Index) Start(opt IndexOptions) map[string]bool {
|
|||
|
||||
if skip, result := fs.SkipWalk(fileName, isDir, isSymlink, done, ignore); skip {
|
||||
if isDir && result != filepath.SkipDir {
|
||||
folder := entity.NewFolder(entity.RootOriginals, fs.Rel(fileName, originalsPath), nil)
|
||||
folder := entity.NewFolder(entity.RootOriginals, fs.RelName(fileName, originalsPath), nil)
|
||||
|
||||
if err := folder.Create(); err == nil {
|
||||
log.Infof("index: added folder /%s", folder.Path)
|
||||
|
@ -191,3 +191,26 @@ func (ind *Index) Start(opt IndexOptions) map[string]bool {
|
|||
|
||||
return done
|
||||
}
|
||||
|
||||
// File indexes a single file and returns the result.
|
||||
func (ind *Index) File(name string) (result IndexResult) {
|
||||
file, err := NewMediaFile(name)
|
||||
|
||||
if err != nil {
|
||||
result.Err = err
|
||||
result.Status = IndexFailed
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
related, err := file.RelatedFiles(false)
|
||||
|
||||
if err != nil {
|
||||
result.Err = err
|
||||
result.Status = IndexFailed
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
return IndexRelated(related, ind, IndexOptionsAll())
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ type IndexStatus string
|
|||
|
||||
type IndexResult struct {
|
||||
Status IndexStatus
|
||||
Error error
|
||||
Err error
|
||||
FileID uint
|
||||
FileUID string
|
||||
PhotoID uint
|
||||
|
@ -44,15 +44,19 @@ func (r IndexResult) String() string {
|
|||
return string(r.Status)
|
||||
}
|
||||
|
||||
func (r IndexResult) Failed() bool {
|
||||
return r.Err != nil
|
||||
}
|
||||
|
||||
func (r IndexResult) Success() bool {
|
||||
return r.Error == nil && r.FileID > 0
|
||||
return r.Err == nil && r.FileID > 0
|
||||
}
|
||||
|
||||
func (r IndexResult) Indexed() bool {
|
||||
return r.Status == IndexAdded || r.Status == IndexUpdated || r.Status == IndexStacked
|
||||
}
|
||||
|
||||
func (r IndexResult) Grouped() bool {
|
||||
func (r IndexResult) Stacked() bool {
|
||||
return r.Status == IndexStacked
|
||||
}
|
||||
|
||||
|
@ -60,7 +64,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
if m == nil {
|
||||
err := errors.New("index: media file is nil - you might have found a bug")
|
||||
log.Error(err)
|
||||
result.Error = err
|
||||
result.Err = err
|
||||
result.Status = IndexFailed
|
||||
return result
|
||||
}
|
||||
|
@ -162,10 +166,10 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
if err := photo.LoadFromYaml(yamlName); err != nil {
|
||||
log.Errorf("index: %s (restore from yaml) for %s", err.Error(), logName)
|
||||
} else if err := photo.Find(); err != nil {
|
||||
log.Infof("index: data restored from %s", txt.Quote(fs.Rel(yamlName, Config().OriginalsPath())))
|
||||
log.Infof("index: data restored from %s", txt.Quote(fs.RelName(yamlName, Config().OriginalsPath())))
|
||||
} else {
|
||||
photoExists = true
|
||||
log.Infof("index: uid %s restored from %s", photo.PhotoUID, txt.Quote(fs.Rel(yamlName, Config().OriginalsPath())))
|
||||
log.Infof("index: uid %s restored from %s", photo.PhotoUID, txt.Quote(fs.RelName(yamlName, Config().OriginalsPath())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -191,7 +195,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
file.OriginalName = originalName
|
||||
|
||||
if file.FilePrimary && photo.OriginalName == "" {
|
||||
photo.OriginalName = fs.Base(originalName, stripSequence)
|
||||
photo.OriginalName = fs.BasePrefix(originalName, stripSequence)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -505,14 +509,14 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
if err := photo.Save(); err != nil {
|
||||
log.Errorf("index: %s for %s", err.Error(), logName)
|
||||
result.Status = IndexFailed
|
||||
result.Error = err
|
||||
result.Err = err
|
||||
return result
|
||||
}
|
||||
} else {
|
||||
if err := photo.FirstOrCreate(); err != nil {
|
||||
log.Errorf("index: %s", err)
|
||||
result.Status = IndexFailed
|
||||
result.Error = err
|
||||
result.Err = err
|
||||
return result
|
||||
}
|
||||
|
||||
|
@ -576,7 +580,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
if err := photo.Save(); err != nil {
|
||||
log.Errorf("index: %s for %s", err, logName)
|
||||
result.Status = IndexFailed
|
||||
result.Error = err
|
||||
result.Err = err
|
||||
return result
|
||||
}
|
||||
|
||||
|
@ -595,7 +599,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
if err := photo.Save(); err != nil {
|
||||
log.Errorf("index: %s for %s", err, logName)
|
||||
result.Status = IndexFailed
|
||||
result.Error = err
|
||||
result.Err = err
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
@ -608,7 +612,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
if err := file.Save(); err != nil {
|
||||
log.Errorf("index: %s for %s", err, logName)
|
||||
result.Status = IndexFailed
|
||||
result.Error = err
|
||||
result.Err = err
|
||||
return result
|
||||
}
|
||||
} else {
|
||||
|
@ -617,7 +621,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
if err := file.Create(); err != nil {
|
||||
log.Errorf("index: %s for %s", err, logName)
|
||||
result.Status = IndexFailed
|
||||
result.Error = err
|
||||
result.Err = err
|
||||
return result
|
||||
}
|
||||
|
||||
|
@ -658,7 +662,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
if err := photo.SaveAsYaml(yamlFile); err != nil {
|
||||
log.Errorf("index: %s (update yaml) for %s", err.Error(), logName)
|
||||
} else {
|
||||
log.Infof("index: updated yaml file %s", txt.Quote(fs.Rel(yamlFile, Config().OriginalsPath())))
|
||||
log.Infof("index: updated yaml file %s", txt.Quote(fs.RelName(yamlFile, Config().OriginalsPath())))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -679,7 +683,7 @@ func (ind *Index) NSFW(jpeg *MediaFile) bool {
|
|||
return false
|
||||
} else {
|
||||
if nsfwLabels.NSFW(nsfw.ThresholdHigh) {
|
||||
log.Warnf("index: %s might contain offensive content", txt.Quote(jpeg.RelativeName(Config().OriginalsPath())))
|
||||
log.Warnf("index: %s might contain offensive content", txt.Quote(jpeg.RelName(Config().OriginalsPath())))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIndexOptionsNone(t *testing.T) {
|
||||
|
|
107
internal/photoprism/index_related.go
Normal file
107
internal/photoprism/index_related.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// IndexMain indexes the main file from a group of related files and returns the result.
|
||||
func IndexMain(related *RelatedFiles, ind *Index, opt IndexOptions) (result IndexResult) {
|
||||
// Skip sidecar files without related media file.
|
||||
if related.Main == nil {
|
||||
result.Err = fmt.Errorf("index: no main file found for %s", txt.Quote(related.String()))
|
||||
result.Status = IndexFailed
|
||||
return result
|
||||
}
|
||||
|
||||
// Enforce file size limit for originals.
|
||||
if ind.conf.OriginalsLimit() > 0 && related.Main.FileSize() > ind.conf.OriginalsLimit() {
|
||||
result.Err = fmt.Errorf("index: %s exceeds file size limit for originals [%d / %d MB]", filepath.Base(related.Main.FileName()), related.Main.FileSize()/(1024*1024), ind.conf.OriginalsLimit()/(1024*1024))
|
||||
result.Status = IndexFailed
|
||||
return result
|
||||
}
|
||||
|
||||
f := related.Main
|
||||
|
||||
if opt.Convert && !f.HasJpeg() {
|
||||
if jpegFile, err := ind.convert.ToJpeg(f); err != nil {
|
||||
result.Err = fmt.Errorf("index: creating jpeg failed (%s)", err.Error())
|
||||
result.Status = IndexFailed
|
||||
|
||||
return result
|
||||
} else {
|
||||
log.Infof("index: %s created", fs.RelName(jpegFile.FileName(), ind.originalsPath()))
|
||||
|
||||
if err := jpegFile.ResampleDefault(ind.thumbPath(), false); err != nil {
|
||||
result.Err = fmt.Errorf("index: could not create default thumbnails (%s)", err.Error())
|
||||
result.Status = IndexFailed
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
related.Files = append(related.Files, jpegFile)
|
||||
}
|
||||
}
|
||||
|
||||
if ind.conf.SidecarJson() && !f.HasJson() {
|
||||
if jsonFile, err := ind.convert.ToJson(f); err != nil {
|
||||
log.Errorf("index: creating json sidecar file failed (%s)", err.Error())
|
||||
} else {
|
||||
log.Infof("index: %s created", fs.RelName(jsonFile.FileName(), ind.originalsPath()))
|
||||
}
|
||||
}
|
||||
|
||||
result = ind.MediaFile(f, opt, "")
|
||||
|
||||
if result.Indexed() && f.IsJpeg() {
|
||||
if err := f.ResampleDefault(ind.thumbPath(), false); err != nil {
|
||||
log.Errorf("index: could not create default thumbnails (%s)", err.Error())
|
||||
query.SetFileError(result.FileUID, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("index: %s main %s file %s", result, f.FileType(), txt.Quote(f.RelName(ind.originalsPath())))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// IndexMain indexes a group of related files and returns the result.
|
||||
func IndexRelated(related RelatedFiles, ind *Index, opt IndexOptions) (result IndexResult) {
|
||||
done := make(map[string]bool)
|
||||
|
||||
result = IndexMain(&related, ind, opt)
|
||||
|
||||
if result.Failed() {
|
||||
log.Error(result.Err)
|
||||
return result
|
||||
} else if !result.Success() || result.Stacked() {
|
||||
// Skip related files if main file was stacked or indexing was not completely successful.
|
||||
return result
|
||||
}
|
||||
|
||||
done[related.Main.FileName()] = true
|
||||
|
||||
for _, f := range related.Files {
|
||||
if done[f.FileName()] {
|
||||
continue
|
||||
}
|
||||
|
||||
res := ind.MediaFile(f, opt, "")
|
||||
done[f.FileName()] = true
|
||||
|
||||
if res.Indexed() && f.IsJpeg() {
|
||||
if err := f.ResampleDefault(ind.thumbPath(), false); err != nil {
|
||||
log.Errorf("index: could not create default thumbnails (%s)", err.Error())
|
||||
query.SetFileError(res.FileUID, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("index: %s related %s file %s", res, f.FileType(), txt.Quote(f.RelName(ind.originalsPath())))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
73
internal/photoprism/index_related_test.go
Normal file
73
internal/photoprism/index_related_test.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/classify"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/nsfw"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIndexRelated(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
testFile, err := NewMediaFile("testdata/2018-04-12 19:24:49.gif")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testRelated, err := testFile.RelatedFiles(true)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testToken := rnd.Token(8)
|
||||
testPath := filepath.Join(conf.OriginalsPath(), testToken)
|
||||
|
||||
for _, f := range testRelated.Files {
|
||||
dest := filepath.Join(testPath, f.BaseName())
|
||||
|
||||
if err := f.Copy(dest); err != nil {
|
||||
t.Fatalf("COPY FAILED: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
mainFile, err := NewMediaFile(filepath.Join(testPath, "2018-04-12 19:24:49.gif"))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
related, err := mainFile.RelatedFiles(true)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tf := classify.New(conf.AssetsPath(), conf.TensorFlowOff())
|
||||
nd := nsfw.New(conf.NSFWModelPath())
|
||||
convert := NewConvert(conf)
|
||||
|
||||
ind := NewIndex(conf, tf, nd, convert)
|
||||
opt := IndexOptionsAll()
|
||||
|
||||
result := IndexRelated(related, ind, opt)
|
||||
|
||||
assert.False(t, result.Failed())
|
||||
assert.False(t, result.Stacked())
|
||||
assert.True(t, result.Success())
|
||||
assert.Equal(t, IndexAdded, result.Status)
|
||||
|
||||
if photo, err := query.PhotoByUID(result.PhotoUID); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, "2018-04-12 19:24:49 +0000 UTC", photo.TakenAt.String())
|
||||
assert.Equal(t, "name", photo.TakenSrc)
|
||||
}
|
||||
}
|
|
@ -1,13 +1,5 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
type IndexJob struct {
|
||||
FileName string
|
||||
Related RelatedFiles
|
||||
|
@ -17,82 +9,6 @@ type IndexJob struct {
|
|||
|
||||
func IndexWorker(jobs <-chan IndexJob) {
|
||||
for job := range jobs {
|
||||
done := make(map[string]bool)
|
||||
related := job.Related
|
||||
opt := job.IndexOpt
|
||||
ind := job.Ind
|
||||
|
||||
// Skip sidecar files without related media file.
|
||||
if related.Main == nil {
|
||||
log.Warnf("index: no media file found for %s", txt.Quote(fs.Rel(job.FileName, ind.originalsPath())))
|
||||
continue
|
||||
}
|
||||
|
||||
// Enforce file size limit for originals.
|
||||
if ind.conf.OriginalsLimit() > 0 && related.Main.FileSize() > ind.conf.OriginalsLimit() {
|
||||
log.Warnf("index: %s exceeds file size limit for originals [%d / %d MB]", filepath.Base(related.Main.FileName()), related.Main.FileSize()/(1024*1024), ind.conf.OriginalsLimit()/(1024*1024))
|
||||
continue
|
||||
}
|
||||
|
||||
f := related.Main
|
||||
|
||||
if opt.Convert && !f.HasJpeg() {
|
||||
if jpegFile, err := ind.convert.ToJpeg(f); err != nil {
|
||||
log.Errorf("index: creating jpeg failed (%s)", err.Error())
|
||||
continue
|
||||
} else {
|
||||
log.Infof("index: %s created", fs.Rel(jpegFile.FileName(), ind.originalsPath()))
|
||||
|
||||
if err := jpegFile.ResampleDefault(ind.thumbPath(), false); err != nil {
|
||||
log.Errorf("index: could not create default thumbnails (%s)", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
related.Files = append(related.Files, jpegFile)
|
||||
}
|
||||
}
|
||||
|
||||
if ind.conf.SidecarJson() && !f.HasJson() {
|
||||
if jsonFile, err := ind.convert.ToJson(f); err != nil {
|
||||
log.Errorf("index: creating json sidecar file failed (%s)", err.Error())
|
||||
} else {
|
||||
log.Infof("index: %s created", fs.Rel(jsonFile.FileName(), ind.originalsPath()))
|
||||
}
|
||||
}
|
||||
|
||||
res := ind.MediaFile(f, opt, "")
|
||||
done[f.FileName()] = true
|
||||
|
||||
if res.Indexed() && f.IsJpeg() {
|
||||
if err := f.ResampleDefault(ind.thumbPath(), false); err != nil {
|
||||
log.Errorf("index: could not create default thumbnails (%s)", err.Error())
|
||||
query.SetFileError(res.FileUID, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("index: %s main %s file %s", res, f.FileType(), txt.Quote(f.RelativeName(ind.originalsPath())))
|
||||
|
||||
// Skip related files if main file was merged or an error occurred.
|
||||
if !res.Success() || res.Grouped() {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, f := range related.Files {
|
||||
if done[f.FileName()] {
|
||||
continue
|
||||
}
|
||||
|
||||
res := ind.MediaFile(f, opt, "")
|
||||
done[f.FileName()] = true
|
||||
|
||||
if res.Indexed() && f.IsJpeg() {
|
||||
if err := f.ResampleDefault(ind.thumbPath(), false); err != nil {
|
||||
log.Errorf("index: could not create default thumbnails (%s)", err.Error())
|
||||
query.SetFileError(res.FileUID, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("index: %s related %s file %s", res, f.FileType(), txt.Quote(f.RelativeName(ind.originalsPath())))
|
||||
}
|
||||
IndexRelated(job.Related, job.Ind, job.IndexOpt)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -218,7 +218,7 @@ func (m *MediaFile) CanonicalNameFromFile() string {
|
|||
// CanonicalNameFromFileWithDirectory gets the canonical name for a MediaFile
|
||||
// including the directory.
|
||||
func (m *MediaFile) CanonicalNameFromFileWithDirectory() string {
|
||||
return m.Directory() + string(os.PathSeparator) + m.CanonicalNameFromFile()
|
||||
return m.Dir() + string(os.PathSeparator) + m.CanonicalNameFromFile()
|
||||
}
|
||||
|
||||
// Hash returns the SHA1 hash of a media file.
|
||||
|
@ -266,7 +266,7 @@ func (m *MediaFile) JsonName() string {
|
|||
// RelatedFiles returns files which are related to this file.
|
||||
func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err error) {
|
||||
// escape any meta characters in the file name
|
||||
matches, err := filepath.Glob(regexp.QuoteMeta(m.AbsBase(stripSequence)) + "*")
|
||||
matches, err := filepath.Glob(regexp.QuoteMeta(m.AbsPrefix(stripSequence)) + "*")
|
||||
|
||||
if err != nil {
|
||||
return result, err
|
||||
|
@ -332,9 +332,9 @@ func (m *MediaFile) PathNameInfo() (fileRoot, fileBase, relativePath, relativeNa
|
|||
rootPath = Config().OriginalsPath()
|
||||
}
|
||||
|
||||
fileBase = m.Base(Config().Settings().Index.Sequences)
|
||||
relativePath = m.RelativePath(rootPath)
|
||||
relativeName = m.RelativeName(rootPath)
|
||||
fileBase = m.BasePrefix(Config().Settings().Index.Sequences)
|
||||
relativePath = m.RelPath(rootPath)
|
||||
relativeName = m.RelName(rootPath)
|
||||
|
||||
return fileRoot, fileBase, relativePath, relativeName
|
||||
}
|
||||
|
@ -354,13 +354,13 @@ func (m *MediaFile) SetFileName(fileName string) {
|
|||
m.fileName = fileName
|
||||
}
|
||||
|
||||
// Rel returns the relative filename.
|
||||
func (m *MediaFile) RelativeName(directory string) string {
|
||||
return fs.Rel(m.fileName, directory)
|
||||
// RelName returns the relative filename.
|
||||
func (m *MediaFile) RelName(directory string) string {
|
||||
return fs.RelName(m.fileName, directory)
|
||||
}
|
||||
|
||||
// RelativePath returns the relative path without filename.
|
||||
func (m *MediaFile) RelativePath(directory string) string {
|
||||
// RelPath returns the relative path without filename.
|
||||
func (m *MediaFile) RelPath(directory string) string {
|
||||
pathname := m.fileName
|
||||
|
||||
if i := strings.Index(pathname, directory); i == 0 {
|
||||
|
@ -390,31 +390,31 @@ func (m *MediaFile) RelativePath(directory string) string {
|
|||
return pathname
|
||||
}
|
||||
|
||||
// RelBase returns the relative filename.
|
||||
func (m *MediaFile) RelativeBase(directory string, stripSequence bool) string {
|
||||
if relativePath := m.RelativePath(directory); relativePath != "" {
|
||||
return filepath.Join(relativePath, m.Base(stripSequence))
|
||||
// RelPrefix returns the relative path and file name prefix.
|
||||
func (m *MediaFile) RelPrefix(directory string, stripSequence bool) string {
|
||||
if relativePath := m.RelPath(directory); relativePath != "" {
|
||||
return filepath.Join(relativePath, m.BasePrefix(stripSequence))
|
||||
}
|
||||
|
||||
return m.Base(stripSequence)
|
||||
return m.BasePrefix(stripSequence)
|
||||
}
|
||||
|
||||
// Directory returns the file path.
|
||||
func (m *MediaFile) Directory() string {
|
||||
// Dir returns the file path.
|
||||
func (m *MediaFile) Dir() string {
|
||||
return filepath.Dir(m.fileName)
|
||||
}
|
||||
|
||||
// SubDirectory returns a sub directory name.
|
||||
func (m *MediaFile) SubDirectory(dir string) string {
|
||||
// SubDir returns a sub directory name.
|
||||
func (m *MediaFile) SubDir(dir string) string {
|
||||
return filepath.Join(filepath.Dir(m.fileName), dir)
|
||||
}
|
||||
|
||||
// Base returns the filename base without any extensions and path.
|
||||
func (m *MediaFile) Base(stripSequence bool) string {
|
||||
return fs.Base(m.FileName(), stripSequence)
|
||||
// BasePrefix returns the filename base without any extensions and path.
|
||||
func (m *MediaFile) BasePrefix(stripSequence bool) string {
|
||||
return fs.BasePrefix(m.FileName(), stripSequence)
|
||||
}
|
||||
|
||||
// Base returns the filename base without any extensions and path.
|
||||
// Root returns the file root directory.
|
||||
func (m *MediaFile) Root() string {
|
||||
if strings.HasPrefix(m.FileName(), Config().OriginalsPath()) {
|
||||
return entity.RootOriginals
|
||||
|
@ -441,9 +441,9 @@ func (m *MediaFile) Root() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// AbsBase returns the directory and base filename without any extensions.
|
||||
func (m *MediaFile) AbsBase(stripSequence bool) string {
|
||||
return fs.AbsBase(m.FileName(), stripSequence)
|
||||
// AbsPrefix returns the directory and base filename without any extensions.
|
||||
func (m *MediaFile) AbsPrefix(stripSequence bool) string {
|
||||
return fs.AbsPrefix(m.FileName(), stripSequence)
|
||||
}
|
||||
|
||||
// MimeType returns the mime type.
|
||||
|
@ -487,16 +487,20 @@ func (m *MediaFile) HasSameName(f *MediaFile) bool {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
func (m *MediaFile) Move(dest string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(m.fileName, dest); err != nil {
|
||||
log.Debugf("could not rename file, falling back to copy and delete: %s", err.Error())
|
||||
} else {
|
||||
m.fileName = newFilename
|
||||
m.fileName = dest
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.Copy(newFilename); err != nil {
|
||||
if err := m.Copy(dest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -504,32 +508,36 @@ func (m *MediaFile) Move(newFilename string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
m.fileName = newFilename
|
||||
m.fileName = dest
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy a MediaFile to another file by destinationFilename.
|
||||
func (m *MediaFile) Copy(destinationFilename string) error {
|
||||
file, err := m.openFile()
|
||||
func (m *MediaFile) Copy(dest string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
thisFile, err := m.openFile()
|
||||
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
defer thisFile.Close()
|
||||
|
||||
destination, err := os.OpenFile(destinationFilename, os.O_RDWR|os.O_CREATE, 0666)
|
||||
destFile, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE, os.ModePerm)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
defer destination.Close()
|
||||
defer destFile.Close()
|
||||
|
||||
_, err = io.Copy(destination, file)
|
||||
_, err = io.Copy(destFile, thisFile)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
|
@ -819,11 +827,11 @@ 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.Base(false))))
|
||||
log.Debug(capture.Time(start, fmt.Sprintf("mediafile: no new thumbnails created for %s", m.BasePrefix(false))))
|
||||
case 1:
|
||||
log.Info(capture.Time(start, fmt.Sprintf("mediafile: one thumbnail created for %s", m.Base(false))))
|
||||
log.Info(capture.Time(start, fmt.Sprintf("mediafile: one thumbnail created for %s", m.BasePrefix(false))))
|
||||
default:
|
||||
log.Info(capture.Time(start, fmt.Sprintf("mediafile: %d thumbnails created for %s", count, m.Base(false))))
|
||||
log.Info(capture.Time(start, fmt.Sprintf("mediafile: %d thumbnails created for %s", count, m.BasePrefix(false))))
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
|
@ -432,7 +432,7 @@ func TestMediaFile_RelatedFiles(t *testing.T) {
|
|||
t.Fatalf("extension should be longer: %s", extension)
|
||||
}
|
||||
|
||||
relativePath := result.RelativePath(conf.ExamplesPath())
|
||||
relativePath := result.RelPath(conf.ExamplesPath())
|
||||
|
||||
if len(relativePath) > 0 {
|
||||
t.Fatalf("relative path should be empty: %s", relativePath)
|
||||
|
@ -634,20 +634,20 @@ func TestMediaFile_RelativeFilename(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Run("directory with end slash", func(t *testing.T) {
|
||||
filename := mediaFile.RelativeName("/go/src/github.com/photoprism/photoprism/assets/")
|
||||
filename := mediaFile.RelName("/go/src/github.com/photoprism/photoprism/assets/")
|
||||
assert.Equal(t, "examples/tree_white.jpg", filename)
|
||||
})
|
||||
|
||||
t.Run("directory without end slash", func(t *testing.T) {
|
||||
filename := mediaFile.RelativeName("/go/src/github.com/photoprism/photoprism/assets")
|
||||
filename := mediaFile.RelName("/go/src/github.com/photoprism/photoprism/assets")
|
||||
assert.Equal(t, "examples/tree_white.jpg", filename)
|
||||
})
|
||||
t.Run("directory not part of filename", func(t *testing.T) {
|
||||
filename := mediaFile.RelativeName("xxx/")
|
||||
filename := mediaFile.RelName("xxx/")
|
||||
assert.Equal(t, conf.ExamplesPath()+"/tree_white.jpg", filename)
|
||||
})
|
||||
t.Run("directory equals example path", func(t *testing.T) {
|
||||
filename := mediaFile.RelativeName("/go/src/github.com/photoprism/photoprism/assets/examples")
|
||||
filename := mediaFile.RelName("/go/src/github.com/photoprism/photoprism/assets/examples")
|
||||
assert.Equal(t, "tree_white.jpg", filename)
|
||||
})
|
||||
}
|
||||
|
@ -663,19 +663,19 @@ func TestMediaFile_RelativePath(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Run("directory with end slash", func(t *testing.T) {
|
||||
path := mediaFile.RelativePath("/go/src/github.com/photoprism/photoprism/assets/")
|
||||
path := mediaFile.RelPath("/go/src/github.com/photoprism/photoprism/assets/")
|
||||
assert.Equal(t, "examples", path)
|
||||
})
|
||||
t.Run("directory without end slash", func(t *testing.T) {
|
||||
path := mediaFile.RelativePath("/go/src/github.com/photoprism/photoprism/assets")
|
||||
path := mediaFile.RelPath("/go/src/github.com/photoprism/photoprism/assets")
|
||||
assert.Equal(t, "examples", path)
|
||||
})
|
||||
t.Run("directory equals filepath", func(t *testing.T) {
|
||||
path := mediaFile.RelativePath(conf.ExamplesPath())
|
||||
path := mediaFile.RelPath(conf.ExamplesPath())
|
||||
assert.Equal(t, "", path)
|
||||
})
|
||||
t.Run("directory does not match filepath", func(t *testing.T) {
|
||||
path := mediaFile.RelativePath("xxx")
|
||||
path := mediaFile.RelPath("xxx")
|
||||
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/examples", path)
|
||||
})
|
||||
|
||||
|
@ -686,15 +686,15 @@ func TestMediaFile_RelativePath(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Run("hidden", func(t *testing.T) {
|
||||
path := mediaFile.RelativePath(conf.ExamplesPath())
|
||||
path := mediaFile.RelPath(conf.ExamplesPath())
|
||||
assert.Equal(t, "", path)
|
||||
})
|
||||
t.Run("hidden empty", func(t *testing.T) {
|
||||
path := mediaFile.RelativePath("")
|
||||
path := mediaFile.RelPath("")
|
||||
assert.Equal(t, conf.ExamplesPath(), path)
|
||||
})
|
||||
t.Run("hidden root", func(t *testing.T) {
|
||||
path := mediaFile.RelativePath(filepath.Join(conf.ExamplesPath(), fs.HiddenPath))
|
||||
path := mediaFile.RelPath(filepath.Join(conf.ExamplesPath(), fs.HiddenPath))
|
||||
assert.Equal(t, "", path)
|
||||
})
|
||||
}
|
||||
|
@ -708,15 +708,15 @@ func TestMediaFile_RelativeBasename(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Run("directory with end slash", func(t *testing.T) {
|
||||
basename := mediaFile.RelativeBase("/go/src/github.com/photoprism/photoprism/assets/", true)
|
||||
basename := mediaFile.RelPrefix("/go/src/github.com/photoprism/photoprism/assets/", true)
|
||||
assert.Equal(t, "examples/tree_white", basename)
|
||||
})
|
||||
t.Run("directory without end slash", func(t *testing.T) {
|
||||
basename := mediaFile.RelativeBase("/go/src/github.com/photoprism/photoprism/assets", true)
|
||||
basename := mediaFile.RelPrefix("/go/src/github.com/photoprism/photoprism/assets", true)
|
||||
assert.Equal(t, "examples/tree_white", basename)
|
||||
})
|
||||
t.Run("directory equals example path", func(t *testing.T) {
|
||||
basename := mediaFile.RelativeBase("/go/src/github.com/photoprism/photoprism/assets/examples/", true)
|
||||
basename := mediaFile.RelPrefix("/go/src/github.com/photoprism/photoprism/assets/examples/", true)
|
||||
assert.Equal(t, "tree_white", basename)
|
||||
})
|
||||
|
||||
|
@ -730,7 +730,7 @@ func TestMediaFile_Directory(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, conf.ExamplesPath(), mediaFile.Directory())
|
||||
assert.Equal(t, conf.ExamplesPath(), mediaFile.Dir())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -742,7 +742,7 @@ func TestMediaFile_Basename(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "limes", mediaFile.Base(true))
|
||||
assert.Equal(t, "limes", mediaFile.BasePrefix(true))
|
||||
})
|
||||
t.Run("/IMG_4120 copy.JPG", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
@ -751,7 +751,7 @@ func TestMediaFile_Basename(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "IMG_4120", mediaFile.Base(true))
|
||||
assert.Equal(t, "IMG_4120", mediaFile.BasePrefix(true))
|
||||
})
|
||||
t.Run("/IMG_4120 (1).JPG", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
@ -760,7 +760,7 @@ func TestMediaFile_Basename(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "IMG_4120", mediaFile.Base(true))
|
||||
assert.Equal(t, "IMG_4120", mediaFile.BasePrefix(true))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1834,7 +1834,7 @@ func TestMediaFile_SubDirectory(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
subdir := mediaFile.SubDirectory("xxx")
|
||||
subdir := mediaFile.SubDir("xxx")
|
||||
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/examples/xxx", subdir)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// List of related files for importing and indexing.
|
||||
type RelatedFiles struct {
|
||||
Files MediaFiles
|
||||
|
@ -7,16 +11,32 @@ type RelatedFiles struct {
|
|||
}
|
||||
|
||||
// ContainsJpeg returns true if related file list contains a JPEG.
|
||||
func (rf RelatedFiles) ContainsJpeg() bool {
|
||||
for _, f := range rf.Files {
|
||||
func (m RelatedFiles) ContainsJpeg() bool {
|
||||
for _, f := range m.Files {
|
||||
if f.IsJpeg() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if rf.Main == nil {
|
||||
if m.Main == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return rf.Main.IsJpeg()
|
||||
return m.Main.IsJpeg()
|
||||
}
|
||||
|
||||
// String returns file names as string.
|
||||
func (m RelatedFiles) String() string {
|
||||
names := make([]string, len(m.Files))
|
||||
|
||||
for i, f := range m.Files {
|
||||
names[i] = f.BaseName()
|
||||
}
|
||||
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
|
||||
// Len returns the number of related files.
|
||||
func (m RelatedFiles) Len() int {
|
||||
return len(m.Files)
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ func (rs *Resample) Start(force bool) (err error) {
|
|||
}
|
||||
|
||||
ignore.Log = func(fileName string) {
|
||||
log.Infof(`resample: ignored "%s"`, fs.Rel(fileName, originalsPath))
|
||||
log.Infof(`resample: ignored "%s"`, fs.RelName(fileName, originalsPath))
|
||||
}
|
||||
|
||||
err = godirwalk.Walk(originalsPath, &godirwalk.Options{
|
||||
|
@ -93,7 +93,7 @@ func (rs *Resample) Start(force bool) (err error) {
|
|||
|
||||
done[fileName] = true
|
||||
|
||||
relativeName := mf.RelativeName(originalsPath)
|
||||
relativeName := mf.RelName(originalsPath)
|
||||
|
||||
event.Publish("index.thumbnails", event.Data{
|
||||
"fileName": relativeName,
|
||||
|
|
|
@ -53,8 +53,8 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.UpdatePhotoLabel(v1)
|
||||
api.GetMomentsTime(v1)
|
||||
api.GetFile(v1)
|
||||
api.PhotoFilePrimary(v1)
|
||||
api.PhotoFileUnstack(v1)
|
||||
api.PhotoPrimary(v1)
|
||||
api.PhotoUnstack(v1)
|
||||
|
||||
api.GetLabels(v1)
|
||||
api.UpdateLabel(v1)
|
||||
|
|
|
@ -35,7 +35,7 @@ func (worker *Sync) relatedDownloads(a entity.Account) (result Downloads, err er
|
|||
|
||||
// Group results by directory and base name
|
||||
for i, file := range files {
|
||||
k := fs.AbsBase(file.RemoteName, worker.conf.Settings().Index.Sequences)
|
||||
k := fs.AbsPrefix(file.RemoteName, worker.conf.Settings().Index.Sequences)
|
||||
|
||||
result[k] = append(result[k], file)
|
||||
|
||||
|
|
|
@ -24,14 +24,8 @@ func StripKnownExt(name string) string {
|
|||
return name
|
||||
}
|
||||
|
||||
// Base returns the filename base without any extensions and path.
|
||||
func Base(fileName string, stripSequence bool) string {
|
||||
name := StripKnownExt(StripExt(filepath.Base(fileName)))
|
||||
|
||||
if !stripSequence {
|
||||
return name
|
||||
}
|
||||
|
||||
// StripSequence removes common sequence patterns at the end of file names.
|
||||
func StripSequence(name string) string {
|
||||
// Strip numeric extensions like .00000, .00001, .4542353245,.... (at least 5 digits).
|
||||
if dot := strings.LastIndex(name, "."); dot != -1 && len(name[dot+1:]) >= 5 {
|
||||
if i, err := strconv.Atoi(name[dot+1:]); err == nil && i >= 0 {
|
||||
|
@ -53,16 +47,27 @@ func Base(fileName string, stripSequence bool) string {
|
|||
return name
|
||||
}
|
||||
|
||||
// RelBase returns the relative filename.
|
||||
func RelBase(fileName, dir string, stripSequence bool) string {
|
||||
if name := Rel(fileName, dir); name != "" {
|
||||
return AbsBase(name, stripSequence)
|
||||
// BasePrefix returns the filename base without any extensions and path.
|
||||
func BasePrefix(fileName string, stripSequence bool) string {
|
||||
name := StripKnownExt(StripExt(filepath.Base(fileName)))
|
||||
|
||||
if !stripSequence {
|
||||
return name
|
||||
}
|
||||
|
||||
return Base(fileName, stripSequence)
|
||||
return StripSequence(name)
|
||||
}
|
||||
|
||||
// AbsBase returns the directory and base filename without any extensions.
|
||||
func AbsBase(fileName string, stripSequence bool) string {
|
||||
return filepath.Join(filepath.Dir(fileName), Base(fileName, stripSequence))
|
||||
// RelPrefix returns the relative filename.
|
||||
func RelPrefix(fileName, dir string, stripSequence bool) string {
|
||||
if name := RelName(fileName, dir); name != "" {
|
||||
return AbsPrefix(name, stripSequence)
|
||||
}
|
||||
|
||||
return BasePrefix(fileName, stripSequence)
|
||||
}
|
||||
|
||||
// AbsPrefix returns the directory and base filename without any extensions.
|
||||
func AbsPrefix(fileName string, stripSequence bool) string {
|
||||
return filepath.Join(filepath.Dir(fileName), BasePrefix(fileName, stripSequence))
|
||||
}
|
||||
|
|
|
@ -42,134 +42,134 @@ func TestStripKnownExt(t *testing.T) {
|
|||
|
||||
func TestBase(t *testing.T) {
|
||||
t.Run("Screenshot 2019-05-21 at 10.45.52.png", func(t *testing.T) {
|
||||
regular := Base("Screenshot 2019-05-21 at 10.45.52.png", false)
|
||||
regular := BasePrefix("Screenshot 2019-05-21 at 10.45.52.png", false)
|
||||
assert.Equal(t, "Screenshot 2019-05-21 at 10.45.52", regular)
|
||||
stripped := Base("Screenshot 2019-05-21 at 10.45.52.png", true)
|
||||
stripped := BasePrefix("Screenshot 2019-05-21 at 10.45.52.png", true)
|
||||
assert.Equal(t, "Screenshot 2019-05-21 at 10.45.52", stripped)
|
||||
})
|
||||
|
||||
t.Run("Test.jpg", func(t *testing.T) {
|
||||
result := Base("/testdata/Test.jpg", true)
|
||||
result := BasePrefix("/testdata/Test.jpg", true)
|
||||
assert.Equal(t, "Test", result)
|
||||
})
|
||||
|
||||
t.Run("Test.jpg.json", func(t *testing.T) {
|
||||
result := Base("/testdata/Test.jpg.json", true)
|
||||
result := BasePrefix("/testdata/Test.jpg.json", true)
|
||||
assert.Equal(t, "Test", result)
|
||||
})
|
||||
|
||||
t.Run("Test copy 3.jpg", func(t *testing.T) {
|
||||
result := Base("/testdata/Test copy 3.jpg", true)
|
||||
result := BasePrefix("/testdata/Test copy 3.jpg", true)
|
||||
assert.Equal(t, "Test", result)
|
||||
})
|
||||
|
||||
t.Run("Test (3).jpg", func(t *testing.T) {
|
||||
result := Base("/testdata/Test (3).jpg", true)
|
||||
result := BasePrefix("/testdata/Test (3).jpg", true)
|
||||
assert.Equal(t, "Test", result)
|
||||
})
|
||||
|
||||
t.Run("Test.jpg", func(t *testing.T) {
|
||||
result := Base("/testdata/Test.jpg", false)
|
||||
result := BasePrefix("/testdata/Test.jpg", false)
|
||||
assert.Equal(t, "Test", result)
|
||||
})
|
||||
|
||||
t.Run("Test.3453453.jpg", func(t *testing.T) {
|
||||
regular := Base("/testdata/Test.3453453.jpg", false)
|
||||
regular := BasePrefix("/testdata/Test.3453453.jpg", false)
|
||||
assert.Equal(t, "Test.3453453", regular)
|
||||
|
||||
stripped := Base("/testdata/Test.3453453.jpg", true)
|
||||
stripped := BasePrefix("/testdata/Test.3453453.jpg", true)
|
||||
assert.Equal(t, "Test", stripped)
|
||||
})
|
||||
|
||||
t.Run("/foo/bar.0000.ZIP", func(t *testing.T) {
|
||||
regular := Base("/foo/bar.0000.ZIP", false)
|
||||
regular := BasePrefix("/foo/bar.0000.ZIP", false)
|
||||
assert.Equal(t, "bar.0000", regular)
|
||||
|
||||
stripped := Base("/foo/bar.0000.ZIP", true)
|
||||
stripped := BasePrefix("/foo/bar.0000.ZIP", true)
|
||||
assert.Equal(t, "bar.0000", stripped)
|
||||
})
|
||||
|
||||
t.Run("/foo/bar.00001.ZIP", func(t *testing.T) {
|
||||
regular := Base("/foo/bar.00001.ZIP", false)
|
||||
regular := BasePrefix("/foo/bar.00001.ZIP", false)
|
||||
assert.Equal(t, "bar.00001", regular)
|
||||
|
||||
stripped := Base("/foo/bar.00001.ZIP", true)
|
||||
stripped := BasePrefix("/foo/bar.00001.ZIP", true)
|
||||
assert.Equal(t, "bar", stripped)
|
||||
})
|
||||
|
||||
t.Run("Test copy 3.jpg", func(t *testing.T) {
|
||||
result := Base("/testdata/Test copy 3.jpg", false)
|
||||
result := BasePrefix("/testdata/Test copy 3.jpg", false)
|
||||
assert.Equal(t, "Test copy 3", result)
|
||||
})
|
||||
|
||||
t.Run("Test (3).jpg", func(t *testing.T) {
|
||||
result := Base("/testdata/Test (3).jpg", false)
|
||||
result := BasePrefix("/testdata/Test (3).jpg", false)
|
||||
assert.Equal(t, "Test (3)", result)
|
||||
})
|
||||
t.Run("20180506_091537_DSC02122.JPG", func(t *testing.T) {
|
||||
result := Base("20180506_091537_DSC02122.JPG", true)
|
||||
result := BasePrefix("20180506_091537_DSC02122.JPG", true)
|
||||
assert.Equal(t, "20180506_091537_DSC02122", result)
|
||||
})
|
||||
t.Run("20180506_091537_DSC02122 (+3.3).JPG", func(t *testing.T) {
|
||||
result := Base("20180506_091537_DSC02122 (+3.3).JPG", true)
|
||||
result := BasePrefix("20180506_091537_DSC02122 (+3.3).JPG", true)
|
||||
assert.Equal(t, "20180506_091537_DSC02122", result)
|
||||
})
|
||||
t.Run("20180506_091537_DSC02122 (-2.7).JPG", func(t *testing.T) {
|
||||
result := Base("20180506_091537_DSC02122 (-2.7).JPG", true)
|
||||
result := BasePrefix("20180506_091537_DSC02122 (-2.7).JPG", true)
|
||||
assert.Equal(t, "20180506_091537_DSC02122", result)
|
||||
})
|
||||
t.Run("20180506_091537_DSC02122(+3.3).JPG", func(t *testing.T) {
|
||||
result := Base("20180506_091537_DSC02122(+3.3).JPG", true)
|
||||
result := BasePrefix("20180506_091537_DSC02122(+3.3).JPG", true)
|
||||
assert.Equal(t, "20180506_091537_DSC02122", result)
|
||||
})
|
||||
t.Run("20180506_091537_DSC02122(-2.7).JPG", func(t *testing.T) {
|
||||
result := Base("20180506_091537_DSC02122(-2.7).JPG", true)
|
||||
result := BasePrefix("20180506_091537_DSC02122(-2.7).JPG", true)
|
||||
assert.Equal(t, "20180506_091537_DSC02122", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRelBase(t *testing.T) {
|
||||
t.Run("/foo/bar.0000.ZIP", func(t *testing.T) {
|
||||
regular := RelBase("/foo/bar.0000.ZIP", "/bar", false)
|
||||
regular := RelPrefix("/foo/bar.0000.ZIP", "/bar", false)
|
||||
assert.Equal(t, "/foo/bar.0000", regular)
|
||||
|
||||
stripped := RelBase("/foo/bar.0000.ZIP", "/bar", true)
|
||||
stripped := RelPrefix("/foo/bar.0000.ZIP", "/bar", true)
|
||||
assert.Equal(t, "/foo/bar.0000", stripped)
|
||||
})
|
||||
|
||||
t.Run("/foo/bar.00001.ZIP", func(t *testing.T) {
|
||||
regular := RelBase("/foo/bar.00001.ZIP", "/bar", false)
|
||||
regular := RelPrefix("/foo/bar.00001.ZIP", "/bar", false)
|
||||
assert.Equal(t, "/foo/bar.00001", regular)
|
||||
|
||||
stripped := RelBase("/foo/bar.00001.ZIP", "/bar", true)
|
||||
stripped := RelPrefix("/foo/bar.00001.ZIP", "/bar", true)
|
||||
assert.Equal(t, "/foo/bar", stripped)
|
||||
})
|
||||
|
||||
t.Run("Test copy 3.jpg", func(t *testing.T) {
|
||||
result := RelBase("/testdata/foo/Test copy 3.jpg", "/testdata", false)
|
||||
result := RelPrefix("/testdata/foo/Test copy 3.jpg", "/testdata", false)
|
||||
assert.Equal(t, "foo/Test copy 3", result)
|
||||
})
|
||||
|
||||
t.Run("Test (3).jpg", func(t *testing.T) {
|
||||
result := RelBase("/testdata/foo/Test (3).jpg", "/testdata", false)
|
||||
result := RelPrefix("/testdata/foo/Test (3).jpg", "/testdata", false)
|
||||
assert.Equal(t, "foo/Test (3)", result)
|
||||
})
|
||||
|
||||
t.Run("Test (3).jpg", func(t *testing.T) {
|
||||
result := RelBase("/testdata/foo/Test (3).jpg", "/testdata/foo/Test (3).jpg", false)
|
||||
result := RelPrefix("/testdata/foo/Test (3).jpg", "/testdata/foo/Test (3).jpg", false)
|
||||
assert.Equal(t, "Test (3)", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBaseAbs(t *testing.T) {
|
||||
t.Run("Test copy 3.jpg", func(t *testing.T) {
|
||||
result := AbsBase("/testdata/Test (4).jpg", true)
|
||||
result := AbsPrefix("/testdata/Test (4).jpg", true)
|
||||
|
||||
assert.Equal(t, "/testdata/Test", result)
|
||||
})
|
||||
|
||||
t.Run("Test (3).jpg", func(t *testing.T) {
|
||||
result := AbsBase("/testdata/Test (4).jpg", false)
|
||||
result := AbsPrefix("/testdata/Test (4).jpg", false)
|
||||
|
||||
assert.Equal(t, "/testdata/Test (4)", result)
|
||||
})
|
||||
|
|
|
@ -149,7 +149,7 @@ var TypeExt = FileExt.TypeExt()
|
|||
|
||||
// Find returns the first filename with the same base name and a given type.
|
||||
func (t FileType) Find(fileName string, stripSequence bool) string {
|
||||
base := Base(fileName, stripSequence)
|
||||
base := BasePrefix(fileName, stripSequence)
|
||||
dir := filepath.Dir(fileName)
|
||||
|
||||
prefix := filepath.Join(dir, base)
|
||||
|
@ -187,7 +187,7 @@ func GetFileType(fileName string) FileType {
|
|||
|
||||
// FindFirst searches a list of directories for the first file with the same base name and a given type.
|
||||
func (t FileType) FindFirst(fileName string, dirs []string, baseDir string, stripSequence bool) string {
|
||||
fileBase := Base(fileName, stripSequence)
|
||||
fileBase := BasePrefix(fileName, stripSequence)
|
||||
fileBaseLower := strings.ToLower(fileBase)
|
||||
fileBaseUpper := strings.ToUpper(fileBase)
|
||||
|
||||
|
@ -206,7 +206,7 @@ func (t FileType) FindFirst(fileName string, dirs []string, baseDir string, stri
|
|||
|
||||
if dir != fileDir {
|
||||
if filepath.IsAbs(dir) {
|
||||
dir = filepath.Join(dir, Rel(fileDir, baseDir))
|
||||
dir = filepath.Join(dir, RelName(fileDir, baseDir))
|
||||
} else {
|
||||
dir = filepath.Join(fileDir, dir)
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ func IsGenerated(fileName string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
base := Base(fileName, false)
|
||||
base := BasePrefix(fileName, false)
|
||||
|
||||
if IsHash(base) {
|
||||
return true
|
||||
|
|
|
@ -10,13 +10,13 @@ import (
|
|||
// FileName returns the a relative filename with the same base and a given extension in a directory.
|
||||
func FileName(fileName, dirName, baseDir, fileExt string, stripSequence bool) string {
|
||||
fileDir := filepath.Dir(fileName)
|
||||
baseName := Base(fileName, stripSequence)
|
||||
baseName := BasePrefix(fileName, stripSequence)
|
||||
|
||||
if dirName == "" || dirName == "." {
|
||||
dirName = fileDir
|
||||
} else if fileDir != dirName {
|
||||
if filepath.IsAbs(dirName) {
|
||||
dirName = filepath.Join(dirName, Rel(fileDir, baseDir))
|
||||
dirName = filepath.Join(dirName, RelName(fileDir, baseDir))
|
||||
} else {
|
||||
dirName = filepath.Join(fileDir, dirName)
|
||||
}
|
||||
|
@ -32,8 +32,8 @@ func FileName(fileName, dirName, baseDir, fileExt string, stripSequence bool) st
|
|||
return result
|
||||
}
|
||||
|
||||
// Rel returns the file name relative to a directory.
|
||||
func Rel(fileName, dir string) string {
|
||||
// RelName returns the file name relative to a directory.
|
||||
func RelName(fileName, dir string) string {
|
||||
if fileName == dir {
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -10,25 +10,25 @@ import (
|
|||
|
||||
func TestRel(t *testing.T) {
|
||||
t.Run("same", func(t *testing.T) {
|
||||
assert.Equal(t, "", Rel("/some/path", "/some/path"))
|
||||
assert.Equal(t, "", RelName("/some/path", "/some/path"))
|
||||
})
|
||||
t.Run("short", func(t *testing.T) {
|
||||
assert.Equal(t, "/some/", Rel("/some/", "/some/path"))
|
||||
assert.Equal(t, "/some/", RelName("/some/", "/some/path"))
|
||||
})
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
assert.Equal(t, "", Rel("", "/some/path"))
|
||||
assert.Equal(t, "", RelName("", "/some/path"))
|
||||
})
|
||||
t.Run("/some/path", func(t *testing.T) {
|
||||
assert.Equal(t, "foo/bar.baz", Rel("/some/path/foo/bar.baz", "/some/path"))
|
||||
assert.Equal(t, "foo/bar.baz", RelName("/some/path/foo/bar.baz", "/some/path"))
|
||||
})
|
||||
t.Run("/some/path/", func(t *testing.T) {
|
||||
assert.Equal(t, "foo/bar.baz", Rel("/some/path/foo/bar.baz", "/some/path/"))
|
||||
assert.Equal(t, "foo/bar.baz", RelName("/some/path/foo/bar.baz", "/some/path/"))
|
||||
})
|
||||
t.Run("/some/path/bar", func(t *testing.T) {
|
||||
assert.Equal(t, "/some/path/foo/bar.baz", Rel("/some/path/foo/bar.baz", "/some/path/bar"))
|
||||
assert.Equal(t, "/some/path/foo/bar.baz", RelName("/some/path/foo/bar.baz", "/some/path/bar"))
|
||||
})
|
||||
t.Run("empty dir", func(t *testing.T) {
|
||||
assert.Equal(t, "/some/path/foo/bar.baz", Rel("/some/path/foo/bar.baz", ""))
|
||||
assert.Equal(t, "/some/path/foo/bar.baz", RelName("/some/path/foo/bar.baz", ""))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ var FileTitleRegexp = regexp.MustCompile("[\\p{L}\\-,':]{2,}")
|
|||
|
||||
// FileTitle returns the string with the first characters of each word converted to uppercase.
|
||||
func FileTitle(s string) string {
|
||||
s = fs.Base(s, true)
|
||||
s = fs.BasePrefix(s, true)
|
||||
|
||||
if len(s) < 3 {
|
||||
return ""
|
||||
|
|
|
@ -29,6 +29,9 @@ sync
|
|||
win
|
||||
usb
|
||||
dsc
|
||||
dcf
|
||||
dmc
|
||||
lx
|
||||
pic
|
||||
pict
|
||||
picture
|
||||
|
|
|
@ -34,6 +34,9 @@ var StopWords = map[string]bool{
|
|||
"win": true,
|
||||
"usb": true,
|
||||
"dsc": true,
|
||||
"dcf": true,
|
||||
"dmc": true,
|
||||
"lx": true,
|
||||
"pic": true,
|
||||
"pict": true,
|
||||
"picture": true,
|
||||
|
|
Loading…
Reference in a new issue