Skip to content

Commit

Permalink
Backups: Update YAML file backups when albums are deleted #4243
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Mayer <[email protected]>
  • Loading branch information
lastzero committed May 14, 2024
1 parent fb2a6fc commit 4e7a3c7
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 44 deletions.
41 changes: 27 additions & 14 deletions internal/api/albums.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"net/http"
"os"
"sync"

"github.com/gin-gonic/gin"
Expand All @@ -14,22 +15,23 @@ import (
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/search"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/i18n"
)

var albumMutex = sync.Mutex{}

// SaveAlbumYaml saves the album metadata to a YAML backup file.
func SaveAlbumYaml(a entity.Album) {
c := get.Config()
func SaveAlbumYaml(album entity.Album) {
conf := get.Config()

// Check if saving YAML backup files is enabled.
if !c.BackupAlbums() {
if !conf.BackupAlbums() {
return
}

// Write album metadata to YAML backup file.
_ = a.SaveBackupYaml(c.BackupAlbumsPath())
_ = album.SaveBackupYaml(conf.BackupAlbumsPath())
}

// GetAlbum returns album details as JSON.
Expand Down Expand Up @@ -222,24 +224,35 @@ func DeleteAlbum(router *gin.RouterGroup) {
if a.IsDefault() {
// Soft delete manually created albums.
err = a.Delete()

// Also update album YAML backup.
if err != nil {
log.Errorf("album: %s (delete)", err)
AbortDeleteFailed(c)
return
} else {
SaveAlbumYaml(a)
}
} else {
// Permanently delete automatically created albums.
err = a.DeletePermanently()
}

if err != nil {
log.Errorf("album: %s (delete)", err)
AbortDeleteFailed(c)
return
// Also remove YAML backup file, if it exists.
if err != nil {
log.Errorf("album: %s (delete permanently)", err)
AbortDeleteFailed(c)
return
} else if fileName, relName, nameErr := a.YamlFileName(get.Config().BackupAlbumsPath()); nameErr != nil {
log.Warnf("album: %s (delete %s)", err, clean.Log(relName))
} else if !fs.FileExists(fileName) {
// Do nothing.
} else if removeErr := os.Remove(fileName); removeErr != nil {
log.Errorf("album: %s (delete %s)", err, clean.Log(relName))
}
}

// PublishAlbumEvent(StatusDeleted, uid, c)

UpdateClientConfig()

// Update album YAML backup.
SaveAlbumYaml(a)

c.JSON(http.StatusOK, a)
})
}
Expand Down
43 changes: 34 additions & 9 deletions internal/api/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,24 +217,49 @@ func BatchAlbumsDelete(router *gin.RouterGroup) {
return
}

if len(f.Albums) == 0 {
// Get album UIDs.
albumUIDs := f.Albums

if len(albumUIDs) == 0 {
Abort(c, http.StatusBadRequest, i18n.ErrNoAlbumsSelected)
return
}

log.Infof("albums: deleting %s", clean.Log(f.String()))

// Soft delete albums, can be restored.
entity.Db().Where("album_uid IN (?)", f.Albums).Delete(&entity.Album{})
// Fetch albums.
albums, queryErr := query.AlbumsByUID(albumUIDs, false)

/*
KEEP ENTRIES AS ALBUMS MAY NOW BE RESTORED BY NAME
entity.Db().Where("album_uid IN (?)", f.Albums).Delete(&entity.PhotoAlbum{})
*/
if queryErr != nil {
log.Errorf("albums: %s (find)", queryErr)
}

UpdateClientConfig()
// Abort if no albums with a matching UID were found.
if len(albums) == 0 {
AbortEntityNotFound(c)
return
}

deleted := 0
conf := get.Config()

event.EntitiesDeleted("albums", f.Albums)
// Flag matching albums as deleted.
for _, a := range albums {
if deleteErr := a.Delete(); deleteErr != nil {
log.Errorf("albums: %s (delete)", deleteErr)
} else {
if conf.BackupAlbums() {
SaveAlbumYaml(a)
}

deleted++
}
}

// Update client config if at least one album was successfully deleted.
if deleted > 0 {
UpdateClientConfig()
}

c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgAlbumsDeleted))
})
Expand Down
10 changes: 5 additions & 5 deletions internal/api/photos.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,21 @@ import (
)

