Index: Improve handling of archived photos, skip when possible #2257

This commit is contained in:
Michael Mayer
2022-04-16 12:39:47 +02:00
parent c13fdedb72
commit fefaada1f1
16 changed files with 78 additions and 41 deletions

View File

@@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl" "github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
@@ -30,8 +31,9 @@ func StartIndexing(router *gin.RouterGroup) {
} }
conf := service.Config() conf := service.Config()
settings := conf.Settings()
if !conf.Settings().Features.Library { if !settings.Features.Library {
AbortFeatureDisabled(c) AbortFeatureDisabled(c)
return return
} }
@@ -45,12 +47,12 @@ func StartIndexing(router *gin.RouterGroup) {
return return
} }
// Configure index options.
path := conf.OriginalsPath() path := conf.OriginalsPath()
convert := settings.Index.Convert && conf.SidecarWritable()
skipArchived := settings.Index.SkipArchived
ind := service.Index() indOpt := photoprism.NewIndexOptions(filepath.Clean(f.Path), f.Rescan, convert, true, false, skipArchived)
convert := conf.Settings().Index.Convert && conf.SidecarWritable()
indOpt := photoprism.NewIndexOptions(filepath.Clean(f.Path), f.Rescan, convert, true, false)
if len(indOpt.Path) > 1 { if len(indOpt.Path) > 1 {
event.InfoMsg(i18n.MsgIndexingFiles, clean.Log(indOpt.Path)) event.InfoMsg(i18n.MsgIndexingFiles, clean.Log(indOpt.Path))
@@ -58,17 +60,21 @@ func StartIndexing(router *gin.RouterGroup) {
event.InfoMsg(i18n.MsgIndexingOriginals) event.InfoMsg(i18n.MsgIndexingOriginals)
} }
// Start indexing.
ind := service.Index()
indexed := ind.Start(indOpt) indexed := ind.Start(indOpt)
RemoveFromFolderCache(entity.RootOriginals) RemoveFromFolderCache(entity.RootOriginals)
prg := service.Purge() // Configure purge options.
prgOpt := photoprism.PurgeOptions{ prgOpt := photoprism.PurgeOptions{
Path: filepath.Clean(f.Path), Path: filepath.Clean(f.Path),
Ignore: indexed, Ignore: indexed,
} }
// Start purging.
prg := service.Purge()
if files, photos, err := prg.Start(prgOpt); err != nil { if files, photos, err := prg.Start(prgOpt); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())})
return return

View File

@@ -52,6 +52,7 @@ func Index() error {
} }
conf := service.Config() conf := service.Config()
settings := conf.Settings()
start := time.Now() start := time.Now()
@@ -59,8 +60,8 @@ func Index() error {
ind := service.Index() ind := service.Index()
convert := conf.Settings().Index.Convert && conf.SidecarWritable() convert := settings.Index.Convert && conf.SidecarWritable()
indOpt := photoprism.NewIndexOptions(entity.RootPath, false, convert, true, false) indOpt := photoprism.NewIndexOptions(entity.RootPath, false, convert, true, false, true)
indexed := ind.Start(indOpt) indexed := ind.Start(indOpt)

View File

@@ -250,9 +250,11 @@ func facesIndexAction(ctx *cli.Context) error {
var indexed fs.Done var indexed fs.Done
settings := conf.Settings()
if w := service.Index(); w != nil { if w := service.Index(); w != nil {
convert := conf.Settings().Index.Convert && conf.SidecarWritable() convert := settings.Index.Convert && conf.SidecarWritable()
opt := photoprism.NewIndexOptions(subPath, true, convert, true, true) opt := photoprism.NewIndexOptions(subPath, true, convert, true, true, true)
indexed = w.Start(opt) indexed = w.Start(opt)
} }

View File

@@ -29,7 +29,11 @@ var IndexCommand = cli.Command{
var indexFlags = []cli.Flag{ var indexFlags = []cli.Flag{
cli.BoolFlag{ cli.BoolFlag{
Name: "force, f", Name: "force, f",
Usage: "re-index all originals, including unchanged files", Usage: "rescan all originals, including unchanged files",
},
cli.BoolFlag{
Name: "archived, a",
Usage: "update archived photos, do not skip them",
}, },
cli.BoolFlag{ cli.BoolFlag{
Name: "cleanup, c", Name: "cleanup, c",
@@ -70,7 +74,7 @@ func indexAction(ctx *cli.Context) error {
if w := service.Index(); w != nil { if w := service.Index(); w != nil {
convert := conf.Settings().Index.Convert && conf.SidecarWritable() convert := conf.Settings().Index.Convert && conf.SidecarWritable()
opt := photoprism.NewIndexOptions(subPath, ctx.Bool("force"), convert, true, false) opt := photoprism.NewIndexOptions(subPath, ctx.Bool("force"), convert, true, false, !ctx.Bool("archived"))
indexed = w.Start(opt) indexed = w.Start(opt)
} }

View File

@@ -17,7 +17,7 @@ var ShowConfigCommand = cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.BoolFlag{ cli.BoolFlag{
Name: "md, m", Name: "md, m",
Usage: "renders valid Markdown", Usage: "render valid Markdown",
}, },
}, },
Action: showConfigAction, Action: showConfigAction,

View File

@@ -17,7 +17,7 @@ var ShowFiltersCommand = cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.BoolFlag{ cli.BoolFlag{
Name: "md, m", Name: "md, m",
Usage: "renders valid Markdown", Usage: "render valid Markdown",
}, },
}, },
Action: showFiltersAction, Action: showFiltersAction,

View File

@@ -17,11 +17,11 @@ var ShowFormatsCommand = cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.BoolFlag{ cli.BoolFlag{
Name: "short, s", Name: "short, s",
Usage: "hides format descriptions", Usage: "hide format descriptions",
}, },
cli.BoolFlag{ cli.BoolFlag{
Name: "md, m", Name: "md, m",
Usage: "renders valid Markdown", Usage: "render valid Markdown",
}, },
}, },
Action: showFormatsAction, Action: showFormatsAction,

View File

@@ -18,7 +18,7 @@ var ShowTagsCommand = cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.BoolFlag{ cli.BoolFlag{
Name: "md, m", Name: "md, m",
Usage: "renders valid Markdown", Usage: "render valid Markdown",
}, },
}, },
Action: showTagsAction, Action: showTagsAction,

