Backup: Restore archive flag from yaml files #912

This commit is contained in:
Michael Mayer 2021-02-05 16:32:08 +01:00
parent c4bb9e8314
commit bf592bdf7c
5 changed files with 177 additions and 92 deletions

View file

@ -38,18 +38,30 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
return return
} }
log.Infof("archive: adding %s", f.String()) log.Infof("photos: archiving %s", f.String())
// Soft delete by setting deleted_at to current date. if service.Config().BackupYaml() {
err := entity.Db().Where("photo_uid IN (?)", f.Photos).Delete(&entity.Photo{}).Error photos, err := query.PhotoSelection(f)
if err != nil { if err != nil {
AbortSaveFailed(c) AbortEntityNotFound(c)
return return
} }
// Remove archived photos from albums. for _, p := range photos {
logError("archive", entity.Db().Model(&entity.PhotoAlbum{}).Where("photo_uid IN (?)", f.Photos).UpdateColumn("hidden", true).Error) if err := p.Archive(); err != nil {
log.Errorf("archive: %s", err)
} else {
SavePhotoAsYaml(p)
}
}
} else if err := entity.Db().Where("photo_uid IN (?)", f.Photos).Delete(&entity.Photo{}).Error; err != nil {
log.Errorf("archive: %s", err)
AbortSaveFailed(c)
return
} else if err := entity.Db().Model(&entity.PhotoAlbum{}).Where("photo_uid IN (?)", f.Photos).UpdateColumn("hidden", true).Error; err != nil {
log.Errorf("archive: %s", err)
}
if err := entity.UpdatePhotoCounts(); err != nil { if err := entity.UpdatePhotoCounts(); err != nil {
log.Errorf("photos: %s", err) log.Errorf("photos: %s", err)
@ -63,6 +75,64 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
}) })
} }
// POST /api/v1/batch/photos/restore
func BatchPhotosRestore(router *gin.RouterGroup) {
router.POST("/batch/photos/restore", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionDelete)
if s.Invalid() {
AbortUnauthorized(c)
return
}
var f form.Selection
if err := c.BindJSON(&f); err != nil {
AbortBadRequest(c)
return
}
if len(f.Photos) == 0 {
Abort(c, http.StatusBadRequest, i18n.ErrNoItemsSelected)
return
}
log.Infof("photos: restoring %s", f.String())
if service.Config().BackupYaml() {
photos, err := query.PhotoSelection(f)
if err != nil {
AbortEntityNotFound(c)
return
}
for _, p := range photos {
if err := p.Restore(); err != nil {
log.Errorf("restore: %s", err)
} else {
SavePhotoAsYaml(p)
}
}
} else if err := entity.Db().Unscoped().Model(&entity.Photo{}).Where("photo_uid IN (?)", f.Photos).
UpdateColumn("deleted_at", gorm.Expr("NULL")).Error; err != nil {
log.Errorf("restore: %s", err)
AbortSaveFailed(c)
return
}
if err := entity.UpdatePhotoCounts(); err != nil {
log.Errorf("photos: %s", err)
}
UpdateClientConfig()
event.EntitiesRestored("photos", f.Photos)
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgSelectionRestored))
})
}
// POST /api/v1/batch/photos/approve // POST /api/v1/batch/photos/approve
func BatchPhotosApprove(router *gin.RouterGroup) { func BatchPhotosApprove(router *gin.RouterGroup) {
router.POST("batch/photos/approve", func(c *gin.Context) { router.POST("batch/photos/approve", func(c *gin.Context) {
@ -98,7 +168,7 @@ func BatchPhotosApprove(router *gin.RouterGroup) {
for _, p := range photos { for _, p := range photos {
if err := p.Approve(); err != nil { if err := p.Approve(); err != nil {
log.Errorf("photo: %s (approve)", err.Error()) log.Errorf("approve: %s", err)
} else { } else {
approved = append(approved, p) approved = append(approved, p)
SavePhotoAsYaml(p) SavePhotoAsYaml(p)
@ -113,50 +183,6 @@ func BatchPhotosApprove(router *gin.RouterGroup) {
}) })
} }
// POST /api/v1/batch/photos/restore
func BatchPhotosRestore(router *gin.RouterGroup) {
router.POST("/batch/photos/restore", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionDelete)
if s.Invalid() {
AbortUnauthorized(c)
return
}
var f form.Selection
if err := c.BindJSON(&f); err != nil {
AbortBadRequest(c)
return
}
if len(f.Photos) == 0 {
Abort(c, http.StatusBadRequest, i18n.ErrNoItemsSelected)
return
}
log.Infof("archive: restoring %s", f.String())
err := entity.Db().Unscoped().Model(&entity.Photo{}).Where("photo_uid IN (?)", f.Photos).
UpdateColumn("deleted_at", gorm.Expr("NULL")).Error
if err != nil {
AbortSaveFailed(c)
return
}
if err := entity.UpdatePhotoCounts(); err != nil {
log.Errorf("photos: %s", err)
}
UpdateClientConfig()
event.EntitiesRestored("photos", f.Photos)
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgSelectionRestored))
})
}
// POST /api/v1/batch/albums/delete // POST /api/v1/batch/albums/delete
func BatchAlbumsDelete(router *gin.RouterGroup) { func BatchAlbumsDelete(router *gin.RouterGroup) {
router.POST("/batch/albums/delete", func(c *gin.Context) { router.POST("/batch/albums/delete", func(c *gin.Context) {
@ -214,12 +240,11 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
return return
} }
log.Infof("photos: mark %s as private", f.String()) log.Infof("photos: updating private flag for %s", f.String())
err := entity.Db().Model(entity.Photo{}).Where("photo_uid IN (?)", f.Photos).UpdateColumn("photo_private", if err := entity.Db().Model(entity.Photo{}).Where("photo_uid IN (?)", f.Photos).UpdateColumn("photo_private",
gorm.Expr("CASE WHEN photo_private > 0 THEN 0 ELSE 1 END")).Error gorm.Expr("CASE WHEN photo_private > 0 THEN 0 ELSE 1 END")).Error; err != nil {
log.Errorf("private: %s", err)
if err != nil {
AbortSaveFailed(c) AbortSaveFailed(c)
return return
} }
@ -228,8 +253,12 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
log.Errorf("photos: %s", err) log.Errorf("photos: %s", err)
} }
if entities, err := query.PhotoSelection(f); err == nil { if photos, err := query.PhotoSelection(f); err == nil {
event.EntitiesUpdated("photos", entities) for _, p := range photos {
SavePhotoAsYaml(p)
}
event.EntitiesUpdated("photos", photos)
} }
UpdateClientConfig() UpdateClientConfig()
@ -313,7 +342,7 @@ func BatchPhotosDelete(router *gin.RouterGroup) {
return return
} }
log.Infof("archive: permanently deleting %s", f.String()) log.Infof("photos: deleting %s", f.String())
photos, err := query.PhotoSelection(f) photos, err := query.PhotoSelection(f)
@ -327,7 +356,7 @@ func BatchPhotosDelete(router *gin.RouterGroup) {
// Delete photos. // Delete photos.
for _, p := range photos { for _, p := range photos {
if err := photoprism.Delete(p); err != nil { if err := photoprism.Delete(p); err != nil {
log.Errorf("photo: %s (delete)", err.Error()) log.Errorf("delete: %s", err)
} else { } else {
deleted = append(deleted, p) deleted = append(deleted, p)
} }

View file

@ -62,7 +62,7 @@ type Photo struct {
PhotoPrivate bool `json:"Private" yaml:"Private,omitempty"` PhotoPrivate bool `json:"Private" yaml:"Private,omitempty"`
PhotoScan bool `json:"Scan" yaml:"Scan,omitempty"` PhotoScan bool `json:"Scan" yaml:"Scan,omitempty"`
PhotoPanorama bool `json:"Panorama" yaml:"Panorama,omitempty"` PhotoPanorama bool `json:"Panorama" yaml:"Panorama,omitempty"`
TimeZone string `gorm:"type:VARBINARY(64);" json:"TimeZone" yaml:"-"` TimeZone string `gorm:"type:VARBINARY(64);" json:"TimeZone" yaml:"TimeZone,omitempty"`
PlaceID string `gorm:"type:VARBINARY(42);index;default:'zz'" json:"PlaceID" yaml:"-"` PlaceID string `gorm:"type:VARBINARY(42);index;default:'zz'" json:"PlaceID" yaml:"-"`
PlaceSrc string `gorm:"type:VARBINARY(8);" json:"PlaceSrc" yaml:"PlaceSrc,omitempty"` PlaceSrc string `gorm:"type:VARBINARY(8);" json:"PlaceSrc" yaml:"PlaceSrc,omitempty"`
CellID string `gorm:"type:VARBINARY(42);index;default:'zz'" json:"CellID" yaml:"-"` CellID string `gorm:"type:VARBINARY(42);index;default:'zz'" json:"CellID" yaml:"-"`
@ -78,7 +78,7 @@ type Photo struct {
PhotoExposure string `gorm:"type:VARBINARY(64);" json:"Exposure" yaml:"Exposure,omitempty"` PhotoExposure string `gorm:"type:VARBINARY(64);" json:"Exposure" yaml:"Exposure,omitempty"`
PhotoFNumber float32 `gorm:"type:FLOAT;" json:"FNumber" yaml:"FNumber,omitempty"` PhotoFNumber float32 `gorm:"type:FLOAT;" json:"FNumber" yaml:"FNumber,omitempty"`
PhotoFocalLength int `json:"FocalLength" yaml:"FocalLength,omitempty"` PhotoFocalLength int `json:"FocalLength" yaml:"FocalLength,omitempty"`
PhotoQuality int `gorm:"type:SMALLINT" json:"Quality" yaml:"-"` PhotoQuality int `gorm:"type:SMALLINT" json:"Quality" yaml:"Quality,omitempty"`
PhotoResolution int `gorm:"type:SMALLINT" json:"Resolution" yaml:"-"` PhotoResolution int `gorm:"type:SMALLINT" json:"Resolution" yaml:"-"`
PhotoColor uint8 `json:"Color" yaml:"-"` PhotoColor uint8 `json:"Color" yaml:"-"`
CameraID uint `gorm:"index:idx_photos_camera_lens;default:1" json:"CameraID" yaml:"-"` CameraID uint `gorm:"index:idx_photos_camera_lens;default:1" json:"CameraID" yaml:"-"`
@ -238,13 +238,7 @@ func (m *Photo) Save() error {
photoMutex.Lock() photoMutex.Lock()
defer photoMutex.Unlock() defer photoMutex.Unlock()
if err := UnscopedDb().Save(m).Error; err == nil { if err := Save(m, "ID"); err == nil {
// Nothing to do.
} else if !strings.Contains(strings.ToLower(err.Error()), "lock") {
log.Debugf("photo: %s (save %s)", err, m.PhotoUID)
return err
} else if err := UnscopedDb().Save(m).Error; err != nil {
log.Debugf("photo: %s (save %s after deadlock)", err, m.PhotoUID)
return err return err
} }
@ -454,17 +448,6 @@ func (m *Photo) PreloadFiles() {
logError(q.Scan(&m.Files)) logError(q.Scan(&m.Files))
} }
/* func (m *Photo) PreloadLabels() {
q := Db().NewScope(nil).DB().
Table("labels").
Select(`labels.*`).
Joins("JOIN photos_labels ON photos_labels.label_id = labels.id AND photos_labels.photo_id = ?", m.ID).
Where("labels.deleted_at IS NULL").
Order("labels.label_name ASC")
logError(q.Scan(&m.Labels))
} */
// PreloadKeywords prepares gorm scope to retrieve photo keywords // PreloadKeywords prepares gorm scope to retrieve photo keywords
func (m *Photo) PreloadKeywords() { func (m *Photo) PreloadKeywords() {
q := Db().NewScope(nil).DB(). q := Db().NewScope(nil).DB().
@ -491,7 +474,6 @@ func (m *Photo) PreloadAlbums() {
// PreloadMany prepares gorm scope to retrieve photo file, albums and keywords // PreloadMany prepares gorm scope to retrieve photo file, albums and keywords
func (m *Photo) PreloadMany() { func (m *Photo) PreloadMany() {
m.PreloadFiles() m.PreloadFiles()
// m.PreloadLabels()
m.PreloadKeywords() m.PreloadKeywords()
m.PreloadAlbums() m.PreloadAlbums()
} }
@ -980,6 +962,32 @@ func (m *Photo) AllFiles() (files Files) {
return files return files
} }
// Archive removes the photo from albums and flags it as archived (soft delete).
func (m *Photo) Archive() error {
deletedAt := Timestamp()
if err := Db().Model(&PhotoAlbum{}).Where("photo_uid = ?", m.PhotoUID).UpdateColumn("hidden", true).Error; err != nil {
return err
} else if err := m.Update("deleted_at", deletedAt); err != nil {
return err
}
m.DeletedAt = &deletedAt
return nil
}
// Restore removes the archive flag (undo soft delete).
func (m *Photo) Restore() error {
if err := m.Update("deleted_at", gorm.Expr("NULL")); err != nil {
return err
}
m.DeletedAt = nil
return nil
}
// Delete deletes the entity from the database. // Delete deletes the entity from the database.
func (m *Photo) Delete(permanently bool) error { func (m *Photo) Delete(permanently bool) error {
if permanently { if permanently {

View file

@ -14,6 +14,9 @@ var photoYamlMutex = sync.Mutex{}
// Yaml returns photo data as YAML string. // Yaml returns photo data as YAML string.
func (m *Photo) Yaml() ([]byte, error) { func (m *Photo) Yaml() ([]byte, error) {
// Load details if not done yet.
m.GetDetails()
out, err := yaml.Marshal(m) out, err := yaml.Marshal(m)
if err != nil { if err != nil {

55
internal/entity/save.go Normal file
View file

@ -0,0 +1,55 @@
package entity
import (
"fmt"
"reflect"
"strings"
)
// Save updates an entity in the database, or inserts if it doesn't exist.
func Save(m interface{}, primaryKeys ...string) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("save: %s (panic)", r)
}
}()
if err := Update(m, primaryKeys...); err == nil {
return nil
} else if err := UnscopedDb().Save(m).Error; err == nil {
return nil
} else if !strings.Contains(strings.ToLower(err.Error()), "lock") {
return err
} else if err := UnscopedDb().Save(m).Error; err != nil {
return err
}
return nil
}
// Update updates an existing entity in the database.
func Update(m interface{}, primaryKeys ...string) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("update: %s (panic)", r)
}
}()
v := reflect.ValueOf(m).Elem()
for _, k := range primaryKeys {
if field := v.FieldByName(k); field.IsZero() {
return fmt.Errorf("key '%s' not found", k)
}
}
if res := UnscopedDb().Model(m).Omit(primaryKeys...).Updates(m); res.Error != nil {
return res.Error
} else if res.RowsAffected == 0 {
return fmt.Errorf("no entity found for updating")
} else if res.RowsAffected > 1 {
log.Warnf("update: more than one row affected - bug?")
}
return nil
}

View file

@ -285,16 +285,6 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
if photo.PhotoQuality == -1 && (file.FilePrimary || fileChanged) { if photo.PhotoQuality == -1 && (file.FilePrimary || fileChanged) {
// Restore photos that have been purged automatically. // Restore photos that have been purged automatically.
photo.DeletedAt = nil photo.DeletedAt = nil
} else if photo.DeletedAt != nil {
// Don't waste time indexing deleted / archived photos.
result.Status = IndexArchived
// Remove missing flag from file.
if err = file.Undelete(); err != nil {
log.Errorf("index: %s in %s (undelete)", err.Error(), logName)
}
return result
} }
// Handle file types. // Handle file types.