Backend: API stub for editing photo metadata
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
4e06deda76
commit
845cc5a77d
7 changed files with 254 additions and 109 deletions
160
internal/api/photo.go
Normal file
160
internal/api/photo.go
Normal file
|
@ -0,0 +1,160 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/repo"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GET /api/v1/photo/:uuid
|
||||
//
|
||||
// Parameters:
|
||||
// uuid: string PhotoUUID as returned by the API
|
||||
func GetPhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.GET("/photos/:uuid", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
r := repo.New(conf.OriginalsPath(), conf.Db())
|
||||
p, err := r.PreloadPhotoByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, p)
|
||||
})
|
||||
}
|
||||
|
||||
// PUT /api/v1/photos/:uuid
|
||||
func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.PUT("/photos/:uuid", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
r := repo.New(conf.OriginalsPath(), conf.Db())
|
||||
|
||||
m, err := r.FindPhotoByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.BindJSON(&m); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
conf.Db().Save(&m)
|
||||
|
||||
event.Success("photo updated")
|
||||
|
||||
c.JSON(http.StatusOK, m)
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/photos/:uuid/download
|
||||
//
|
||||
// Parameters:
|
||||
// uuid: string PhotoUUID as returned by the API
|
||||
func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.GET("/photos/:uuid/download", func(c *gin.Context) {
|
||||
r := repo.New(conf.OriginalsPath(), conf.Db())
|
||||
file, err := r.FindFileByPhotoUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("%s/%s", conf.OriginalsPath(), file.FileName)
|
||||
|
||||
if !util.Exists(fileName) {
|
||||
log.Errorf("could not find original: %s", c.Param("uuid"))
|
||||
c.Data(404, "image/svg+xml", photoIconSvg)
|
||||
|
||||
// Set missing flag so that the file doesn't show up in search results anymore
|
||||
file.FileMissing = true
|
||||
conf.Db().Save(&file)
|
||||
return
|
||||
}
|
||||
|
||||
downloadFileName := file.DownloadFileName()
|
||||
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
|
||||
|
||||
c.File(fileName)
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/photos/:uuid/like
|
||||
//
|
||||
// Parameters:
|
||||
// uuid: string PhotoUUID as returned by the API
|
||||
func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/photos/:uuid/like", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
r := repo.New(conf.OriginalsPath(), conf.Db())
|
||||
m, err := r.FindPhotoByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
m.PhotoFavorite = true
|
||||
conf.Db().Save(&m)
|
||||
|
||||
event.Publish("count.favorites", event.Data{
|
||||
"count": 1,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"photo": m})
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE /api/v1/photos/:uuid/like
|
||||
//
|
||||
// Parameters:
|
||||
// uuid: string PhotoUUID as returned by the API
|
||||
func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.DELETE("/photos/:uuid/like", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
r := repo.New(conf.OriginalsPath(), conf.Db())
|
||||
m, err := r.FindPhotoByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
m.PhotoFavorite = false
|
||||
conf.Db().Save(&m)
|
||||
|
||||
event.Publish("count.favorites", event.Data{
|
||||
"count": -1,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"photo": m})
|
||||
})
|
||||
}
|
|
@ -1,12 +1,10 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/repo"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
|
||||
|
@ -54,97 +52,3 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) {
|
|||
c.JSON(http.StatusOK, result)
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/photos/:uuid/download
|
||||
//
|
||||
// Parameters:
|
||||
// uuid: string PhotoUUID as returned by the API
|
||||
func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.GET("/photos/:uuid/download", func(c *gin.Context) {
|
||||
r := repo.New(conf.OriginalsPath(), conf.Db())
|
||||
file, err := r.FindFileByPhotoUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("%s/%s", conf.OriginalsPath(), file.FileName)
|
||||
|
||||
if !util.Exists(fileName) {
|
||||
log.Errorf("could not find original: %s", c.Param("uuid"))
|
||||
c.Data(404, "image/svg+xml", photoIconSvg)
|
||||
|
||||
// Set missing flag so that the file doesn't show up in search results anymore
|
||||
file.FileMissing = true
|
||||
conf.Db().Save(&file)
|
||||
return
|
||||
}
|
||||
|
||||
downloadFileName := file.DownloadFileName()
|
||||
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
|
||||
|
||||
c.File(fileName)
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/photos/:uuid/like
|
||||
//
|
||||
// Parameters:
|
||||
// uuid: string PhotoUUID as returned by the API
|
||||
func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/photos/:uuid/like", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
r := repo.New(conf.OriginalsPath(), conf.Db())
|
||||
m, err := r.FindPhotoByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
m.PhotoFavorite = true
|
||||
conf.Db().Save(&m)
|
||||
|
||||
event.Publish("count.favorites", event.Data{
|
||||
"count": 1,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"photo": m})
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE /api/v1/photos/:uuid/like
|
||||
//
|
||||
// Parameters:
|
||||
// uuid: string PhotoUUID as returned by the API
|
||||
func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.DELETE("/photos/:uuid/like", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
r := repo.New(conf.OriginalsPath(), conf.Db())
|
||||
m, err := r.FindPhotoByUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(404, gin.H{"error": util.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
m.PhotoFavorite = false
|
||||
conf.Db().Save(&m)
|
||||
|
||||
event.Publish("count.favorites", event.Data{
|
||||
"count": -1,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"photo": m})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,3 +8,16 @@ Additional information concerning data storage can be found in our Developer Gui
|
|||
https://github.com/photoprism/photoprism/wiki/Storage
|
||||
*/
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
)
|
||||
|
||||
var log = event.Log
|
||||
|
||||
func logError(result *gorm.DB) {
|
||||
if result.Error != nil {
|
||||
log.Error(result.Error.Error())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var logBuffer bytes.Buffer
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log = logrus.StandardLogger()
|
||||
log.Out = &logBuffer
|
||||
log.SetLevel(logrus.DebugLevel)
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
|
|
@ -16,15 +16,15 @@ type Photo struct {
|
|||
PhotoToken string `gorm:"type:varchar(64);"`
|
||||
PhotoPath string `gorm:"type:varchar(128);index;"`
|
||||
PhotoName string
|
||||
PhotoTitle string
|
||||
PhotoTitle string `json:"PhotoTitle"`
|
||||
PhotoTitleChanged bool
|
||||
PhotoDescription string `gorm:"type:text;"`
|
||||
PhotoNotes string `gorm:"type:text;"`
|
||||
PhotoArtist string
|
||||
PhotoFavorite bool
|
||||
PhotoPrivate bool
|
||||
PhotoNSFW bool
|
||||
PhotoStory bool
|
||||
PhotoDescription string `gorm:"type:text;"`
|
||||
PhotoNotes string `gorm:"type:text;"`
|
||||
PhotoArtist string `json:"PhotoArtist"`
|
||||
PhotoFavorite bool `json:"PhotoFavorite"`
|
||||
PhotoPrivate bool `json:"PhotoPrivate"`
|
||||
PhotoNSFW bool `json:"PhotoNSFW"`
|
||||
PhotoStory bool `json:"PhotoStory"`
|
||||
PhotoLat float64 `gorm:"index;"`
|
||||
PhotoLong float64 `gorm:"index;"`
|
||||
PhotoAltitude int
|
||||
|
@ -48,9 +48,10 @@ type Photo struct {
|
|||
TakenAtLocal time.Time
|
||||
TakenAtChanged bool
|
||||
TimeZone string
|
||||
Labels []*PhotoLabel
|
||||
Albums []*PhotoAlbum
|
||||
Files []*File
|
||||
Files []File
|
||||
Labels []Label
|
||||
Keywords []Keyword
|
||||
Albums []Album
|
||||
}
|
||||
|
||||
func (m *Photo) BeforeCreate(scope *gorm.Scope) error {
|
||||
|
@ -94,3 +95,52 @@ func (m *Photo) IndexKeywords(keywords []string, db *gorm.DB) {
|
|||
|
||||
db.Where("photo_id = ? AND keyword_id NOT IN (?)", m.ID, keywordIds).Delete(&PhotoKeyword{})
|
||||
}
|
||||
|
||||
func (m *Photo) PreloadFiles(db *gorm.DB) {
|
||||
q := db.NewScope(nil).DB().
|
||||
Table("files").
|
||||
Select(`files.*`).
|
||||
Where("files.photo_id = ?", m.ID).
|
||||
Order("files.file_primary DESC")
|
||||
|
||||
logError(q.Scan(&m.Files))
|
||||
}
|
||||
|
||||
func (m *Photo) PreloadLabels(db *gorm.DB) {
|
||||
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))
|
||||
}
|
||||
|
||||
func (m *Photo) PreloadKeywords(db *gorm.DB) {
|
||||
q := db.NewScope(nil).DB().
|
||||
Table("keywords").
|
||||
Select(`keywords.*`).
|
||||
Joins("JOIN photos_keywords ON photos_keywords.keyword_id = keywords.id AND photos_keywords.photo_id = ?", m.ID).
|
||||
Order("keywords.keyword ASC")
|
||||
|
||||
logError(q.Scan(&m.Keywords))
|
||||
}
|
||||
|
||||
func (m *Photo) PreloadAlbums(db *gorm.DB) {
|
||||
q := db.NewScope(nil).DB().
|
||||
Table("albums").
|
||||
Select(`albums.*`).
|
||||
Joins("JOIN photos_albums ON photos_albums.album_uuid = albums.album_uuid AND photos_albums.photo_uuid = ?", m.PhotoUUID).
|
||||
Where("albums.deleted_at IS NULL").
|
||||
Order("albums.album_name ASC")
|
||||
|
||||
logError(q.Scan(&m.Albums))
|
||||
}
|
||||
|
||||
func (m *Photo) PreloadMany(db *gorm.DB) {
|
||||
m.PreloadFiles(db)
|
||||
m.PreloadLabels(db)
|
||||
m.PreloadKeywords(db)
|
||||
m.PreloadAlbums(db)
|
||||
}
|
||||
|
|
|
@ -336,3 +336,14 @@ func (s *Repo) FindPhotoByUUID(photoUUID string) (photo entity.Photo, err error)
|
|||
|
||||
return photo, nil
|
||||
}
|
||||
|
||||
// PreloadPhotoByUUID returns a Photo based on the UUID with all dependencies preloaded.
|
||||
func (s *Repo) PreloadPhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
|
||||
if err := s.db.Where("photo_uuid = ?", photoUUID).Preload("Camera").Preload("Lens").First(&photo).Error; err != nil {
|
||||
return photo, err
|
||||
}
|
||||
|
||||
photo.PreloadMany(s.db)
|
||||
|
||||
return photo, nil
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.CreateZip(v1, conf)
|
||||
api.DownloadZip(v1, conf)
|
||||
|
||||
api.GetPhoto(v1, conf)
|
||||
api.UpdatePhoto(v1, conf)
|
||||
api.GetPhotos(v1, conf)
|
||||
api.GetPhotoDownload(v1, conf)
|
||||
api.LikePhoto(v1, conf)
|
||||
|
|
Loading…
Reference in a new issue