API: Proof-of-concept for form handling

We don't want to directly write to models so that only selected fields can be changed and values can be validated for security reasons.

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-02-02 03:36:00 +01:00
parent a90aecea51
commit 4efa383c57
9 changed files with 87 additions and 11 deletions

1
go.mod
View file

@ -66,6 +66,7 @@ require (
github.com/uber/jaeger-client-go v2.15.0+incompatible // indirect
github.com/uber/jaeger-lib v1.5.0 // indirect
github.com/ugorji/go v1.1.7 // indirect
github.com/ulule/deepcopier v0.0.0-20200117111125-792cfb847af8
github.com/unrolled/render v0.0.0-20181210145518-4c664cb3ad2f // indirect
github.com/urfave/cli v1.20.0
go.uber.org/atomic v1.4.0 // indirect

9
go.sum
View file

@ -126,7 +126,6 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/open-location-code v0.0.0-20191230190541-a6eb95b4d2f9 h1:MOOYh4mJsm+TXOgXRXT1E0g4FHrl41qhvUqXd/EPgFg=
github.com/google/open-location-code/go v0.0.0-20191230190541-a6eb95b4d2f9 h1:6ILzS4n0F17S38XvOB1BcyzB+0BtVzU77EyuMtkMffo=
github.com/google/open-location-code/go v0.0.0-20191230190541-a6eb95b4d2f9/go.mod h1:eJfRN6aj+kR/rnua/rw9jAgYhqoMHldQkdTi+sePRKk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -150,6 +149,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v0.0.0-20160910222444-6b7015e65d36/
github.com/grpc-ecosystem/grpc-gateway v1.4.1/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/grpc-ecosystem/grpc-gateway v1.5.1 h1:3scN4iuXkNOyP98jF55Lv8a9j1o/IwvnDIZ0LHJK1nk=
github.com/grpc-ecosystem/grpc-gateway v1.5.1/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/guregu/null v3.4.0+incompatible h1:a4mw37gBO7ypcBlTJeZGuMpSxxFTV9qFfFKgWxQSGaM=
github.com/guregu/null v3.4.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@ -183,6 +184,8 @@ github.com/leandro-lugaresi/hub v1.1.0 h1:yHYA0WsMYaJd+I6J24nYlCP2CFD4RTnhaHCRmK
github.com/leandro-lugaresi/hub v1.1.0/go.mod h1:IVKrfZTYfU1SbWCGQMHNGYdW4j1Pl7Cg8gr6sSeT/84=
github.com/lib/pq v1.1.0 h1:/5u4a+KGJptBRqGzPvYQL9p0d/tPR4S31+Tnzj9lEO4=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/machinebox/progress v0.2.0/go.mod h1:hl4FywxSjfmkmCrersGhmJH7KwuKl+Ueq9BXkOny+iE=
@ -328,6 +331,8 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ulule/deepcopier v0.0.0-20200117111125-792cfb847af8 h1:iCslye9fWwp1ExDu06BcKM8/gjDRAlX9DBL+T8CjuWU=
github.com/ulule/deepcopier v0.0.0-20200117111125-792cfb847af8/go.mod h1:wUZg40sMUnY+1FU5F9rZwwCruLb8h1bHF8HzI09kgok=
github.com/unrolled/render v0.0.0-20171102162132-65450fb6b2d3/go.mod h1:tu82oB5W2ykJRVioYsB+IQKcft7ryBr7w12qMBUPyXg=
github.com/unrolled/render v0.0.0-20181210145518-4c664cb3ad2f h1:+feYJlxPM00jEkdybexHiwIIOVuClwTEbh1WLiNr0mk=
github.com/unrolled/render v0.0.0-20181210145518-4c664cb3ad2f/go.mod h1:tu82oB5W2ykJRVioYsB+IQKcft7ryBr7w12qMBUPyXg=
@ -351,8 +356,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b h1:VHyIDlv3XkfCa5/a81uzaoDkHH4rr81Z62g+xlnO8uM=

View file

@ -5,13 +5,14 @@ import (
"net/http"
"path"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/gin-gonic/gin"
)
// GET /api/v1/photos/:uuid
@ -55,12 +56,26 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
return
}
if err := c.BindJSON(&m); err != nil {
// TODO: Proof-of-concept for form handling - might need refactoring
// 1) Init form with model values
f, err := form.NewPhoto(m)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
return
}
conf.Db().Save(&m)
// 2) Update form with values from request
if err := c.BindJSON(&f); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
return
}
// 3) Save model with values from form
if err := entity.SavePhoto(m, f, conf.Db()); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
return
}
PublishPhotoEvent(EntityUpdated, id, c, q)