// SaveSidecarYaml saves the photo metadata to a YAML sidecar file.
func SaveSidecarYaml(p *entity.Photo) {
if p == nil {
func SaveSidecarYaml(photo *entity.Photo) {
if photo == nil {
log.Debugf("api: photo is nil (update yaml)")
return
}

c := get.Config()
conf := get.Config()

// Check if saving YAML sidecar files is enabled.
if !c.SidecarYaml() {
if !conf.SidecarYaml() {
return
}

// Write photo metadata to YAML sidecar file.
_ = p.SaveSidecarYaml(c.OriginalsPath(), c.SidecarPath())
_ = photo.SaveSidecarYaml(conf.OriginalsPath(), conf.SidecarPath())
}

// GetPhoto returns photo details as JSON.
Expand Down
14 changes: 12 additions & 2 deletions internal/entity/album.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func (m *Album) AfterUpdate(tx *gorm.DB) (err error) {
return
}

// AfterDelete flushes the album cache.
// AfterDelete flushes the album cache when an album gets deleted.
func (m *Album) AfterDelete(tx *gorm.DB) (err error) {
FlushAlbumCache()
return
Expand Down Expand Up @@ -410,6 +410,10 @@ func FindAlbum(find Album) *Album {

// HasID tests if the album has a valid id and uid.
func (m *Album) HasID() bool {
if m == nil {
return false
}

return m.ID > 0 && rnd.IsUID(m.AlbumUID, AlbumUID)
}

Expand Down Expand Up @@ -705,8 +709,14 @@ func (m *Album) Delete() error {
return nil
}

if err := Db().Delete(m).Error; err != nil {
now := TimeStamp()

if err := UnscopedDb().Model(m).UpdateColumns(Map{"updated_at": now, "deleted_at": now}).Error; err != nil {
return err
} else {
m.UpdatedAt = now
m.DeletedAt = &now
FlushAlbumCache()
}

m.PublishCountChange(-1)
Expand Down
4 changes: 4 additions & 0 deletions internal/entity/photo.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ func (m *Photo) GetID() uint {

// HasID checks if the photo has an id and uid assigned to it.
func (m *Photo) HasID() bool {
if m == nil {
return false
}

return m.ID > 0 && m.PhotoUID != ""
}

Expand Down
21 changes: 14 additions & 7 deletions internal/photoprism/backup_albums.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/photoprism/photoprism/pkg/fs"
)

var backupAlbumsLatest = time.Time{}
var backupAlbumsTime = time.Time{}
var backupAlbumsMutex = sync.Mutex{}

// BackupAlbums creates a YAML file backup of all albums.
Expand All @@ -18,6 +18,7 @@ func BackupAlbums(backupPath string, force bool) (count int, err error) {
backupAlbumsMutex.Lock()
defer backupAlbumsMutex.Unlock()

// Get albums from database.
albums, queryErr := query.Albums(0, 1000000)

if queryErr != nil {
Expand All @@ -33,20 +34,24 @@ func BackupAlbums(backupPath string, force bool) (count int, err error) {

var latest time.Time

// Ignore the last modification timestamp if the force flag is set.
if !force {
latest = backupAlbumsLatest
latest = backupAlbumsTime
}

// Save albums to YAML backup files.
for _, a := range albums {
// Album modification timestamp.
changed := a.UpdatedAt

// Skip albums that have already been saved to YAML backup files.
if !force && !backupAlbumsLatest.IsZero() && !a.UpdatedAt.IsZero() && !backupAlbumsLatest.Before(a.UpdatedAt) {
if !force && !backupAlbumsTime.IsZero() && !changed.IsZero() && !backupAlbumsTime.Before(changed) {
continue
}

// Remember most recent date.
if a.UpdatedAt.After(latest) {
latest = a.UpdatedAt
// Remember the lastest modification timestamp.
if changed.After(latest) {
latest = changed
}

// Write album metadata to YAML backup file.
Expand All @@ -57,7 +62,9 @@ func BackupAlbums(backupPath string, force bool) (count int, err error) {
}
}

backupAlbumsLatest = latest
// Set backupAlbumsTime to latest modification timestamp,
// so that already saved albums can be skipped next time.
backupAlbumsTime = latest

return count, err
}
10 changes: 10 additions & 0 deletions internal/query/albums.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ func Albums(offset, limit int) (results entity.Albums, err error) {
return results, err
}

// AlbumsByUID returns albums by UID.
func AlbumsByUID(albumUIDs []string, includeDeleted bool) (results entity.Albums, err error) {
if includeDeleted {
err = UnscopedDb().Where("album_uid IN (?)", albumUIDs).Find(&results).Error
} else {
err = UnscopedDb().Where("album_uid IN (?) AND deleted_at IS NULL", albumUIDs).Find(&results).Error
}
return results, err
}

// AlbumByUID returns a Album based on the UID.
func AlbumByUID(albumUID string) (album entity.Album, err error) {
if rnd.InvalidUID(albumUID, entity.AlbumUID) {
Expand Down
37 changes: 30 additions & 7 deletions internal/query/albums_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,36 +75,59 @@ func TestAlbumCoverByUID(t *testing.T) {
}

func TestUpdateAlbumDates(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
if err := UpdateAlbumDates(); err != nil {
t.Fatal(err)
}
})
}

func TestUpdateMissingAlbumEntries(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
if err := UpdateMissingAlbumEntries(); err != nil {
t.Fatal(err)
}
})
}

func TestAlbumEntryFound(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
if err := AlbumEntryFound("ps6sg6bexxvl0yh0"); err != nil {
t.Fatal(err)
}
})
}

func TestGetAlbums(t *testing.T) {
t.Run("success", func(t *testing.T) {
r, err := Albums(0, 3)
func TestAlbums(t *testing.T) {
t.Run("Success", func(t *testing.T) {
results, err := Albums(0, 3)

if err != nil {
t.Fatal(err)
}
assert.Equal(t, 3, len(r))

assert.Len(t, results, 3)
})
}

func TestAlbumsByUID(t *testing.T) {
t.Run("Success", func(t *testing.T) {
results, err := AlbumsByUID([]string{"as6sg6bxpogaaba7", "as6sg6bxpogaaba8"}, false)

if err != nil {
t.Fatal(err)
}

assert.Len(t, results, 2)
})

t.Run("IncludeDeleted", func(t *testing.T) {
results, err := AlbumsByUID([]string{"as6sg6bxpogaaba7", "as6sg6bxpogaaba8"}, true)

if err != nil {
t.Fatal(err)
}

assert.Len(t, results, 2)
})
}

0 comments on commit 4e7a3c7

Please sign in to comment.