photoprism/internal/api/photo.go
Michael Mayer 7940786ce9 UX: Refactor config options to be more intuitive
Make sure to update your local config files when upgrading as
the name of some config values has changed. The default
config path has changed from "settings" to "config".
2020-12-18 09:11:42 +01:00

334 lines
7.1 KiB
Go

package api
import (
"fmt"
"net/http"
"path/filepath"
"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/form"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
)
// SavePhotoAsYaml saves photo data as YAML file.
func SavePhotoAsYaml(p entity.Photo) {
c := service.Config()
// Write YAML sidecar file (optional).
if !c.BackupYaml() {
return
}
fileName := p.YamlFileName(c.OriginalsPath(), c.SidecarPath())
if err := p.SaveAsYaml(fileName); err != nil {
log.Errorf("photo: %s (update yaml)", err)
} else {
log.Debugf("photo: updated yaml file %s", txt.Quote(filepath.Base(fileName)))
}
}
// GET /api/v1/photos/:uid
//
// Parameters:
// uid: string PhotoUID as returned by the API
func GetPhoto(router *gin.RouterGroup) {
router.GET("/photos/:uid", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionRead)
if s.Invalid() {
AbortUnauthorized(c)
return
}
p, err := query.PhotoPreloadByUID(c.Param("uid"))
if err != nil {
AbortEntityNotFound(c)
return
}
c.IndentedJSON(http.StatusOK, p)
})
}
// PUT /api/v1/photos/:uid
func UpdatePhoto(router *gin.RouterGroup) {
router.PUT("/photos/:uid", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
if s.Invalid() {
AbortUnauthorized(c)
return
}
uid := c.Param("uid")
m, err := query.PhotoByUID(uid)
if err != nil {
AbortEntityNotFound(c)
return
}
// TODO: Proof-of-concept for form handling - might need refactoring
// 1) Init form with model values
f, err := form.NewPhoto(m)
if err != nil {
Abort(c, http.StatusInternalServerError, i18n.ErrSaveFailed)
return
}
// 2) Update form with values from request
if err := c.BindJSON(&f); err != nil {
Abort(c, http.StatusBadRequest, i18n.ErrBadRequest)
return
}
// 3) Save model with values from form
if err := entity.SavePhotoForm(m, f); err != nil {
Abort(c, http.StatusInternalServerError, i18n.ErrSaveFailed)
return
}
PublishPhotoEvent(EntityUpdated, uid, c)
event.SuccessMsg(i18n.MsgChangesSaved)
p, err := query.PhotoPreloadByUID(uid)
if err != nil {
AbortEntityNotFound(c)
return
}
SavePhotoAsYaml(p)
c.JSON(http.StatusOK, p)
})
}
// GET /api/v1/photos/:uid/dl
//
// Parameters:
// uid: string PhotoUID as returned by the API
func GetPhotoDownload(router *gin.RouterGroup) {
router.GET("/photos/:uid/dl", func(c *gin.Context) {
if InvalidDownloadToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
f, err := query.FileByPhotoUID(c.Param("uid"))
if err != nil {
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
return
}
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
log.Errorf("photo: file %s is missing", txt.Quote(f.FileName))
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore.
logError("photo", f.Update("FileMissing", true))
return
}
downloadFileName := f.ShareBase()
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
c.File(fileName)
})
}
// GET /api/v1/photos/:uid/yaml
//
// Parameters:
// uid: string PhotoUID as returned by the API
func GetPhotoYaml(router *gin.RouterGroup) {
router.GET("/photos/:uid/yaml", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionExport)
if s.Invalid() {
AbortUnauthorized(c)
return
}
p, err := query.PhotoPreloadByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatus(http.StatusNotFound)
return
}
data, err := p.Yaml()
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
if c.Query("download") != "" {
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", c.Param("uid")+fs.YamlExt))
}
c.Data(http.StatusOK, "text/x-yaml; charset=utf-8", data)
})
}
// POST /api/v1/photos/:uid/approve
//
// Parameters:
// uid: string PhotoUID as returned by the API
func ApprovePhoto(router *gin.RouterGroup) {
router.POST("/photos/:uid/approve", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
if s.Invalid() {
AbortUnauthorized(c)
return
}
id := c.Param("uid")
m, err := query.PhotoByUID(id)
if err != nil {
AbortEntityNotFound(c)
return
}
if err := m.Approve(); err != nil {
log.Errorf("photo: %s", err.Error())
AbortSaveFailed(c)
return
}
SavePhotoAsYaml(m)
PublishPhotoEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, gin.H{"photo": m})
})
}
// POST /api/v1/photos/:uid/like
//
// Parameters:
// uid: string PhotoUID as returned by the API
func LikePhoto(router *gin.RouterGroup) {
router.POST("/photos/:uid/like", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionLike)
if s.Invalid() {
AbortUnauthorized(c)
return
}
id := c.Param("uid")
m, err := query.PhotoByUID(id)
if err != nil {
AbortEntityNotFound(c)
return
}
if err := m.SetFavorite(true); err != nil {
log.Errorf("photo: %s", err.Error())
AbortSaveFailed(c)
return
}
SavePhotoAsYaml(m)
PublishPhotoEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, gin.H{"photo": m})
})
}
// DELETE /api/v1/photos/:uid/like
//
// Parameters:
// uid: string PhotoUID as returned by the API
func DislikePhoto(router *gin.RouterGroup) {
router.DELETE("/photos/:uid/like", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionLike)
if s.Invalid() {
AbortUnauthorized(c)
return
}
id := c.Param("uid")
m, err := query.PhotoByUID(id)
if err != nil {
AbortEntityNotFound(c)
return
}
if err := m.SetFavorite(false); err != nil {
log.Errorf("photo: %s", err.Error())
AbortSaveFailed(c)
return
}
SavePhotoAsYaml(m)
PublishPhotoEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, gin.H{"photo": m})
})
}
// POST /api/v1/photos/:uid/files/:file_uid/primary
//
// Parameters:
// uid: string PhotoUID as returned by the API
// file_uid: string File UID as returned by the API
func PhotoPrimary(router *gin.RouterGroup) {
router.POST("/photos/:uid/files/:file_uid/primary", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionUpdate)
if s.Invalid() {
AbortUnauthorized(c)
return
}
uid := c.Param("uid")
fileUID := c.Param("file_uid")
err := query.SetPhotoPrimary(uid, fileUID)
if err != nil {
AbortEntityNotFound(c)
return
}
PublishPhotoEvent(EntityUpdated, uid, c)
event.SuccessMsg(i18n.MsgChangesSaved)
p, err := query.PhotoPreloadByUID(uid)
if err != nil {
AbortEntityNotFound(c)
return
}
c.JSON(http.StatusOK, p)
})
}