Albums: Zip download #15

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer
2019-12-05 19:21:35 +01:00
parent a7ef85c467
commit 25d0e92036
24 changed files with 531 additions and 301 deletions

View File

@@ -61,7 +61,7 @@
small
title="Download"
color="teal accent-4"
@click.stop="batchDownload()"
@click.stop="downloadZip()"
class="p-photo-clipboard-download"
>
<v-icon>save</v-icon>
@@ -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');
},

View File

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

View File

@@ -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(&params); 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(&params); 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,24 +94,24 @@ func BatchPhotosStory(router *gin.RouterGroup, conf *config.Config) {
start := time.Now()
var params PhotoUUIDs
var f form.PhotoUUIDs
if err := c.BindJSON(&params); 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{}{
db.Model(models.Photo{}).Where("photo_uuid IN (?)", f.Photos).Updates(map[string]interface{}{
"photo_story": gorm.Expr("IF (`photo_story`, 0, 1)"),
})

View File

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

View File

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

View File

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

View File

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

View File

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

148
internal/api/zip.go Normal file
View File

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

5
internal/form/album.go Normal file
View File

@@ -0,0 +1,5 @@
package form
type Album struct {
AlbumName string `json:"AlbumName"`
}

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package forms
package form
import (
"os"

View File

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

View File

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

6
internal/form/login.go Normal file
View File

@@ -0,0 +1,6 @@
package form
type Login struct {
Email string `json:"email"`
Password string `json:"password"`
}

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
package form
type PhotoUUIDs struct {
Photos []string `json:"photos"`
}

View File

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

View File

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

View File

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

61
internal/util/zip.go Normal file
View File

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