View File

@@ -2,7 +2,8 @@ package config
// IndexSettings represents indexing settings. // IndexSettings represents indexing settings.
type IndexSettings struct { type IndexSettings struct {
Path string `json:"path" yaml:"Path"` Path string `json:"path" yaml:"Path"`
Convert bool `json:"convert" yaml:"Convert"` Convert bool `json:"convert" yaml:"Convert"`
Rescan bool `json:"rescan" yaml:"Rescan"` Rescan bool `json:"rescan" yaml:"Rescan"`
SkipArchived bool `json:"skipArchived" yaml:"SkipArchived"`
} }

View File

@@ -36,6 +36,7 @@ Index:
Path: / Path: /
Convert: true Convert: true
Rescan: false Rescan: false
SkipArchived: false
Stack: Stack:
UUID: true UUID: true
Meta: true Meta: true

View File

@@ -11,14 +11,16 @@ func Count(m interface{}, keys []string, values []interface{}) int {
return -1 return -1
} }
var count int db, count := UnscopedDb(), 0
stmt := Db().Model(m) stmt := db.Model(m)
// Compose where condition.
for k := range keys { for k := range keys {
stmt.Where("? = ?", gorm.Expr(keys[k]), values[k]) stmt.Where("? = ?", gorm.Expr(keys[k]), values[k])
} }
// Fetch count from database.
if err := stmt.Count(&count).Error; err != nil { if err := stmt.Count(&count).Error; err != nil {
log.Debugf("entity: %s (count records)", err) log.Debugf("entity: %s (count records)", err)
return -1 return -1

View File

@@ -6,11 +6,15 @@ import (
// Update updates an existing record in the database. // Update updates an existing record in the database.
func Update(m interface{}, keyNames ...string) (err error) { func Update(m interface{}, keyNames ...string) (err error) {
// Unscoped so soft-deleted records can still be updated.
db := UnscopedDb()
// New entity? // New entity?
if Db().NewRecord(m) { if db.NewRecord(m) {
return fmt.Errorf("new record") return fmt.Errorf("new record")
} }
// Extract interface slice with all values including zero.
values, keys, err := ModelValues(m, keyNames...) values, keys, err := ModelValues(m, keyNames...)
// Has keys and values? // Has keys and values?
@@ -20,16 +24,16 @@ func Update(m interface{}, keyNames ...string) (err error) {
return fmt.Errorf("record keys missing") return fmt.Errorf("record keys missing")
} }
// Perform update. // Update values.
res := Db().Model(m).Updates(values) result := db.Model(m).Updates(values)
// Successful? // Successful?
if res.Error != nil { if result.Error != nil {
return err return err
} else if res.RowsAffected > 1 { } else if result.RowsAffected > 1 {
log.Debugf("entity: updated statement affected more than one record - possible bug") log.Debugf("entity: updated statement affected more than one record - possible bug")
return nil return nil
} else if res.RowsAffected == 1 { } else if result.RowsAffected == 1 {
return nil return nil
} else if Count(m, keyNames, keys) != 1 { } else if Count(m, keyNames, keys) != 1 {
return fmt.Errorf("record not found") return fmt.Errorf("record not found")

View File

@@ -47,22 +47,31 @@ func ModelValues(m interface{}, keyNames ...string) (result Values, keys []inter
continue continue
} }
name := field.Name fieldName := field.Name
// Skip timestamps. // Skip timestamps.
if name == "" || name == "UpdatedAt" || name == "CreatedAt" { if fieldName == "" || fieldName == "UpdatedAt" || fieldName == "CreatedAt" {
continue continue
} }
v := values.Field(i) v := values.Field(i)
switch v.Kind() {
case reflect.Slice, reflect.Chan, reflect.Func, reflect.Map, reflect.UnsafePointer:
continue
case reflect.Struct:
if v.IsZero() {
continue
}
}
// Skip read-only fields. // Skip read-only fields.
if !v.CanSet() { if !v.CanSet() {
continue continue
} }
// Skip keys. // Skip keys.
if isKey(name) { if isKey(fieldName) {
if !v.IsZero() { if !v.IsZero() {
keys = append(keys, v.Interface()) keys = append(keys, v.Interface())
} }
@@ -70,7 +79,7 @@ func ModelValues(m interface{}, keyNames ...string) (result Values, keys []inter
} }
// Add value to result. // Add value to result.
result[name] = v.Interface() result[fieldName] = v.Interface()
} }
if len(result) == 0 { if len(result) == 0 {

View File

@@ -102,8 +102,9 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
filesImported := 0 filesImported := 0
convert := imp.conf.Settings().Index.Convert && imp.conf.SidecarWritable() settings := imp.conf.Settings()
indexOpt := NewIndexOptions("/", true, convert, true, false) convert := settings.Index.Convert && imp.conf.SidecarWritable()
indexOpt := NewIndexOptions("/", true, convert, true, false, false)
skipRaw := imp.conf.DisableRaw() skipRaw := imp.conf.DisableRaw()
ignore := fs.NewIgnoreList(fs.IgnoreFile, true, false) ignore := fs.NewIgnoreList(fs.IgnoreFile, true, false)

View File

@@ -307,8 +307,12 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
} }
if photo.PhotoQuality == -1 && (file.FilePrimary || fileChanged) { if photo.PhotoQuality == -1 && (file.FilePrimary || fileChanged) {
// Restore photos that have been purged automatically. // Restore pictures that have been purged automatically.
photo.DeletedAt = nil photo.DeletedAt = nil
} else if o.SkipArchived && photo.DeletedAt != nil {
// Skip archived pictures for faster indexing.
result.Status = IndexArchived
return result
} }
// Extra labels to ba added when new files have a photo id. // Extra labels to ba added when new files have a photo id.

View File

@@ -7,18 +7,20 @@ type IndexOptions struct {
Convert bool Convert bool
Stack bool Stack bool
FacesOnly bool FacesOnly bool
SkipArchived bool
OriginalsLimit int OriginalsLimit int
ResolutionLimit int ResolutionLimit int
} }
// NewIndexOptions returns new index options instance. // NewIndexOptions returns new index options instance.
func NewIndexOptions(path string, rescan, convert, stack, facesOnly bool) IndexOptions { func NewIndexOptions(path string, rescan, convert, stack, facesOnly, skipArchived bool) IndexOptions {
result := IndexOptions{ result := IndexOptions{
Path: path, Path: path,
Rescan: rescan, Rescan: rescan,
Convert: convert, Convert: convert,
Stack: stack, Stack: stack,
FacesOnly: facesOnly, FacesOnly: facesOnly,
SkipArchived: skipArchived,
OriginalsLimit: Config().OriginalsLimit(), OriginalsLimit: Config().OriginalsLimit(),
ResolutionLimit: Config().ResolutionLimit(), ResolutionLimit: Config().ResolutionLimit(),
} }
@@ -33,20 +35,20 @@ func (o *IndexOptions) SkipUnchanged() bool {
// IndexOptionsAll returns new index options with all options set to true. // IndexOptionsAll returns new index options with all options set to true.
func IndexOptionsAll() IndexOptions { func IndexOptionsAll() IndexOptions {
return NewIndexOptions("/", true, true, true, false) return NewIndexOptions("/", true, true, true, false, true)
} }
// IndexOptionsFacesOnly returns new index options for updating faces only. // IndexOptionsFacesOnly returns new index options for updating faces only.
func IndexOptionsFacesOnly() IndexOptions { func IndexOptionsFacesOnly() IndexOptions {
return NewIndexOptions("/", true, true, true, true) return NewIndexOptions("/", true, true, true, true, true)
} }
// IndexOptionsSingle returns new index options for unstacked, single files. // IndexOptionsSingle returns new index options for unstacked, single files.
func IndexOptionsSingle() IndexOptions { func IndexOptionsSingle() IndexOptions {
return NewIndexOptions("/", true, true, false, false) return NewIndexOptions("/", true, true, false, false, false)
} }
// IndexOptionsNone returns new index options with all options set to false. // IndexOptionsNone returns new index options with all options set to false.
func IndexOptionsNone() IndexOptions { func IndexOptionsNone() IndexOptions {
return NewIndexOptions("", false, false, false, false) return NewIndexOptions("", false, false, false, false, false)
} }