View file

@ -5,8 +5,10 @@ import (
"time"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/ulule/deepcopier"
)
// Photo represents a photo that can have multiple image or sidecar files.
@ -34,9 +36,9 @@ type Photo struct {
PhotoExposure string `gorm:"type:varbinary(64);" json:"PhotoExposure"`
CameraID uint `gorm:"index:idx_photos_camera_lens;" json:"CameraID"`
LensID uint `gorm:"index:idx_photos_camera_lens;" json:"LensID"`
LocationID string `gorm:"type:varbinary(16);index;" json:"LocationID"`
PlaceID string `gorm:"type:varbinary(16);index;" json:"PlaceID"`
AccountID uint `json:"AccountID"`
PlaceID string `gorm:"type:varbinary(16);index;" json:"PlaceID"`
LocationID string `gorm:"type:varbinary(16);index;" json:"LocationID"`
LocationEstimated bool `json:"LocationEstimated"`
PhotoCountry string `gorm:"index:idx_photos_country_year_month;" json:"PhotoCountry"`
PhotoYear int `gorm:"index:idx_photos_country_year_month;"`
@ -61,6 +63,15 @@ type Photo struct {
DeletedAt *time.Time `sql:"index"`
}
// SavePhoto updates a model using form data and persists it in the database.
func SavePhoto(model Photo, form form.Photo, db *gorm.DB) error {
if err := deepcopier.Copy(&model).From(form); err != nil {
return err
}
return db.Save(&model).Error
}
func (m *Photo) BeforeCreate(scope *gorm.Scope) error {
if err := scope.SetColumn("PhotoUUID", rnd.PPID('p')); err != nil {
return err

View file

@ -1,5 +1,6 @@
package form
// Album represents an album edit form.
type Album struct {
AlbumName string `json:"AlbumName"`
AlbumDescription string `json:"AlbumDescription"`

View file

@ -1,5 +1,6 @@
package form
// Label represents a label edit form.
type Label struct {
LabelName string `json:"LabelName"`
LabelUncertainty int `json:"LabelUncertainty"`

45
internal/form/photo.go Normal file
View file

@ -0,0 +1,45 @@
package form
import (
"time"
"github.com/ulule/deepcopier"
)
// Photo represents a photo edit form.
type Photo struct {
TakenAt time.Time `json:"TakenAt"`
PhotoTitle string `json:"PhotoTitle"`
PhotoDescription string `json:"PhotoDescription"`
PhotoNotes string `json:"PhotoNotes"`
PhotoArtist string `json:"PhotoArtist"`
PhotoCopyright string `json:"PhotoCopyright"`
PhotoFavorite bool `json:"PhotoFavorite"`
PhotoPrivate bool `json:"PhotoPrivate"`
PhotoNSFW bool `json:"PhotoNSFW"`
PhotoStory bool `json:"PhotoStory"`
PhotoLat float64 `json:"PhotoLat"`
PhotoLng float64 `json:"PhotoLng"`
PhotoAltitude int `json:"PhotoAltitude"`
PhotoFocalLength int `json:"PhotoFocalLength"`
PhotoIso int `json:"PhotoIso"`
PhotoFNumber float64 `json:"PhotoFNumber"`
PhotoExposure string `json:"PhotoExposure"`
CameraID uint `json:"CameraID"`
LensID uint `json:"LensID"`
LocationID string `json:"LocationID"`
PlaceID string `json:"PlaceID"`
PhotoCountry string `json:"PhotoCountry"`
TimeZone string `json:"TimeZone"`
TakenAtLocal time.Time `json:"TakenAtLocal"`
ModifiedTitle bool `json:"ModifiedTitle"`
ModifiedDetails bool `json:"ModifiedDetails"`
ModifiedLocation bool `json:"ModifiedLocation"`
ModifiedDate bool `json:"ModifiedDate"`
}
func NewPhoto(m interface{}) (f Photo, err error) {
err = deepcopier.Copy(m).To(&f)
return f, err
}

View file

@ -11,7 +11,7 @@ func Clip(s string, size int) string {
runes := []rune(s)
if len(runes) > size {
s = string(runes[0:size-1])
s = string(runes[0 : size-1])
}
return s

View file

@ -10,4 +10,3 @@ func SlugToTitle(s string) string {
return Title(strings.Join(Words(s), " "))
}