diff --git a/frontend/src/component/p-photo-clipboard.vue b/frontend/src/component/p-photo-clipboard.vue index c633e051e..a64962829 100644 --- a/frontend/src/component/p-photo-clipboard.vue +++ b/frontend/src/component/p-photo-clipboard.vue @@ -61,7 +61,7 @@ small title="Download" color="teal accent-4" - @click.stop="batchDownload()" + @click.stop="downloadZip()" class="p-photo-clipboard-download" > save @@ -212,10 +212,15 @@ Notify.warning("Not implemented yet"); this.expanded = false; }, - batchDownload() { - Notify.warning("Not implemented yet"); + downloadZip() { + Api.post("zip", {"photos": this.selection}).then(this.onDownload.bind(this)); this.expanded = false; }, + onDownload(r) { + console.log("onDownload", r); + Notify.success(r.data.message); + window.open("/api/v1/zip/" + r.data.filename, "_blank"); + }, openDocs() { window.open('https://docs.photoprism.org/en/latest/', '_blank'); }, diff --git a/internal/api/albums.go b/internal/api/albums.go index d082e629e..991d32153 100644 --- a/internal/api/albums.go +++ b/internal/api/albums.go @@ -6,12 +6,12 @@ import ( "strconv" "github.com/photoprism/photoprism/internal/event" + "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/models" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/photoprism/photoprism/internal/config" - "github.com/photoprism/photoprism/internal/forms" "github.com/photoprism/photoprism/internal/photoprism" "github.com/photoprism/photoprism/internal/util" ) @@ -19,24 +19,24 @@ import ( // GET /api/v1/albums func GetAlbums(router *gin.RouterGroup, conf *config.Config) { router.GET("/albums", func(c *gin.Context) { - var form forms.AlbumSearchForm + var f form.AlbumSearch search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) - err := c.MustBindWith(&form, binding.Form) + err := c.MustBindWith(&f, binding.Form) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())}) return } - result, err := search.Albums(form) + result, err := search.Albums(f) if err != nil { c.AbortWithStatusJSON(400, gin.H{"error": util.UcFirst(err.Error())}) return } - c.Header("X-Result-Count", strconv.Itoa(form.Count)) - c.Header("X-Result-Offset", strconv.Itoa(form.Offset)) + c.Header("X-Result-Count", strconv.Itoa(f.Count)) + c.Header("X-Result-Offset", strconv.Itoa(f.Offset)) c.JSON(http.StatusOK, result) }) @@ -58,10 +58,6 @@ func GetAlbum(router *gin.RouterGroup, conf *config.Config) { }) } -type AlbumParams struct { - AlbumName string `json:"AlbumName"` -} - // POST /api/v1/albums func CreateAlbum(router *gin.RouterGroup, conf *config.Config) { router.POST("/albums", func(c *gin.Context) { @@ -70,14 +66,14 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) { return } - var params AlbumParams + var f form.Album - if err := c.BindJSON(¶ms); err != nil { + if err := c.BindJSON(&f); err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())}) return } - m := models.NewAlbum(params.AlbumName) + m := models.NewAlbum(f.AlbumName) if res := conf.Db().Create(m); res.Error != nil { log.Error(res.Error.Error()) @@ -99,9 +95,9 @@ func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) { return } - var params AlbumParams + var f form.Album - if err := c.BindJSON(¶ms); err != nil { + if err := c.BindJSON(&f); err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())}) return } @@ -116,7 +112,7 @@ func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) { return } - m.Rename(params.AlbumName) + m.Rename(f.AlbumName) conf.Db().Save(&m) event.Publish("config.updated", event.Data(conf.ClientConfig())) @@ -192,14 +188,14 @@ func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) { return } - var params PhotoUUIDs + var f form.PhotoUUIDs - if err := c.BindJSON(¶ms); err != nil { + if err := c.BindJSON(&f); err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())}) return } - if len(params.Photos) == 0 { + if len(f.Photos) == 0 { log.Error("no photos selected") c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst("no photos selected")}) return @@ -213,13 +209,11 @@ func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) { return } - log.Infof("adding %d photos to album %s", len(params.Photos), a.AlbumName) - db := conf.Db() var added []*models.PhotoAlbum var failed []string - for _, photoUUID := range params.Photos { + for _, photoUUID := range f.Photos { if p, err := search.FindPhotoByUUID(photoUUID); err != nil { failed = append(failed, photoUUID) } else { @@ -245,14 +239,14 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup, conf *config.Config) { return } - var params PhotoUUIDs + var f form.PhotoUUIDs - if err := c.BindJSON(¶ms); err != nil { + if err := c.BindJSON(&f); err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())}) return } - if len(params.Photos) == 0 { + if len(f.Photos) == 0 { log.Error("no photos selected") c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst("no photos selected")}) return @@ -266,14 +260,12 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup, conf *config.Config) { return } - log.Infof("adding %d photos to album %s", len(params.Photos), a.AlbumName) - db := conf.Db() - db.Where("album_uuid = ? AND photo_uuid IN (?)", a.AlbumUUID, params.Photos).Delete(&models.PhotoAlbum{}) + db.Where("album_uuid = ? AND photo_uuid IN (?)", a.AlbumUUID, f.Photos).Delete(&models.PhotoAlbum{}) event.Success(fmt.Sprintf("photos removed from %s", a.AlbumName)) - c.JSON(http.StatusOK, gin.H{"message": "photos removed from album", "album": a, "photos": params.Photos}) + c.JSON(http.StatusOK, gin.H{"message": "photos removed from album", "album": a, "photos": f.Photos}) }) } diff --git a/internal/api/batch.go b/internal/api/batch.go index 33bafd502..1aed91f11 100644 --- a/internal/api/batch.go +++ b/internal/api/batch.go @@ -7,16 +7,13 @@ import ( "github.com/jinzhu/gorm" "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/models" "github.com/photoprism/photoprism/internal/util" "github.com/gin-gonic/gin" ) -type PhotoUUIDs struct { - Photos []string `json:"photos"` -} - // POST /api/v1/batch/photos/delete func BatchPhotosDelete(router *gin.RouterGroup, conf *config.Config) { router.POST("/batch/photos/delete", func(c *gin.Context) { @@ -27,24 +24,24 @@ func BatchPhotosDelete(router *gin.RouterGroup, conf *config.Config) { start := time.Now() - var params PhotoUUIDs + var f form.PhotoUUIDs - if err := c.BindJSON(¶ms); err != nil { + if err := c.BindJSON(&f); err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())}) return } - if len(params.Photos) == 0 { + if len(f.Photos) == 0 { log.Error("no photos selected") c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst("no photos selected")}) return } - log.Infof("deleting photos: %#v", params.Photos) + log.Infof("deleting photos: %#v", f.Photos) db := conf.Db() - db.Where("photo_uuid IN (?)", params.Photos).Delete(&models.Photo{}) + db.Where("photo_uuid IN (?)", f.Photos).Delete(&models.Photo{}) elapsed := time.Since(start) @@ -62,24 +59,24 @@ func BatchPhotosPrivate(router *gin.RouterGroup, conf *config.Config) { start := time.Now() - var params PhotoUUIDs + var f form.PhotoUUIDs - if err := c.BindJSON(¶ms); err != nil { + if err := c.BindJSON(&f); err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())}) return } - if len(params.Photos) == 0 { + if len(f.Photos) == 0 { log.Error("no photos selected") c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst("no photos selected")}) return } - log.Infof("marking photos as private: %#v", params.Photos) + log.Infof("marking photos as private: %#v", f.Photos) db := conf.Db() - db.Model(models.Photo{}).Where("photo_uuid IN (?)", params.Photos).UpdateColumn("photo_private", gorm.Expr("IF (`photo_private`, 0, 1)")) + db.Model(models.Photo{}).Where("photo_uuid IN (?)", f.Photos).UpdateColumn("photo_private", gorm.Expr("IF (`photo_private`, 0, 1)")) elapsed := time.Since(start) @@ -97,25 +94,25 @@ func BatchPhotosStory(router *gin.RouterGroup, conf *config.Config) { start := time.Now() - var params PhotoUUIDs + var f form.PhotoUUIDs - if err := c.BindJSON(¶ms); err != nil { + if err := c.BindJSON(&f); err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())}) return } - if len(params.Photos) == 0 { + if len(f.Photos) == 0 { log.Error("no photos selected") c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst("no photos selected")}) return } - log.Infof("marking photos as story: %#v", params.Photos) + log.Infof("marking photos as story: %#v", f.Photos) db := conf.Db() - db.Model(models.Photo{}).Where("photo_uuid IN (?)", params.Photos).Updates(map[string]interface{}{ - "photo_story": gorm.Expr("IF (`photo_story`, 0, 1)"), + db.Model(models.Photo{}).Where("photo_uuid IN (?)", f.Photos).Updates(map[string]interface{}{ + "photo_story": gorm.Expr("IF (`photo_story`, 0, 1)"), }) elapsed := time.Since(start) diff --git a/internal/api/download.go b/internal/api/download.go index 411ee3bf1..c95c272f5 100644 --- a/internal/api/download.go +++ b/internal/api/download.go @@ -10,6 +10,10 @@ import ( "github.com/photoprism/photoprism/internal/photoprism" ) +// TODO: GET /api/v1/dl/file/:hash +// TODO: GET /api/v1/dl/photo/:uuid +// TODO: GET /api/v1/dl/album/:uuid + // GET /api/v1/download/:hash // // Parameters: diff --git a/internal/api/labels.go b/internal/api/labels.go index 237d384a8..2d49923a6 100644 --- a/internal/api/labels.go +++ b/internal/api/labels.go @@ -7,7 +7,7 @@ import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/photoprism/photoprism/internal/config" - "github.com/photoprism/photoprism/internal/forms" + "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/photoprism" "github.com/photoprism/photoprism/internal/util" ) @@ -15,24 +15,24 @@ import ( // GET /api/v1/labels func GetLabels(router *gin.RouterGroup, conf *config.Config) { router.GET("/labels", func(c *gin.Context) { - var form forms.LabelSearchForm + var f form.LabelSearch search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) - err := c.MustBindWith(&form, binding.Form) + err := c.MustBindWith(&f, binding.Form) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())}) return } - result, err := search.Labels(form) + result, err := search.Labels(f) if err != nil { c.AbortWithStatusJSON(400, gin.H{"error": util.UcFirst(err.Error())}) return } - c.Header("X-Result-Count", strconv.Itoa(form.Count)) - c.Header("X-Result-Offset", strconv.Itoa(form.Offset)) + c.Header("X-Result-Count", strconv.Itoa(f.Count)) + c.Header("X-Result-Offset", strconv.Itoa(f.Offset)) c.JSON(http.StatusOK, result) }) diff --git a/internal/api/photos.go b/internal/api/photos.go index 11ffbbae2..0cefbd2eb 100644 --- a/internal/api/photos.go +++ b/internal/api/photos.go @@ -9,7 +9,7 @@ import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" - "github.com/photoprism/photoprism/internal/forms" + "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/photoprism" ) @@ -29,24 +29,24 @@ import ( // favorites: bool Find favorites only func GetPhotos(router *gin.RouterGroup, conf *config.Config) { router.GET("/photos", func(c *gin.Context) { - var form forms.PhotoSearchForm + var f form.PhotoSearch search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) - err := c.MustBindWith(&form, binding.Form) + err := c.MustBindWith(&f, binding.Form) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())}) return } - result, err := search.Photos(form) + result, err := search.Photos(f) if err != nil { c.AbortWithStatusJSON(400, gin.H{"error": util.UcFirst(err.Error())}) return } - c.Header("X-Result-Count", strconv.Itoa(form.Count)) - c.Header("X-Result-Offset", strconv.Itoa(form.Offset)) + c.Header("X-Result-Count", strconv.Itoa(f.Count)) + c.Header("X-Result-Offset", strconv.Itoa(f.Offset)) c.JSON(http.StatusOK, result) }) diff --git a/internal/api/session.go b/internal/api/session.go index 34aa59873..7e3fa1aa5 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -6,25 +6,21 @@ import ( "github.com/gin-gonic/gin" "github.com/patrickmn/go-cache" "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/util" ) -type CreateSessionParams struct { - Email string `json:"email"` - Password string `json:"password"` -} - // POST /api/v1/session func CreateSession(router *gin.RouterGroup, conf *config.Config) { router.POST("/session", func(c *gin.Context) { - var params CreateSessionParams + var f form.Login - if err := c.BindJSON(¶ms); err != nil { + if err := c.BindJSON(&f); err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())}) return } - if params.Password != conf.AdminPassword() { + if f.Password != conf.AdminPassword() { c.AbortWithStatusJSON(400, gin.H{"error": "Invalid password"}) return } diff --git a/internal/api/upload.go b/internal/api/upload.go index 13434bde0..70977f0ea 100644 --- a/internal/api/upload.go +++ b/internal/api/upload.go @@ -29,14 +29,14 @@ func Upload(router *gin.RouterGroup, conf *config.Config) { start := time.Now() subPath := c.Param("path") - form, err := c.MultipartForm() + f, err := c.MultipartForm() if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())}) return } - files := form.File["files"] + files := f.File["files"] path := fmt.Sprintf("%s/upload/%s", conf.ImportPath(), subPath) diff --git a/internal/api/zip.go b/internal/api/zip.go new file mode 100644 index 000000000..d4271ea6b --- /dev/null +++ b/internal/api/zip.go @@ -0,0 +1,148 @@ +package api + +import ( + "archive/zip" + "fmt" + "io" + "net/http" + "os" + "path" + "path/filepath" + "time" + + "github.com/gosimple/slug" + "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/form" + "github.com/photoprism/photoprism/internal/util" + + "github.com/gin-gonic/gin" + "github.com/photoprism/photoprism/internal/photoprism" +) + +// POST /api/v1/zip +func CreateZip(router *gin.RouterGroup, conf *config.Config) { + router.POST("/zip", func(c *gin.Context) { + var f form.PhotoUUIDs + start := time.Now() + + if err := c.BindJSON(&f); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())}) + return + } + + if len(f.Photos) == 0 { + log.Error("no photos selected") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst("no photos selected")}) + return + } + + search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + files, err := search.FindFilesByUUID(f.Photos, 1000, 0) + + if err != nil { + c.AbortWithStatusJSON(404, gin.H{"error": err.Error()}) + return + } + + zipPath := path.Join(conf.ExportPath(), "zip") + zipDate := time.Now().Format("20060201-150405") + zipBaseName := fmt.Sprintf("photos-%s.zip", zipDate) + zipFileName := fmt.Sprintf("%s/%s", zipPath, zipBaseName) + + if err := os.MkdirAll(zipPath, 0700); err != nil { + log.Error(err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": util.UcFirst("failed to create zip directory")}) + return + } + + newZipFile, err := os.Create(zipFileName) + + if err != nil { + log.Error(err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": util.UcFirst(err.Error())}) + return + } + + defer newZipFile.Close() + + zipWriter := zip.NewWriter(newZipFile) + defer zipWriter.Close() + + for i, file := range files { + fileName := fmt.Sprintf("%s/%s", conf.OriginalsPath(), file.FileName) + fileSlug := slug.MakeLang(file.Photo.PhotoTitle, "en") + fileAlias := fmt.Sprintf("%05d-%s.%s", i, fileSlug, file.FileType) + + if util.Exists(fileName) { + if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil { + log.Error(err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": util.UcFirst("failed to create zip file")}) + return + } + log.Infof("zip: added %s as %s", file.FileName, fileAlias) + } else { + log.Warnf("zip: %s is missing", file.FileName) + file.FileMissing = true + conf.Db().Save(&file) + } + } + + elapsed := int(time.Since(start).Seconds()) + + log.Infof("zip: archive %s created in %s", zipBaseName, time.Since(start)) + + c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("zip created in %d s", elapsed), "filename": zipBaseName}) + }) +} + +// GET /api/v1/zip/:filename +func DownloadZip(router *gin.RouterGroup, conf *config.Config) { + router.GET("/zip/:filename", func(c *gin.Context) { + zipBaseName := filepath.Base(c.Param("filename")) + zipPath := path.Join(conf.ExportPath(), "zip") + zipFileName := fmt.Sprintf("%s/%s", zipPath, zipBaseName) + + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", zipBaseName)) + + if !util.Exists(zipFileName) { + log.Errorf("could not find zip file: %s", zipFileName) + c.Data(404, "image/svg+xml", photoIconSvg) + return + } + + c.File(zipFileName) + + }) +} + +func addFileToZip(zipWriter *zip.Writer, fileName, fileAlias string) error { + fileToZip, err := os.Open(fileName) + if err != nil { + return err + } + defer fileToZip.Close() + + // Get the file information + info, err := fileToZip.Stat() + if err != nil { + return err + } + + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + + header.Name = fileAlias + + // Change to deflate to gain better compression + // see http://golang.org/pkg/archive/zip/#pkg-constants + header.Method = zip.Deflate + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + _, err = io.Copy(writer, fileToZip) + return err +} diff --git a/internal/form/album.go b/internal/form/album.go new file mode 100644 index 000000000..2f885445c --- /dev/null +++ b/internal/form/album.go @@ -0,0 +1,5 @@ +package form + +type Album struct { + AlbumName string `json:"AlbumName"` +} diff --git a/internal/forms/album_search.go b/internal/form/album_search.go similarity index 96% rename from internal/forms/album_search.go rename to internal/form/album_search.go index d67c43cda..be551448b 100644 --- a/internal/forms/album_search.go +++ b/internal/form/album_search.go @@ -1,4 +1,4 @@ -package forms +package form import ( "bytes" @@ -15,7 +15,7 @@ import ( ) // Query parameters for GET /api/v1/albums -type AlbumSearchForm struct { +type AlbumSearch struct { Query string `form:"q"` Slug string `form:"slug"` @@ -27,7 +27,7 @@ type AlbumSearchForm struct { Order string `form:"order"` } -func (f *AlbumSearchForm) ParseQueryString() (result error) { +func (f *AlbumSearch) ParseQueryString() (result error) { var key, value []byte var escaped, isKeyValue bool diff --git a/internal/forms/album_search_test.go b/internal/form/album_search_test.go similarity index 76% rename from internal/forms/album_search_test.go rename to internal/form/album_search_test.go index 38c3155d5..b45491ce3 100644 --- a/internal/forms/album_search_test.go +++ b/internal/form/album_search_test.go @@ -1,4 +1,4 @@ -package forms +package form import ( log "github.com/sirupsen/logrus" @@ -7,15 +7,15 @@ import ( ) func TestAlbumSearchForm(t *testing.T) { - form := &AlbumSearchForm{} + form := &AlbumSearch{} - assert.IsType(t, new(AlbumSearchForm), form) + assert.IsType(t, new(AlbumSearch), form) } func TestParseQueryStringAlbum(t *testing.T) { t.Run("valid query", func(t *testing.T) { - form := &AlbumSearchForm{Query: "slug:album1 favorites:true count:10"} + form := &AlbumSearch{Query: "slug:album1 favorites:true count:10"} err := form.ParseQueryString() @@ -27,7 +27,7 @@ func TestParseQueryStringAlbum(t *testing.T) { assert.Equal(t, 10, form.Count) }) t.Run("valid query 2", func(t *testing.T) { - form := &AlbumSearchForm{Query: "name:album1 favorites:false offset:100 order:newest query:\"query text\""} + form := &AlbumSearch{Query: "name:album1 favorites:false offset:100 order:newest query:\"query text\""} err := form.ParseQueryString() @@ -41,7 +41,7 @@ func TestParseQueryStringAlbum(t *testing.T) { assert.Equal(t, "query text", form.Query) }) t.Run("query for invalid filter", func(t *testing.T) { - form := &AlbumSearchForm{Query: "xxx:false"} + form := &AlbumSearch{Query: "xxx:false"} err := form.ParseQueryString() @@ -50,7 +50,7 @@ func TestParseQueryStringAlbum(t *testing.T) { assert.Equal(t, "unknown filter: Xxx", err.Error()) }) t.Run("query for favorites with invalid type", func(t *testing.T) { - form := &AlbumSearchForm{Query: "favorites:cat"} + form := &AlbumSearch{Query: "favorites:cat"} err := form.ParseQueryString() @@ -59,7 +59,7 @@ func TestParseQueryStringAlbum(t *testing.T) { assert.Equal(t, "not a bool value: Favorites", err.Error()) }) t.Run("query for count with invalid type", func(t *testing.T) { - form := &AlbumSearchForm{Query: "count:cat"} + form := &AlbumSearch{Query: "count:cat"} err := form.ParseQueryString() diff --git a/internal/forms/doc.go b/internal/form/doc.go similarity index 58% rename from internal/forms/doc.go rename to internal/form/doc.go index 3bb9cce09..88ed3d251 100644 --- a/internal/forms/doc.go +++ b/internal/form/doc.go @@ -1,8 +1,8 @@ /* -Package forms contains tagged structs for input value validation. +Package form contains tagged structs for input value validation. Additional information can be found in our Developer Guide: https://github.com/photoprism/photoprism/wiki */ -package forms +package form diff --git a/internal/forms/forms_test.go b/internal/form/forms_test.go similarity index 91% rename from internal/forms/forms_test.go rename to internal/form/forms_test.go index bd8c01df6..336ecc040 100644 --- a/internal/forms/forms_test.go +++ b/internal/form/forms_test.go @@ -1,4 +1,4 @@ -package forms +package form import ( "os" diff --git a/internal/forms/label_search.go b/internal/form/label_search.go similarity index 96% rename from internal/forms/label_search.go rename to internal/form/label_search.go index 5461d6542..429bc383a 100644 --- a/internal/forms/label_search.go +++ b/internal/form/label_search.go @@ -1,4 +1,4 @@ -package forms +package form import ( "bytes" @@ -15,7 +15,7 @@ import ( ) // Query parameters for GET /api/v1/labels -type LabelSearchForm struct { +type LabelSearch struct { Query string `form:"q"` Slug string `form:"slug"` @@ -28,7 +28,7 @@ type LabelSearchForm struct { Order string `form:"order"` } -func (f *LabelSearchForm) ParseQueryString() (result error) { +func (f *LabelSearch) ParseQueryString() (result error) { var key, value []byte var escaped, isKeyValue bool diff --git a/internal/forms/label_search_test.go b/internal/form/label_search_test.go similarity index 76% rename from internal/forms/label_search_test.go rename to internal/form/label_search_test.go index 32b27448e..d66716de5 100644 --- a/internal/forms/label_search_test.go +++ b/internal/form/label_search_test.go @@ -1,4 +1,4 @@ -package forms +package form import ( log "github.com/sirupsen/logrus" @@ -7,15 +7,15 @@ import ( ) func TestLabelSearchForm(t *testing.T) { - form := &LabelSearchForm{} + form := &LabelSearch{} - assert.IsType(t, new(LabelSearchForm), form) + assert.IsType(t, new(LabelSearch), form) } func TestParseQueryStringLabel(t *testing.T) { t.Run("valid query", func(t *testing.T) { - form := &LabelSearchForm{Query: "name:cat favorites:true count:10 priority:4 query:\"query text\""} + form := &LabelSearch{Query: "name:cat favorites:true count:10 priority:4 query:\"query text\""} err := form.ParseQueryString() @@ -29,7 +29,7 @@ func TestParseQueryStringLabel(t *testing.T) { assert.Equal(t, "query text", form.Query) }) t.Run("valid query 2", func(t *testing.T) { - form := &LabelSearchForm{Query: "slug:cat favorites:false offset:2 order:oldest"} + form := &LabelSearch{Query: "slug:cat favorites:false offset:2 order:oldest"} err := form.ParseQueryString() @@ -42,7 +42,7 @@ func TestParseQueryStringLabel(t *testing.T) { assert.Equal(t, "oldest", form.Order) }) t.Run("query for invalid filter", func(t *testing.T) { - form := &LabelSearchForm{Query: "xxx:false"} + form := &LabelSearch{Query: "xxx:false"} err := form.ParseQueryString() @@ -51,7 +51,7 @@ func TestParseQueryStringLabel(t *testing.T) { assert.Equal(t, "unknown filter: Xxx", err.Error()) }) t.Run("query for favorites with invalid type", func(t *testing.T) { - form := &LabelSearchForm{Query: "favorites:cat"} + form := &LabelSearch{Query: "favorites:cat"} err := form.ParseQueryString() @@ -60,7 +60,7 @@ func TestParseQueryStringLabel(t *testing.T) { assert.Equal(t, "not a bool value: Favorites", err.Error()) }) t.Run("query for count with invalid type", func(t *testing.T) { - form := &LabelSearchForm{Query: "count:cat"} + form := &LabelSearch{Query: "count:cat"} err := form.ParseQueryString() diff --git a/internal/form/login.go b/internal/form/login.go new file mode 100644 index 000000000..b93d1aade --- /dev/null +++ b/internal/form/login.go @@ -0,0 +1,6 @@ +package form + +type Login struct { + Email string `json:"email"` + Password string `json:"password"` +} diff --git a/internal/forms/photo_search.go b/internal/form/photo_search.go similarity index 97% rename from internal/forms/photo_search.go rename to internal/form/photo_search.go index 84dd7ca9b..a9df03964 100644 --- a/internal/forms/photo_search.go +++ b/internal/form/photo_search.go @@ -1,4 +1,4 @@ -package forms +package form import ( "bytes" @@ -15,7 +15,7 @@ import ( ) // Query parameters for GET /api/v1/photos -type PhotoSearchForm struct { +type PhotoSearch struct { Query string `form:"q"` Title string `form:"title"` @@ -47,7 +47,7 @@ type PhotoSearchForm struct { Order string `form:"order"` } -func (f *PhotoSearchForm) ParseQueryString() (result error) { +func (f *PhotoSearch) ParseQueryString() (result error) { var key, value []byte var escaped, isKeyValue bool diff --git a/internal/forms/photo_search_test.go b/internal/form/photo_search_test.go similarity index 77% rename from internal/forms/photo_search_test.go rename to internal/form/photo_search_test.go index 551c18e1b..67fc3dd28 100644 --- a/internal/forms/photo_search_test.go +++ b/internal/form/photo_search_test.go @@ -1,4 +1,4 @@ -package forms +package form import ( "testing" @@ -10,15 +10,15 @@ import ( ) func TestPhotoSearchForm(t *testing.T) { - form := &PhotoSearchForm{} + form := &PhotoSearch{} - assert.IsType(t, new(PhotoSearchForm), form) + assert.IsType(t, new(PhotoSearch), form) } func TestParseQueryString(t *testing.T) { t.Run("valid query", func(t *testing.T) { - form := &PhotoSearchForm{Query: "label:cat query:\"fooBar baz\" before:2019-01-15 camera:23 favorites:false dist:25000 lat:33.45343166666667"} + form := &PhotoSearch{Query: "label:cat query:\"fooBar baz\" before:2019-01-15 camera:23 favorites:false dist:25000 lat:33.45343166666667"} err := form.ParseQueryString() @@ -34,7 +34,7 @@ func TestParseQueryString(t *testing.T) { assert.Equal(t, 33.45343166666667, form.Lat) }) t.Run("valid query 2", func(t *testing.T) { - form := &PhotoSearchForm{Query: "chroma:600 description:\"test\" after:2018-01-15 duplicate:false favorites:true long:33.45343166666667"} + form := &PhotoSearch{Query: "chroma:600 description:\"test\" after:2018-01-15 duplicate:false favorites:true long:33.45343166666667"} err := form.ParseQueryString() @@ -48,7 +48,7 @@ func TestParseQueryString(t *testing.T) { assert.Equal(t, 33.45343166666667, form.Long) }) t.Run("query for invalid filter", func(t *testing.T) { - form := &PhotoSearchForm{Query: "xxx:false"} + form := &PhotoSearch{Query: "xxx:false"} err := form.ParseQueryString() @@ -57,7 +57,7 @@ func TestParseQueryString(t *testing.T) { assert.Equal(t, "unknown filter: Xxx", err.Error()) }) t.Run("query for favorites with invalid type", func(t *testing.T) { - form := &PhotoSearchForm{Query: "favorites:cat"} + form := &PhotoSearch{Query: "favorites:cat"} err := form.ParseQueryString() @@ -66,7 +66,7 @@ func TestParseQueryString(t *testing.T) { assert.Equal(t, "not a bool value: Favorites", err.Error()) }) t.Run("query for lat with invalid type", func(t *testing.T) { - form := &PhotoSearchForm{Query: "lat:cat"} + form := &PhotoSearch{Query: "lat:cat"} err := form.ParseQueryString() @@ -75,7 +75,7 @@ func TestParseQueryString(t *testing.T) { assert.Equal(t, "strconv.ParseFloat: parsing \"cat\": invalid syntax", err.Error()) }) t.Run("query for dist with invalid type", func(t *testing.T) { - form := &PhotoSearchForm{Query: "dist:cat"} + form := &PhotoSearch{Query: "dist:cat"} err := form.ParseQueryString() @@ -84,7 +84,7 @@ func TestParseQueryString(t *testing.T) { assert.Equal(t, "strconv.Atoi: parsing \"cat\": invalid syntax", err.Error()) }) t.Run("query for camera with invalid type", func(t *testing.T) { - form := &PhotoSearchForm{Query: "camera:cat"} + form := &PhotoSearch{Query: "camera:cat"} err := form.ParseQueryString() @@ -93,7 +93,7 @@ func TestParseQueryString(t *testing.T) { assert.Equal(t, "strconv.Atoi: parsing \"cat\": invalid syntax", err.Error()) }) t.Run("query for before with invalid type", func(t *testing.T) { - form := &PhotoSearchForm{Query: "before:cat"} + form := &PhotoSearch{Query: "before:cat"} err := form.ParseQueryString() diff --git a/internal/form/photo_uuids.go b/internal/form/photo_uuids.go new file mode 100644 index 000000000..e42419efb --- /dev/null +++ b/internal/form/photo_uuids.go @@ -0,0 +1,5 @@ +package form + +type PhotoUUIDs struct { + Photos []string `json:"photos"` +} diff --git a/internal/photoprism/search.go b/internal/photoprism/search.go index ba8e6d97b..6b1eb8391 100644 --- a/internal/photoprism/search.go +++ b/internal/photoprism/search.go @@ -7,7 +7,7 @@ import ( "github.com/gosimple/slug" "github.com/jinzhu/gorm" - "github.com/photoprism/photoprism/internal/forms" + "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/models" "github.com/photoprism/photoprism/internal/util" ) @@ -37,12 +37,12 @@ func NewSearch(originalsPath string, db *gorm.DB) *Search { } // Photos searches for photos based on a Form and returns a PhotoSearchResult slice. -func (s *Search) Photos(form forms.PhotoSearchForm) (results []PhotoSearchResult, err error) { - if err := form.ParseQueryString(); err != nil { +func (s *Search) Photos(f form.PhotoSearch) (results []PhotoSearchResult, err error) { + if err := f.ParseQueryString(); err != nil { return results, err } - defer util.ProfileTime(time.Now(), fmt.Sprintf("search: %+v", form)) + defer util.ProfileTime(time.Now(), fmt.Sprintf("search: %+v", f)) q := s.db.NewScope(nil).DB() @@ -72,10 +72,10 @@ func (s *Search) Photos(form forms.PhotoSearchForm) (results []PhotoSearchResult var label models.Label var labelIds []uint - if form.Label != "" { - if result := s.db.First(&label, "label_slug = ?", strings.ToLower(form.Label)); result.Error != nil { - log.Errorf("search: label \"%s\" not found", form.Label) - return results, fmt.Errorf("label \"%s\" not found", form.Label) + if f.Label != "" { + if result := s.db.First(&label, "label_slug = ?", strings.ToLower(f.Label)); result.Error != nil { + log.Errorf("search: label \"%s\" not found", f.Label) + return results, fmt.Errorf("label \"%s\" not found", f.Label) } else { labelIds = append(labelIds, label.ID) @@ -89,24 +89,24 @@ func (s *Search) Photos(form forms.PhotoSearchForm) (results []PhotoSearchResult } } - if form.Location == true { + if f.Location == true { q = q.Where("location_id > 0") - if form.Query != "" { - likeString := "%" + strings.ToLower(form.Query) + "%" + if f.Query != "" { + likeString := "%" + strings.ToLower(f.Query) + "%" q = q.Where("LOWER(locations.loc_display_name) LIKE ?", likeString) } - } else if form.Query != "" { - slugString := slug.Make(form.Query) - lowerString := strings.ToLower(form.Query) + } else if f.Query != "" { + slugString := slug.Make(f.Query) + lowerString := strings.ToLower(f.Query) likeString := "%" + lowerString + "%" if result := s.db.First(&label, "label_slug = ?", slugString); result.Error != nil { - log.Infof("search: label \"%s\" not found", form.Query) + log.Infof("search: label \"%s\" not found", f.Query) q = q.Where("labels.label_slug = ? OR LOWER(photo_title) LIKE ? OR files.file_main_color = ?", slugString, likeString, lowerString) } else { - log.Infof("search: label \"%s\"", form.Query) + log.Infof("search: label \"%s\"", f.Query) labelIds = append(labelIds, label.ID) @@ -121,94 +121,94 @@ func (s *Search) Photos(form forms.PhotoSearchForm) (results []PhotoSearchResult } - if form.Album != "" { - q = q.Joins("JOIN photos_albums ON photos_albums.photo_uuid = photos.photo_uuid").Where("photos_albums.album_uuid = ?", form.Album) + if f.Album != "" { + q = q.Joins("JOIN photos_albums ON photos_albums.photo_uuid = photos.photo_uuid").Where("photos_albums.album_uuid = ?", f.Album) } - if form.Camera > 0 { - q = q.Where("photos.camera_id = ?", form.Camera) + if f.Camera > 0 { + q = q.Where("photos.camera_id = ?", f.Camera) } - if form.Color != "" { - q = q.Where("files.file_main_color = ?", strings.ToLower(form.Color)) + if f.Color != "" { + q = q.Where("files.file_main_color = ?", strings.ToLower(f.Color)) } - if form.Favorites { + if f.Favorites { q = q.Where("photos.photo_favorite = 1") } - if form.Country != "" { - q = q.Where("locations.loc_country_code = ?", form.Country) + if f.Country != "" { + q = q.Where("locations.loc_country_code = ?", f.Country) } - if form.Title != "" { - q = q.Where("LOWER(photos.photo_title) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(form.Title))) + if f.Title != "" { + q = q.Where("LOWER(photos.photo_title) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Title))) } - if form.Description != "" { - q = q.Where("LOWER(photos.photo_description) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(form.Description))) + if f.Description != "" { + q = q.Where("LOWER(photos.photo_description) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Description))) } - if form.Notes != "" { - q = q.Where("LOWER(photos.photo_notes) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(form.Notes))) + if f.Notes != "" { + q = q.Where("LOWER(photos.photo_notes) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Notes))) } - if form.Hash != "" { - q = q.Where("files.file_hash = ?", form.Hash) + if f.Hash != "" { + q = q.Where("files.file_hash = ?", f.Hash) } - if form.Duplicate { + if f.Duplicate { q = q.Where("files.file_duplicate = 1") } - if form.Portrait { + if f.Portrait { q = q.Where("files.file_portrait = 1") } - if form.Mono { + if f.Mono { q = q.Where("files.file_chroma = 0") - } else if form.Chroma > 9 { - q = q.Where("files.file_chroma > ?", form.Chroma) - } else if form.Chroma > 0 { - q = q.Where("files.file_chroma > 0 AND files.file_chroma <= ?", form.Chroma) + } else if f.Chroma > 9 { + q = q.Where("files.file_chroma > ?", f.Chroma) + } else if f.Chroma > 0 { + q = q.Where("files.file_chroma > 0 AND files.file_chroma <= ?", f.Chroma) } - if form.Fmin > 0 { - q = q.Where("photos.photo_f_number >= ?", form.Fmin) + if f.Fmin > 0 { + q = q.Where("photos.photo_f_number >= ?", f.Fmin) } - if form.Fmax > 0 { - q = q.Where("photos.photo_f_number <= ?", form.Fmax) + if f.Fmax > 0 { + q = q.Where("photos.photo_f_number <= ?", f.Fmax) } - if form.Dist == 0 { - form.Dist = 20 - } else if form.Dist > 1000 { - form.Dist = 1000 + if f.Dist == 0 { + f.Dist = 20 + } else if f.Dist > 1000 { + f.Dist = 1000 } // Inaccurate distance search, but probably 'good enough' for now - if form.Lat > 0 { - latMin := form.Lat - SearchRadius*float64(form.Dist) - latMax := form.Lat + SearchRadius*float64(form.Dist) + if f.Lat > 0 { + latMin := f.Lat - SearchRadius*float64(f.Dist) + latMax := f.Lat + SearchRadius*float64(f.Dist) q = q.Where("photos.photo_lat BETWEEN ? AND ?", latMin, latMax) } - if form.Long > 0 { - longMin := form.Long - SearchRadius*float64(form.Dist) - longMax := form.Long + SearchRadius*float64(form.Dist) + if f.Long > 0 { + longMin := f.Long - SearchRadius*float64(f.Dist) + longMax := f.Long + SearchRadius*float64(f.Dist) q = q.Where("photos.photo_long BETWEEN ? AND ?", longMin, longMax) } - if !form.Before.IsZero() { - q = q.Where("photos.taken_at <= ?", form.Before.Format("2006-01-02")) + if !f.Before.IsZero() { + q = q.Where("photos.taken_at <= ?", f.Before.Format("2006-01-02")) } - if !form.After.IsZero() { - q = q.Where("photos.taken_at >= ?", form.After.Format("2006-01-02")) + if !f.After.IsZero() { + q = q.Where("photos.taken_at >= ?", f.After.Format("2006-01-02")) } - switch form.Order { + switch f.Order { case "newest": q = q.Order("taken_at DESC") case "oldest": @@ -219,8 +219,8 @@ func (s *Search) Photos(form forms.PhotoSearchForm) (results []PhotoSearchResult q = q.Order("taken_at DESC") } - if form.Count > 0 && form.Count <= 1000 { - q = q.Limit(form.Count).Offset(form.Offset) + if f.Count > 0 && f.Count <= 1000 { + q = q.Limit(f.Count).Offset(f.Offset) } else { q = q.Limit(100).Offset(0) } @@ -242,6 +242,15 @@ func (s *Search) FindFiles(limit int, offset int) (files []models.File, err erro return files, nil } +// FindFilesByUUID +func (s *Search) FindFilesByUUID(u []string, limit int, offset int) (files []models.File, err error) { + if err := s.db.Where("(photo_uuid IN (?) AND file_primary = 1) OR file_uuid IN (?)", u, u).Preload("Photo").Limit(limit).Offset(offset).Find(&files).Error; err != nil { + return files, err + } + + return files, nil +} + // FindFileByID returns a mediafile given a certain ID. func (s *Search) FindFileByID(id string) (file models.File, err error) { if err := s.db.Where("id = ?", id).Preload("Photo").First(&file).Error; err != nil { @@ -303,12 +312,12 @@ func (s *Search) FindLabelThumbBySlug(labelSlug string) (file models.File, err e } // Labels searches labels based on their name. -func (s *Search) Labels(form forms.LabelSearchForm) (results []LabelSearchResult, err error) { - if err := form.ParseQueryString(); err != nil { +func (s *Search) Labels(f form.LabelSearch) (results []LabelSearchResult, err error) { + if err := f.ParseQueryString(); err != nil { return results, err } - defer util.ProfileTime(time.Now(), fmt.Sprintf("search: %+v", form)) + defer util.ProfileTime(time.Now(), fmt.Sprintf("search: %+v", f)) q := s.db.NewScope(nil).DB() @@ -320,15 +329,15 @@ func (s *Search) Labels(form forms.LabelSearchForm) (results []LabelSearchResult Where("labels.deleted_at IS NULL"). Group("labels.id") - if form.Query != "" { + if f.Query != "" { var labelIds []uint var categories []models.Category var label models.Label - likeString := "%" + strings.ToLower(form.Query) + "%" + likeString := "%" + strings.ToLower(f.Query) + "%" - if result := s.db.First(&label, "LOWER(label_name) LIKE LOWER(?)", form.Query); result.Error != nil { - log.Infof("search: label \"%s\" not found", form.Query) + if result := s.db.First(&label, "LOWER(label_name) LIKE LOWER(?)", f.Query); result.Error != nil { + log.Infof("search: label \"%s\" not found", f.Query) q = q.Where("LOWER(labels.label_name) LIKE ?", likeString) } else { @@ -340,31 +349,31 @@ func (s *Search) Labels(form forms.LabelSearchForm) (results []LabelSearchResult labelIds = append(labelIds, category.LabelID) } - log.Infof("search: labels %#v", form.Query) + log.Infof("search: labels %#v", f.Query) q = q.Where("labels.id IN (?) OR LOWER(labels.label_name) LIKE ?", labelIds, likeString) } } - if form.Favorites { + if f.Favorites { q = q.Where("labels.label_favorite = 1") } - if form.Priority != 0 { - q = q.Where("labels.label_priority > ?", form.Priority) + if f.Priority != 0 { + q = q.Where("labels.label_priority > ?", f.Priority) } else { q = q.Where("labels.label_priority >= -1") } - switch form.Order { + switch f.Order { case "slug": q = q.Order("labels.label_favorite DESC, label_slug ASC") default: q = q.Order("labels.label_favorite DESC, labels.label_priority DESC, label_count DESC, labels.created_at DESC") } - if form.Count > 0 && form.Count <= 1000 { - q = q.Limit(form.Count).Offset(form.Offset) + if f.Count > 0 && f.Count <= 1000 { + q = q.Limit(f.Count).Offset(f.Offset) } else { q = q.Limit(100).Offset(0) } @@ -402,12 +411,12 @@ func (s *Search) FindAlbumThumbByUUID(albumUUID string) (file models.File, err e } // Albums searches albums based on their name. -func (s *Search) Albums(form forms.AlbumSearchForm) (results []AlbumSearchResult, err error) { - if err := form.ParseQueryString(); err != nil { +func (s *Search) Albums(f form.AlbumSearch) (results []AlbumSearchResult, err error) { + if err := f.ParseQueryString(); err != nil { return results, err } - defer util.ProfileTime(time.Now(), fmt.Sprintf("search: %+v", form)) + defer util.ProfileTime(time.Now(), fmt.Sprintf("search: %+v", f)) q := s.db.NewScope(nil).DB() @@ -419,24 +428,24 @@ func (s *Search) Albums(form forms.AlbumSearchForm) (results []AlbumSearchResult Where("albums.deleted_at IS NULL"). Group("albums.id") - if form.Query != "" { - likeString := "%" + strings.ToLower(form.Query) + "%" + if f.Query != "" { + likeString := "%" + strings.ToLower(f.Query) + "%" q = q.Where("LOWER(albums.album_name) LIKE ?", likeString) } - if form.Favorites { + if f.Favorites { q = q.Where("albums.album_favorite = 1") } - switch form.Order { + switch f.Order { case "slug": q = q.Order("albums.album_favorite DESC, album_slug ASC") default: q = q.Order("albums.album_favorite DESC, album_count DESC, albums.created_at DESC") } - if form.Count > 0 && form.Count <= 1000 { - q = q.Limit(form.Count).Offset(form.Offset) + if f.Count > 0 && f.Count <= 1000 { + q = q.Limit(f.Count).Offset(f.Offset) } else { q = q.Limit(100).Offset(0) } diff --git a/internal/photoprism/search_test.go b/internal/photoprism/search_test.go index c944c6cd9..a4359fc5a 100644 --- a/internal/photoprism/search_test.go +++ b/internal/photoprism/search_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/photoprism/photoprism/internal/config" - "github.com/photoprism/photoprism/internal/forms" + "github.com/photoprism/photoprism/internal/form" ) func TestSearch_Photos_Query(t *testing.T) { @@ -16,12 +16,12 @@ func TestSearch_Photos_Query(t *testing.T) { search := NewSearch(conf.OriginalsPath(), conf.Db()) t.Run("normal query", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "animal" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "animal" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -30,12 +30,12 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos[0]) }) t.Run("label query", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "label:dog" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "label:dog" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -44,25 +44,25 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("invalid label query", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "label:xxx" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "label:xxx" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) assert.Equal(t, err.Error(), "label \"xxx\" not found") t.Log(photos) }) t.Run("form.location true", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "" - form.Count = 3 - form.Offset = 0 - form.Location = true + var f form.PhotoSearch + f.Query = "" + f.Count = 3 + f.Offset = 0 + f.Location = true - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -71,13 +71,13 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.camera", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "" - form.Count = 3 - form.Offset = 0 - form.Camera = 2 + var f form.PhotoSearch + f.Query = "" + f.Count = 3 + f.Offset = 0 + f.Camera = 2 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -86,13 +86,13 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.color", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "" - form.Count = 3 - form.Offset = 0 - form.Color = "blue" + var f form.PhotoSearch + f.Query = "" + f.Count = 3 + f.Offset = 0 + f.Color = "blue" - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -101,12 +101,12 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.favorites", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "favorites:true" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "favorites:true" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -115,12 +115,12 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.country", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "country:de" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "country:de" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -129,12 +129,12 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.title", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "title:Pug Dog" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "title:Pug Dog" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -143,12 +143,12 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.description", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "description:xxx" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "description:xxx" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -157,12 +157,12 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.notes", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "notes:xxx" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "notes:xxx" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -171,12 +171,12 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.hash", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "hash:xxx" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "hash:xxx" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -185,12 +185,12 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.duplicate", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "duplicate:true" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "duplicate:true" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -199,12 +199,12 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.portrait", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "portrait:true" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "portrait:true" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -213,12 +213,12 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.mono", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "mono:true" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "mono:true" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -227,12 +227,12 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.chroma", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "chroma:50" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "chroma:50" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -241,12 +241,12 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.fmin and Order:oldest", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "Fmin:5 Order:oldest" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "Fmin:5 Order:oldest" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -255,12 +255,12 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.fmax and Order:newest", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "Fmax:2 Order:newest" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "Fmax:2 Order:newest" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -269,12 +269,12 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.Lat and form.Long and Order:imported", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "Lat:33.45343166666667 Long:25.764711666666667 Dist:2000 Order:imported" - form.Count = 3 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "Lat:33.45343166666667 Long:25.764711666666667 Dist:2000 Order:imported" + f.Count = 3 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) @@ -283,12 +283,12 @@ func TestSearch_Photos_Query(t *testing.T) { t.Log(photos) }) t.Run("form.Before and form.After", func(t *testing.T) { - var form forms.PhotoSearchForm - form.Query = "Before:2005-01-01 After:2003-01-01" - form.Count = 5000 - form.Offset = 0 + var f form.PhotoSearch + f.Query = "Before:2005-01-01 After:2003-01-01" + f.Count = 5000 + f.Offset = 0 - photos, err := search.Photos(form) + photos, err := search.Photos(f) if err != nil { t.Fatal(err) diff --git a/internal/server/routes.go b/internal/server/routes.go index 9fd41befd..71a3b6143 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -23,6 +23,8 @@ func registerRoutes(router *gin.Engine, conf *config.Config) { api.GetThumbnail(v1, conf) api.GetDownload(v1, conf) + api.CreateZip(v1, conf) + api.DownloadZip(v1, conf) api.GetPhotos(v1, conf) api.LikePhoto(v1, conf) diff --git a/internal/util/zip.go b/internal/util/zip.go new file mode 100644 index 000000000..a998827c7 --- /dev/null +++ b/internal/util/zip.go @@ -0,0 +1,61 @@ +package util + +import ( + "archive/zip" + "io" + "os" +) + +// ZipFiles compresses one or many files into a single zip archive file. +// Param 1: filename is the output zip file's name. +// Param 2: files is a list of files to add to the zip. +func ZipFiles(filename string, files []string) error { + newZipFile, err := os.Create(filename) + if err != nil { + return err + } + defer newZipFile.Close() + + zipWriter := zip.NewWriter(newZipFile) + defer zipWriter.Close() + + // Add files to zip + for _, file := range files { + if err = AddFileToZip(zipWriter, file); err != nil { + return err + } + } + + return nil +} + +func AddFileToZip(zipWriter *zip.Writer, filename string) error { + + fileToZip, err := os.Open(filename) + if err != nil { + return err + } + defer fileToZip.Close() + + // Get the file information + info, err := fileToZip.Stat() + if err != nil { + return err + } + + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + + // Change to deflate to gain better compression + // see http://golang.org/pkg/archive/zip/#pkg-constants + header.Method = zip.Deflate + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + _, err = io.Copy(writer, fileToZip) + return err +}