Backup: Restore archive flag from yaml files #912
This commit is contained in:
parent
c4bb9e8314
commit
bf592bdf7c
5 changed files with 177 additions and 92 deletions
|
@ -38,19 +38,31 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
log.Infof("archive: adding %s", f.String())
|
||||
log.Infof("photos: archiving %s", f.String())
|
||||
|
||||
// Soft delete by setting deleted_at to current date.
|
||||
err := entity.Db().Where("photo_uid IN (?)", f.Photos).Delete(&entity.Photo{}).Error
|
||||
if service.Config().BackupYaml() {
|
||||
photos, err := query.PhotoSelection(f)
|
||||
|
||||
if err != nil {
|
||||
if err != nil {
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
for _, p := range photos {
|
||||
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)
|
||||
}
|
||||
|
||||
// Remove archived photos from albums.
|
||||
logError("archive", entity.Db().Model(&entity.PhotoAlbum{}).Where("photo_uid IN (?)", f.Photos).UpdateColumn("hidden", true).Error)
|
||||
|
||||
if err := entity.UpdatePhotoCounts(); err != nil {
|
||||
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
|
||||
func BatchPhotosApprove(router *gin.RouterGroup) {
|
||||
router.POST("batch/photos/approve", func(c *gin.Context) {
|
||||
|
@ -98,7 +168,7 @@ func BatchPhotosApprove(router *gin.RouterGroup) {
|
|||
|
||||
for _, p := range photos {
|
||||
if err := p.Approve(); err != nil {
|
||||
log.Errorf("photo: %s (approve)", err.Error())
|
||||
log.Errorf("approve: %s", err)
|
||||
} else {
|
||||
approved = append(approved, 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
|
||||
func BatchAlbumsDelete(router *gin.RouterGroup) {
|
||||
router.POST("/batch/albums/delete", func(c *gin.Context) {
|
||||
|
@ -214,12 +240,11 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
|
|||
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",
|
||||
gorm.Expr("CASE WHEN photo_private > 0 THEN 0 ELSE 1 END")).Error
|
||||
|
||||
if err != nil {
|
||||
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; err != nil {
|
||||
log.Errorf("private: %s", err)
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
@ -228,8 +253,12 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
|
|||
log.Errorf("photos: %s", err)
|
||||
}
|
||||
|
||||
if entities, err := query.PhotoSelection(f); err == nil {
|
||||
event.EntitiesUpdated("photos", entities)
|
||||
if photos, err := query.PhotoSelection(f); err == nil {
|
||||
for _, p := range photos {
|
||||
SavePhotoAsYaml(p)
|
||||
}
|
||||
|
||||
event.EntitiesUpdated("photos", photos)
|
||||
}
|
||||
|
||||
UpdateClientConfig()
|
||||
|
@ -313,7 +342,7 @@ func BatchPhotosDelete(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
log.Infof("archive: permanently deleting %s", f.String())
|
||||
log.Infof("photos: deleting %s", f.String())
|
||||
|
||||
photos, err := query.PhotoSelection(f)
|
||||
|
||||
|
@ -327,7 +356,7 @@ func BatchPhotosDelete(router *gin.RouterGroup) {
|
|||
// Delete photos.
|
||||
for _, p := range photos {
|
||||
if err := photoprism.Delete(p); err != nil {
|
||||
log.Errorf("photo: %s (delete)", err.Error())
|
||||
log.Errorf("delete: %s", err)
|
||||
} else {
|
||||
deleted = append(deleted, p)
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ type Photo struct {
|
|||
PhotoPrivate bool `json:"Private" yaml:"Private,omitempty"`
|
||||
PhotoScan bool `json:"Scan" yaml:"Scan,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:"-"`
|
||||
PlaceSrc string `gorm:"type:VARBINARY(8);" json:"PlaceSrc" yaml:"PlaceSrc,omitempty"`
|
||||
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"`
|
||||
PhotoFNumber float32 `gorm:"type:FLOAT;" json:"FNumber" yaml:"FNumber,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:"-"`
|
||||
PhotoColor uint8 `json:"Color" 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()
|
||||
defer photoMutex.Unlock()
|
||||
|
||||
if err := UnscopedDb().Save(m).Error; 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)
|
||||
if err := Save(m, "ID"); err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -454,17 +448,6 @@ func (m *Photo) PreloadFiles() {
|
|||
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
|
||||
func (m *Photo) PreloadKeywords() {
|
||||
q := Db().NewScope(nil).DB().
|
||||
|
@ -491,7 +474,6 @@ func (m *Photo) PreloadAlbums() {
|
|||
// PreloadMany prepares gorm scope to retrieve photo file, albums and keywords
|
||||
func (m *Photo) PreloadMany() {
|
||||
m.PreloadFiles()
|
||||
// m.PreloadLabels()
|
||||
m.PreloadKeywords()
|
||||
m.PreloadAlbums()
|
||||
}
|
||||
|
@ -980,6 +962,32 @@ func (m *Photo) AllFiles() (files 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.
|
||||
func (m *Photo) Delete(permanently bool) error {
|
||||
if permanently {
|
||||
|
|
|
@ -14,6 +14,9 @@ var photoYamlMutex = sync.Mutex{}
|
|||
|
||||
// Yaml returns photo data as YAML string.
|
||||
func (m *Photo) Yaml() ([]byte, error) {
|
||||
// Load details if not done yet.
|
||||
m.GetDetails()
|
||||
|
||||
out, err := yaml.Marshal(m)
|
||||
|
||||
if err != nil {
|
||||
|
|
55
internal/entity/save.go
Normal file
55
internal/entity/save.go
Normal 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
|
||||
}
|
|
@ -285,16 +285,6 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
if photo.PhotoQuality == -1 && (file.FilePrimary || fileChanged) {
|
||||
// Restore photos that have been purged automatically.
|
||||
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.
|
||||
|
|
Loading…
Reference in a new issue