2020-07-13 17:25:27 +02:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
2020-12-12 11:08:53 +01:00
|
|
|
"fmt"
|
2020-07-13 17:25:27 +02:00
|
|
|
"net/http"
|
2020-07-14 11:00:49 +02:00
|
|
|
"path/filepath"
|
2020-07-13 17:25:27 +02:00
|
|
|
|
2021-12-14 18:34:52 +01:00
|
|
|
"github.com/photoprism/photoprism/pkg/sanitize"
|
|
|
|
|
2020-07-13 17:25:27 +02:00
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/photoprism/photoprism/internal/acl"
|
|
|
|
"github.com/photoprism/photoprism/internal/entity"
|
|
|
|
"github.com/photoprism/photoprism/internal/event"
|
|
|
|
"github.com/photoprism/photoprism/internal/i18n"
|
|
|
|
"github.com/photoprism/photoprism/internal/photoprism"
|
|
|
|
"github.com/photoprism/photoprism/internal/query"
|
|
|
|
"github.com/photoprism/photoprism/internal/service"
|
|
|
|
)
|
|
|
|
|
2022-01-05 16:37:19 +01:00
|
|
|
// PhotoUnstack removes a file from an existing photo stack.
|
|
|
|
//
|
2020-07-13 17:25:27 +02:00
|
|
|
// POST /api/v1/photos/:uid/files/:file_uid/unstack
|
|
|
|
//
|
|
|
|
// Parameters:
|
|
|
|
// uid: string Photo UID as returned by the API
|
|
|
|
// file_uid: string File UID as returned by the API
|
2020-07-14 11:00:49 +02:00
|
|
|
func PhotoUnstack(router *gin.RouterGroup) {
|
2020-07-13 17:25:27 +02:00
|
|
|
router.POST("/photos/:uid/files/:file_uid/unstack", func(c *gin.Context) {
|
|
|
|
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
|
|
|
|
|
|
|
|
if s.Invalid() {
|
|
|
|
AbortUnauthorized(c)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-07-14 11:00:49 +02:00
|
|
|
conf := service.Config()
|
2021-12-14 18:34:52 +01:00
|
|
|
fileUID := sanitize.IdString(c.Param("file_uid"))
|
2020-07-13 17:25:27 +02:00
|
|
|
file, err := query.FileByUID(fileUID)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("photo: %s (unstack)", err)
|
|
|
|
AbortEntityNotFound(c)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if file.FilePrimary {
|
2022-01-05 11:40:44 +01:00
|
|
|
log.Errorf("photo: cannot unstack primary file")
|
2020-07-13 17:25:27 +02:00
|
|
|
AbortBadRequest(c)
|
|
|
|
return
|
2020-07-14 11:00:49 +02:00
|
|
|
} else if file.FileSidecar {
|
2022-01-05 11:40:44 +01:00
|
|
|
log.Errorf("photo: cannot unstack sidecar files")
|
2020-07-14 11:00:49 +02:00
|
|
|
AbortBadRequest(c)
|
|
|
|
return
|
|
|
|
} else if file.FileRoot != entity.RootOriginals {
|
|
|
|
log.Errorf("photo: only originals can be unstacked")
|
|
|
|
AbortBadRequest(c)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
fileName := photoprism.FileName(file.FileRoot, file.FileName)
|
|
|
|
baseName := filepath.Base(fileName)
|
|
|
|
|
2020-12-12 11:08:53 +01:00
|
|
|
unstackFile, err := photoprism.NewMediaFile(fileName)
|
2020-07-14 11:00:49 +02:00
|
|
|
|
|
|
|
if err != nil {
|
2021-12-14 20:01:39 +01:00
|
|
|
log.Errorf("photo: %s (unstack %s)", err, sanitize.Log(baseName))
|
2020-07-14 11:00:49 +02:00
|
|
|
AbortEntityNotFound(c)
|
|
|
|
return
|
2022-01-06 14:33:49 +01:00
|
|
|
} else if file.Photo == nil {
|
|
|
|
log.Errorf("photo: cannot find photo for file uid %s (unstack)", fileUID)
|
|
|
|
AbortEntityNotFound(c)
|
|
|
|
return
|
2020-07-14 11:00:49 +02:00
|
|
|
}
|
|
|
|
|
2020-12-12 11:08:53 +01:00
|
|
|
stackPhoto := *file.Photo
|
|
|
|
stackPrimary, err := stackPhoto.PrimaryFile()
|
|
|
|
|
|
|
|
if err != nil {
|
2022-01-05 11:40:44 +01:00
|
|
|
log.Errorf("photo: cannot find primary file for %s (unstack)", sanitize.Log(baseName))
|
2020-12-12 11:08:53 +01:00
|
|
|
AbortUnexpected(c)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-06 14:33:49 +01:00
|
|
|
// Flag original photo as unstacked / not stackable.
|
|
|
|
stackPhoto.SetStack(entity.IsUnstacked)
|
|
|
|
|
2020-12-12 11:08:53 +01:00
|
|
|
related, err := unstackFile.RelatedFiles(false)
|
2020-07-14 11:00:49 +02:00
|
|
|
|
|
|
|
if err != nil {
|
2021-12-14 20:01:39 +01:00
|
|
|
log.Errorf("photo: %s (unstack %s)", err, sanitize.Log(baseName))
|
2020-07-14 11:00:49 +02:00
|
|
|
AbortEntityNotFound(c)
|
|
|
|
return
|
|
|
|
} else if related.Len() == 0 {
|
2021-12-14 20:01:39 +01:00
|
|
|
log.Errorf("photo: found no files for %s (unstack)", sanitize.Log(baseName))
|
2020-12-12 11:08:53 +01:00
|
|
|
AbortEntityNotFound(c)
|
|
|
|
return
|
|
|
|
} else if related.Main == nil {
|
2021-12-14 20:01:39 +01:00
|
|
|
log.Errorf("photo: found no main file for %s (unstack)", sanitize.Log(baseName))
|
2020-07-14 11:00:49 +02:00
|
|
|
AbortEntityNotFound(c)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-12-12 13:05:58 +01:00
|
|
|
var files photoprism.MediaFiles
|
2020-12-12 11:08:53 +01:00
|
|
|
unstackSingle := false
|
|
|
|
|
|
|
|
if unstackFile.BasePrefix(false) == stackPhoto.PhotoName {
|
2020-07-14 11:00:49 +02:00
|
|
|
if conf.ReadOnly() {
|
2022-01-05 11:40:44 +01:00
|
|
|
log.Errorf("photo: cannot rename files in read only mode (unstack %s)", sanitize.Log(baseName))
|
2020-07-14 11:00:49 +02:00
|
|
|
AbortFeatureDisabled(c)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-12-12 13:05:58 +01:00
|
|
|
destName := fmt.Sprintf("%s.%s%s", unstackFile.AbsPrefix(false), unstackFile.Checksum(), unstackFile.Extension())
|
|
|
|
|
|
|
|
if err := unstackFile.Move(destName); err != nil {
|
2022-01-05 11:40:44 +01:00
|
|
|
log.Errorf("photo: cannot rename %s to %s (unstack)", sanitize.Log(unstackFile.BaseName()), sanitize.Log(filepath.Base(destName)))
|
2020-12-12 13:05:58 +01:00
|
|
|
AbortUnexpected(c)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
files = append(files, unstackFile)
|
2020-12-12 11:08:53 +01:00
|
|
|
unstackSingle = true
|
2020-12-12 13:05:58 +01:00
|
|
|
} else {
|
|
|
|
files = related.Files
|
2020-07-13 17:25:27 +02:00
|
|
|
}
|
|
|
|
|
2022-01-06 14:33:49 +01:00
|
|
|
// Create new photo, also flagged as unstacked / not stackable.
|
2020-12-19 19:15:32 +01:00
|
|
|
newPhoto := entity.NewPhoto(false)
|
2020-12-12 13:05:58 +01:00
|
|
|
newPhoto.PhotoPath = unstackFile.RootRelPath()
|
|
|
|
newPhoto.PhotoName = unstackFile.BasePrefix(false)
|
2020-07-13 17:25:27 +02:00
|
|
|
|
|
|
|
if err := newPhoto.Create(); err != nil {
|
2021-12-14 20:01:39 +01:00
|
|
|
log.Errorf("photo: %s (unstack %s)", err.Error(), sanitize.Log(baseName))
|
2020-07-13 17:25:27 +02:00
|
|
|
AbortSaveFailed(c)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-12-12 13:05:58 +01:00
|
|
|
for _, r := range files {
|
2020-12-12 11:08:53 +01:00
|
|
|
relName := r.RootRelName()
|
|
|
|
relRoot := r.Root()
|
2020-07-13 17:25:27 +02:00
|
|
|
|
2020-12-12 11:08:53 +01:00
|
|
|
if unstackSingle {
|
2020-12-12 13:05:58 +01:00
|
|
|
relName = file.FileName
|
|
|
|
relRoot = file.FileRoot
|
2020-07-14 11:00:49 +02:00
|
|
|
}
|
|
|
|
|
2020-12-12 11:08:53 +01:00
|
|
|
if err := entity.UnscopedDb().Exec(`UPDATE files
|
|
|
|
SET photo_id = ?, photo_uid = ?, file_name = ?, file_missing = 0
|
|
|
|
WHERE file_name = ? AND file_root = ?`,
|
|
|
|
newPhoto.ID, newPhoto.PhotoUID, r.RootRelName(),
|
|
|
|
relName, relRoot).Error; err != nil {
|
|
|
|
// Handle error...
|
2021-12-14 20:01:39 +01:00
|
|
|
log.Errorf("photo: %s (unstack %s)", err.Error(), sanitize.Log(r.BaseName()))
|
2020-12-12 11:08:53 +01:00
|
|
|
|
2021-10-05 22:44:27 +02:00
|
|
|
// Remove new photo from index.
|
2021-09-24 13:13:59 +02:00
|
|
|
if _, err := newPhoto.Delete(true); err != nil {
|
2021-12-14 20:01:39 +01:00
|
|
|
log.Errorf("photo: %s (unstack %s)", err.Error(), sanitize.Log(r.BaseName()))
|
2020-12-12 11:08:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Revert file rename.
|
2020-12-12 13:05:58 +01:00
|
|
|
if unstackSingle {
|
|
|
|
if err := r.Move(photoprism.FileName(relRoot, relName)); err != nil {
|
2021-12-14 20:01:39 +01:00
|
|
|
log.Errorf("photo: %s (unstack %s)", err.Error(), sanitize.Log(r.BaseName()))
|
2020-12-12 13:05:58 +01:00
|
|
|
}
|
2020-12-12 11:08:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
AbortSaveFailed(c)
|
|
|
|
return
|
|
|
|
}
|
2020-07-13 17:25:27 +02:00
|
|
|
}
|
|
|
|
|
2020-07-14 11:00:49 +02:00
|
|
|
ind := service.Index()
|
2020-07-13 17:25:27 +02:00
|
|
|
|
2020-12-12 11:08:53 +01:00
|
|
|
// Index unstacked files.
|
|
|
|
if res := ind.FileName(unstackFile.FileName(), photoprism.IndexOptionsSingle()); res.Failed() {
|
2021-12-14 20:01:39 +01:00
|
|
|
log.Errorf("photo: %s (unstack %s)", res.Err, sanitize.Log(baseName))
|
2020-07-14 11:00:49 +02:00
|
|
|
AbortSaveFailed(c)
|
|
|
|
return
|
|
|
|
}
|
2020-07-13 17:25:27 +02:00
|
|
|
|
2020-12-12 11:08:53 +01:00
|
|
|
// Reset type for existing photo stack to image.
|
|
|
|
if err := stackPhoto.Update("PhotoType", entity.TypeImage); err != nil {
|
2021-12-14 20:01:39 +01:00
|
|
|
log.Errorf("photo: %s (unstack %s)", err, sanitize.Log(baseName))
|
2020-07-14 11:00:49 +02:00
|
|
|
AbortUnexpected(c)
|
2020-07-13 17:25:27 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-12-12 11:08:53 +01:00
|
|
|
// Re-index existing photo stack.
|
2020-12-19 19:15:32 +01:00
|
|
|
if res := ind.FileName(photoprism.FileName(stackPrimary.FileRoot, stackPrimary.FileName), photoprism.IndexOptionsSingle()); res.Failed() {
|
2021-12-14 20:01:39 +01:00
|
|
|
log.Errorf("photo: %s (unstack %s)", res.Err, sanitize.Log(baseName))
|
2020-07-13 17:25:27 +02:00
|
|
|
AbortSaveFailed(c)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-07-14 11:00:49 +02:00
|
|
|
// Notify clients by publishing events.
|
2020-12-12 11:08:53 +01:00
|
|
|
PublishPhotoEvent(EntityCreated, newPhoto.PhotoUID, c)
|
|
|
|
PublishPhotoEvent(EntityUpdated, stackPhoto.PhotoUID, c)
|
2020-07-13 17:25:27 +02:00
|
|
|
|
|
|
|
event.SuccessMsg(i18n.MsgFileUnstacked)
|
|
|
|
|
2020-12-12 11:08:53 +01:00
|
|
|
p, err := query.PhotoPreloadByUID(stackPhoto.PhotoUID)
|
2020-07-13 17:25:27 +02:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
AbortEntityNotFound(c)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, p)
|
|
|
|
})
|
|
|
|
}
|