Backend: API stub for editing photo metadata

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2019-12-11 19:11:44 +01:00
parent 4e06deda76
commit 845cc5a77d
7 changed files with 254 additions and 109 deletions

160
internal/api/photo.go Normal file
View 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})
})
}

View file

@ -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})
})
}

View file

@ -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())
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)