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:
parent
a90aecea51
commit
4efa383c57
9 changed files with 87 additions and 11 deletions
1
go.mod
1
go.mod
|
@ -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
9
go.sum
|
@ -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=
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package form
|
||||
|
||||
// Album represents an album edit form.
|
||||
type Album struct {
|
||||
AlbumName string `json:"AlbumName"`
|
||||
AlbumDescription string `json:"AlbumDescription"`
|
||||
|
|
|
@ -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
45
internal/form/photo.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -10,4 +10,3 @@ func SlugToTitle(s string) string {
|
|||
|
||||
return Title(strings.Join(Words(s), " "))
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue