b855922283
Signed-off-by: Michael Mayer <michael@photoprism.app>
207 lines
5.4 KiB
Go
207 lines
5.4 KiB
Go
package photoprism
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime/debug"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/dustin/go-humanize/english"
|
|
|
|
"github.com/photoprism/photoprism/internal/config"
|
|
"github.com/photoprism/photoprism/internal/entity"
|
|
"github.com/photoprism/photoprism/internal/event"
|
|
"github.com/photoprism/photoprism/internal/mutex"
|
|
"github.com/photoprism/photoprism/internal/query"
|
|
"github.com/photoprism/photoprism/pkg/clean"
|
|
"github.com/photoprism/photoprism/pkg/fastwalk"
|
|
"github.com/photoprism/photoprism/pkg/fs"
|
|
)
|
|
|
|
// CleanUp represents a worker that deletes unneeded data and files.
|
|
type CleanUp struct {
|
|
conf *config.Config
|
|
}
|
|
|
|
// NewCleanUp returns a new cleanup worker.
|
|
func NewCleanUp(conf *config.Config) *CleanUp {
|
|
instance := &CleanUp{
|
|
conf: conf,
|
|
}
|
|
|
|
return instance
|
|
}
|
|
|
|
// Start removes orphan index entries and thumbnails.
|
|
func (w *CleanUp) Start(opt CleanUpOptions) (thumbs int, orphans int, sidecars int, err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = fmt.Errorf("cleanup: %s (panic)\nstack: %s", r, debug.Stack())
|
|
log.Error(err)
|
|
}
|
|
}()
|
|
|
|
originalsPath := w.conf.OriginalsPath()
|
|
|
|
// Check if originals folder is empty.
|
|
if fs.DirIsEmpty(originalsPath) {
|
|
return thumbs, orphans, sidecars, err
|
|
}
|
|
|
|
if err = mutex.MainWorker.Start(); err != nil {
|
|
log.Warnf("cleanup: %s (start)", err)
|
|
return thumbs, orphans, sidecars, err
|
|
}
|
|
|
|
defer mutex.MainWorker.Stop()
|
|
|
|
if opt.Dry {
|
|
log.Infof("cleanup: dry run, nothing will actually be removed")
|
|
}
|
|
|
|
// Find and remove orphan photo index entries.
|
|
cleanupStart := time.Now()
|
|
photos, err := query.OrphanPhotos()
|
|
|
|
if err != nil {
|
|
return thumbs, orphans, sidecars, err
|
|
}
|
|
|
|
var deleted []string
|
|
|
|
// Delete orphaned photos from the index and remaining sidecar files from storage, if any.
|
|
for _, p := range photos {
|
|
if mutex.MainWorker.Canceled() {
|
|
return thumbs, orphans, sidecars, errors.New("cleanup canceled")
|
|
}
|
|
|
|
if opt.Dry {
|
|
orphans++
|
|
log.Infof("cleanup: %s would be removed from index", p.String())
|
|
continue
|
|
}
|
|
|
|
// Deletes the index entry and remaining sidecar files outside the "originals" folder.
|
|
if n, err := DeletePhoto(p, true, false); err != nil {
|
|
sidecars += n
|
|
log.Errorf("cleanup: %s (remove orphans)", err)
|
|
} else {
|
|
orphans++
|
|
sidecars += n
|
|
deleted = append(deleted, p.PhotoUID)
|
|
log.Infof("cleanup: removed %s from index", p.String())
|
|
}
|
|
}
|
|
|
|
log.Infof("cleanup: removed %s and %s [%s]", english.Plural(orphans, "index entry", "index entries"), english.Plural(sidecars, "sidecar file", "sidecar files"), time.Since(cleanupStart))
|
|
|
|
// Remove orphan index entries.
|
|
if opt.Dry {
|
|
if files, err := query.OrphanFiles(); err != nil {
|
|
log.Errorf("index: %s (find orphan files)", err)
|
|
} else if l := len(files); l > 0 {
|
|
log.Infof("index: found %s", english.Plural(l, "orphan file", "orphan files"))
|
|
} else {
|
|
log.Infof("index: found no orphan files")
|
|
}
|
|
} else {
|
|
if err = query.PurgeOrphans(); err != nil {
|
|
log.Errorf("index: %s (purge orphans)", err)
|
|
}
|
|
}
|
|
|
|
// Remove orphaned media and thumbnail cache files.
|
|
thumbs, err = w.Cache(opt)
|
|
|
|
// Only update counts if anything was deleted.
|
|
if len(deleted) > 0 {
|
|
// Update precalculated photo and file counts.
|
|
if err = entity.UpdateCounts(); err != nil {
|
|
log.Warnf("index: %s (update counts)", err)
|
|
}
|
|
|
|
// Update album, subject, and label cover thumbs.
|
|
if err = query.UpdateCovers(); err != nil {
|
|
log.Warnf("index: %s (update covers)", err)
|
|
}
|
|
|
|
// Show success notification.
|
|
event.EntitiesDeleted("photos", deleted)
|
|
}
|
|
|
|
return thumbs, orphans, sidecars, err
|
|
}
|
|
|
|
// Cache removes orphaned media and thumbnail cache files.
|
|
func (w *CleanUp) Cache(opt CleanUpOptions) (deleted int, err error) {
|
|
cleanupStart := time.Now()
|
|
|
|
var fileHashes, thumbHashes query.HashMap
|
|
|
|
// Fetch existing media and thumb file hashes.
|
|
if fileHashes, err = query.FileHashMap(); err != nil {
|
|
return deleted, err
|
|
} else if thumbHashes, err = query.ThumbHashMap(); err != nil {
|
|
return deleted, err
|
|
}
|
|
|
|
// At least one SHA1 checksum found?
|
|
if len(fileHashes) == 0 {
|
|
log.Info("cleanup: empty index, aborting search for orphaned cache files")
|
|
return deleted, err
|
|
}
|
|
|
|
// Cache directories.
|
|
dirs := []string{w.conf.MediaCachePath(), w.conf.ThumbCachePath()}
|
|
|
|
log.Info("cleanup: searching for orphaned cache files")
|
|
|
|
// Find and remove orphan thumbnail files.
|
|
for _, dir := range dirs {
|
|
err = fastwalk.Walk(dir, func(fileName string, info os.FileMode) error {
|
|
base := filepath.Base(fileName)
|
|
|
|
if info.IsDir() || strings.HasPrefix(base, ".") {
|
|
return nil
|
|
}
|
|
|
|
// Example: 01244519acf35c62a5fea7a5a7dcefdbec4fb2f5_3x3_resize.png
|
|
i := strings.IndexAny(base, "_.")
|
|
|
|
if i < 39 {
|
|
return nil
|
|
}
|
|
|
|
hash := base[:i]
|
|
logName := clean.Log(fs.RelName(fileName, filepath.Dir(dir)))
|
|
|
|
if ok := fileHashes[hash]; ok {
|
|
// Do nothing.
|
|
} else if ok = thumbHashes[hash]; ok {
|
|
// Do nothing.
|
|
} else if opt.Dry {
|
|
deleted++
|
|
log.Debugf("cleanup: %s would be removed", logName)
|
|
} else if err := os.Remove(fileName); err != nil {
|
|
log.Warnf("cleanup: %s in %s", err, logName)
|
|
} else {
|
|
deleted++
|
|
log.Debugf("cleanup: removed %s from cache", logName)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
log.Infof("cleanup: removed %s from cache [%s]", english.Plural(deleted, "file", "files"), time.Since(cleanupStart))
|
|
|
|
return deleted, err
|
|
}
|
|
|
|
// Cancel stops the current operation.
|
|
func (w *CleanUp) Cancel() {
|
|
mutex.MainWorker.Cancel()
|
|
}
|