Backend: Query package refactoring

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer
2020-05-08 15:41:01 +02:00
parent 695294fc58
commit 842da9f09b
66 changed files with 1153 additions and 1229 deletions

View File

@@ -10,7 +10,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/workers"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -25,7 +25,6 @@ func GetAccounts(router *gin.RouterGroup, conf *config.Config) {
var f form.AccountSearch
q := service.Query()
err := c.MustBindWith(&f, binding.Form)
if err != nil {
@@ -33,7 +32,7 @@ func GetAccounts(router *gin.RouterGroup, conf *config.Config) {
return
}
result, err := q.Accounts(f)
result, err := query.Accounts(f)
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
@@ -59,10 +58,9 @@ func GetAccount(router *gin.RouterGroup, conf *config.Config) {
return
}
q := service.Query()
id := ParseUint(c.Param("id"))
if m, err := q.AccountByID(id); err == nil {
if m, err := query.AccountByID(id); err == nil {
c.JSON(http.StatusOK, m)
} else {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)
@@ -81,10 +79,9 @@ func GetAccountDirs(router *gin.RouterGroup, conf *config.Config) {
return
}
q := service.Query()
id := ParseUint(c.Param("id"))
m, err := q.AccountByID(id)
m, err := query.AccountByID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)
@@ -114,10 +111,9 @@ func ShareWithAccount(router *gin.RouterGroup, conf *config.Config) {
return
}
q := service.Query()
id := ParseUint(c.Param("id"))
m, err := q.AccountByID(id)
m, err := query.AccountByID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)
@@ -132,7 +128,7 @@ func ShareWithAccount(router *gin.RouterGroup, conf *config.Config) {
}
dst := f.Destination
files, err := q.FilesByUUID(f.Photos, 1000, 0)
files, err := query.FilesByUUID(f.Photos, 1000, 0)
if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
@@ -202,9 +198,7 @@ func UpdateAccount(router *gin.RouterGroup, conf *config.Config) {
id := ParseUint(c.Param("id"))
q := service.Query()
m, err := q.AccountByID(id)
m, err := query.AccountByID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -236,7 +230,7 @@ func UpdateAccount(router *gin.RouterGroup, conf *config.Config) {
event.Success("account saved")
m, err = q.AccountByID(id)
m, err = query.AccountByID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)
@@ -259,9 +253,8 @@ func DeleteAccount(router *gin.RouterGroup, conf *config.Config) {
}
id := ParseUint(c.Param("id"))
q := service.Query()
m, err := q.AccountByID(id)
m, err := query.AccountByID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)

View File

@@ -14,7 +14,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
@@ -35,7 +35,6 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
var f form.AlbumSearch
q := service.Query()
err := c.MustBindWith(&f, binding.Form)
if err != nil {
@@ -43,7 +42,7 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
return
}
result, err := q.Albums(f)
result, err := query.Albums(f)
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
return
@@ -61,8 +60,7 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
func GetAlbum(router *gin.RouterGroup, conf *config.Config) {
router.GET("/albums/:uuid", func(c *gin.Context) {
id := c.Param("uuid")
q := service.Query()
m, err := q.AlbumByUUID(id)
m, err := query.AlbumByUUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@@ -88,13 +86,12 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
return
}
q := service.Query()
m := entity.NewAlbum(f.AlbumName)
m.AlbumFavorite = f.AlbumFavorite
log.Debugf("create album: %+v %+v", f, m)
if res := conf.Db().Create(m); res.Error != nil {
if res := entity.Db().Create(m); res.Error != nil {
log.Error(res.Error.Error())
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("%s already exists", txt.Quote(m.AlbumName))})
return
@@ -104,7 +101,7 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
event.Publish("config.updated", event.Data(conf.ClientConfig()))
PublishAlbumEvent(EntityCreated, m.AlbumUUID, c, q)
PublishAlbumEvent(EntityCreated, m.AlbumUUID, c)
c.JSON(http.StatusOK, m)
})
@@ -119,9 +116,7 @@ func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) {
}
uuid := c.Param("uuid")
q := service.Query()
m, err := q.AlbumByUUID(uuid)
m, err := query.AlbumByUUID(uuid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@@ -151,7 +146,7 @@ func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) {
event.Publish("config.updated", event.Data(conf.ClientConfig()))
event.Success("album saved")
PublishAlbumEvent(EntityUpdated, uuid, c, q)
PublishAlbumEvent(EntityUpdated, uuid, c)
c.JSON(http.StatusOK, m)
})
@@ -166,16 +161,15 @@ func DeleteAlbum(router *gin.RouterGroup, conf *config.Config) {
}
id := c.Param("uuid")
q := service.Query()
m, err := q.AlbumByUUID(id)
m, err := query.AlbumByUUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
return
}
PublishAlbumEvent(EntityDeleted, id, c, q)
PublishAlbumEvent(EntityDeleted, id, c)
conf.Db().Delete(&m)
@@ -198,9 +192,7 @@ func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
}
id := c.Param("uuid")
q := service.Query()
album, err := q.AlbumByUUID(id)
album, err := query.AlbumByUUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@@ -211,7 +203,7 @@ func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
conf.Db().Save(&album)
event.Publish("config.updated", event.Data(conf.ClientConfig()))
PublishAlbumEvent(EntityUpdated, id, c, q)
PublishAlbumEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, http.Response{})
})
@@ -229,8 +221,7 @@ func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
}
id := c.Param("uuid")
q := service.Query()
album, err := q.AlbumByUUID(id)
album, err := query.AlbumByUUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@@ -241,7 +232,7 @@ func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
conf.Db().Save(&album)
event.Publish("config.updated", event.Data(conf.ClientConfig()))
PublishAlbumEvent(EntityUpdated, id, c, q)
PublishAlbumEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, http.Response{})
})
@@ -263,15 +254,14 @@ func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) {
}
uuid := c.Param("uuid")
q := service.Query()
a, err := q.AlbumByUUID(uuid)
a, err := query.AlbumByUUID(uuid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
return
}
photos, err := q.PhotoSelection(f)
photos, err := query.PhotoSelection(f)
if err != nil {
log.Errorf("album: %s", err)
@@ -291,7 +281,7 @@ func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) {
event.Success(fmt.Sprintf("%d photos added to %s", len(added), a.AlbumName))
}
PublishAlbumEvent(EntityUpdated, a.AlbumUUID, c, q)
PublishAlbumEvent(EntityUpdated, a.AlbumUUID, c)
c.JSON(http.StatusOK, gin.H{"message": "photos added to album", "album": a, "added": added})
})
@@ -318,21 +308,18 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup, conf *config.Config) {
return
}
q := service.Query()
a, err := q.AlbumByUUID(c.Param("uuid"))
a, err := query.AlbumByUUID(c.Param("uuid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
return
}
db := conf.Db()
db.Where("album_uuid = ? AND photo_uuid IN (?)", a.AlbumUUID, f.Photos).Delete(&entity.PhotoAlbum{})
entity.Db().Where("album_uuid = ? AND photo_uuid IN (?)", a.AlbumUUID, f.Photos).Delete(&entity.PhotoAlbum{})
event.Success(fmt.Sprintf("photos removed from %s", a.AlbumName))
PublishAlbumEvent(EntityUpdated, a.AlbumUUID, c, q)
PublishAlbumEvent(EntityUpdated, a.AlbumUUID, c)
c.JSON(http.StatusOK, gin.H{"message": "photos removed from album", "album": a, "photos": f.Photos})
})
@@ -343,15 +330,14 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
router.GET("/albums/:uuid/download", func(c *gin.Context) {
start := time.Now()
q := service.Query()
a, err := q.AlbumByUUID(c.Param("uuid"))
a, err := query.AlbumByUUID(c.Param("uuid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
return
}
p, _, err := q.Photos(form.PhotoSearch{
p, _, err := query.Photos(form.PhotoSearch{
Album: a.AlbumUUID,
Count: 10000,
Offset: 0,
@@ -384,7 +370,7 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
defer newZipFile.Close()
zipWriter := zip.NewWriter(newZipFile)
defer zipWriter.Close()
defer func() { _ = zipWriter.Close() }()
for _, f := range p {
fileName := path.Join(conf.OriginalsPath(), f.FileName)
@@ -405,7 +391,7 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
}
log.Infof("album: archive %s created in %s", txt.Quote(zipBaseName), time.Since(start))
zipWriter.Close()
_ = zipWriter.Close()
newZipFile.Close()
if !fs.FileExists(zipFileName) {
@@ -443,8 +429,6 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
return
}
q := service.Query()
gc := conf.Cache()
cacheKey := fmt.Sprintf("album-thumbnail:%s:%s", uuid, typeName)
@@ -454,7 +438,7 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
return
}
f, err := q.AlbumThumbByUUID(uuid)
f, err := query.AlbumThumbByUUID(uuid)
if err != nil {
log.Debugf("album: no photos yet, using generic image for %s", uuid)
@@ -470,7 +454,7 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
// Set missing flag so that the file doesn't show up in search results anymore
f.FileMissing = true
conf.Db().Save(&f)
entity.Db().Save(&f)
return
}

View File

@@ -41,9 +41,11 @@ func BatchPhotosArchive(router *gin.RouterGroup, conf *config.Config) {
log.Infof("photos: archiving %#v", f.Photos)
db := conf.Db()
entity.Db().Where("photo_uuid IN (?)", f.Photos).Delete(&entity.Photo{})
db.Where("photo_uuid IN (?)", f.Photos).Delete(&entity.Photo{})
if err := query.UpdatePhotoCounts(); err != nil {
log.Errorf("photos: %s", err)
}
elapsed := int(time.Since(start).Seconds())
@@ -80,9 +82,7 @@ func BatchPhotosRestore(router *gin.RouterGroup, conf *config.Config) {
log.Infof("restoring photos: %#v", f.Photos)
db := conf.Db()
db.Unscoped().Model(&entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).
entity.Db().Unscoped().Model(&entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).
UpdateColumn("deleted_at", gorm.Expr("NULL"))
elapsed := int(time.Since(start).Seconds())
@@ -118,10 +118,8 @@ func BatchAlbumsDelete(router *gin.RouterGroup, conf *config.Config) {
log.Infof("albums: deleting %#v", f.Albums)
db := conf.Db()
db.Where("album_uuid IN (?)", f.Albums).Delete(&entity.Album{})
db.Where("album_uuid IN (?)", f.Albums).Delete(&entity.PhotoAlbum{})
entity.Db().Where("album_uuid IN (?)", f.Albums).Delete(&entity.Album{})
entity.Db().Where("album_uuid IN (?)", f.Albums).Delete(&entity.PhotoAlbum{})
event.Publish("config.updated", event.Data(conf.ClientConfig()))
@@ -156,18 +154,14 @@ func BatchPhotosPrivate(router *gin.RouterGroup, conf *config.Config) {
log.Infof("marking photos as private: %#v", f.Photos)
db := conf.Db()
err := db.Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).UpdateColumn("photo_private", gorm.Expr("IF (`photo_private`, 0, 1)")).Error
err := entity.Db().Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).UpdateColumn("photo_private", gorm.Expr("IF (`photo_private`, 0, 1)")).Error
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, ErrSaveFailed)
return
}
q := query.New(db)
if entities, err := q.PhotoSelection(f); err == nil {
if entities, err := query.PhotoSelection(f); err == nil {
event.EntitiesUpdated("photos", entities)
}
@@ -204,9 +198,7 @@ func BatchPhotosStory(router *gin.RouterGroup, conf *config.Config) {
log.Infof("marking photos as story: %#v", f.Photos)
db := conf.Db()
db.Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).Updates(map[string]interface{}{
entity.Db().Model(entity.Photo{}).Where("photo_uuid IN (?)", f.Photos).Updates(map[string]interface{}{
"photo_story": gorm.Expr("IF (`photo_story`, 0, 1)"),
})
@@ -239,9 +231,7 @@ func BatchLabelsDelete(router *gin.RouterGroup, conf *config.Config) {
log.Infof("labels: deleting %#v", f.Labels)
db := conf.Db()
db.Where("label_uuid IN (?)", f.Labels).Delete(&entity.Label{})
entity.Db().Where("label_uuid IN (?)", f.Labels).Delete(&entity.Label{})
event.Publish("config.updated", event.Data(conf.ClientConfig()))

View File

@@ -5,7 +5,8 @@ import (
"path"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/gin-gonic/gin"
@@ -23,8 +24,7 @@ func GetDownload(router *gin.RouterGroup, conf *config.Config) {
router.GET("/download/:hash", func(c *gin.Context) {
fileHash := c.Param("hash")
q := service.Query()
f, err := q.FileByHash(fileHash)
f, err := query.FileByHash(fileHash)
if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
@@ -39,7 +39,7 @@ func GetDownload(router *gin.RouterGroup, conf *config.Config) {
// Set missing flag so that the file doesn't show up in search results anymore
f.FileMissing = true
conf.Db().Save(&f)
entity.Db().Save(&f)
return
}

View File

@@ -17,9 +17,9 @@ const (
EntityDeleted EntityEvent = "deleted"
)
func PublishPhotoEvent(e EntityEvent, uuid string, c *gin.Context, q *query.Query) {
func PublishPhotoEvent(e EntityEvent, uuid string, c *gin.Context) {
f := form.PhotoSearch{ID: uuid, Merged: true}
result, _, err := q.Photos(f)
result, _, err := query.Photos(f)
if err != nil {
log.Error(err)
@@ -30,9 +30,9 @@ func PublishPhotoEvent(e EntityEvent, uuid string, c *gin.Context, q *query.Quer
event.PublishEntities("photos", string(e), result)
}
func PublishAlbumEvent(e EntityEvent, uuid string, c *gin.Context, q *query.Query) {
func PublishAlbumEvent(e EntityEvent, uuid string, c *gin.Context) {
f := form.AlbumSearch{ID: uuid}
result, err := q.Albums(f)
result, err := query.Albums(f)
if err != nil {
log.Error(err)
@@ -43,9 +43,9 @@ func PublishAlbumEvent(e EntityEvent, uuid string, c *gin.Context, q *query.Quer
event.PublishEntities("albums", string(e), result)
}
func PublishLabelEvent(e EntityEvent, uuid string, c *gin.Context, q *query.Query) {
func PublishLabelEvent(e EntityEvent, uuid string, c *gin.Context) {
f := form.LabelSearch{ID: uuid}
result, err := q.Labels(f)
result, err := query.Labels(f)
if err != nil {
log.Error(err)

View File

@@ -5,9 +5,9 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -22,8 +22,7 @@ func GetFile(router *gin.RouterGroup, conf *config.Config) {
return
}
q := service.Query()
p, err := q.FileByHash(c.Param("hash"))
p, err := query.FileByHash(c.Param("hash"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -45,10 +44,7 @@ func LinkFile(router *gin.RouterGroup, conf *config.Config) {
return
}
db := conf.Db()
q := query.New(db)
m, err := q.FileByUUID(c.Param("uuid"))
m, err := query.FileByUUID(c.Param("uuid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrFileNotFound)
@@ -59,7 +55,7 @@ func LinkFile(router *gin.RouterGroup, conf *config.Config) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
return
} else {
db.Model(&m).Association("Links").Append(link)
entity.Db().Model(&m).Association("Links").Append(link)
}
event.Success("created file share link")

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/gin-gonic/gin"
@@ -24,7 +24,6 @@ func GetGeo(router *gin.RouterGroup, conf *config.Config) {
var f form.GeoSearch
q := service.Query()
err := c.MustBindWith(&f, binding.Form)
if err != nil {
@@ -32,7 +31,7 @@ func GetGeo(router *gin.RouterGroup, conf *config.Config) {
return
}
photos, err := q.Geo(f)
photos, err := query.Geo(f)
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})

View File

@@ -11,9 +11,10 @@ import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
@@ -29,7 +30,6 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) {
var f form.LabelSearch
q := service.Query()
err := c.MustBindWith(&f, binding.Form)
if err != nil {
@@ -37,7 +37,8 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) {
return
}
result, err := q.Labels(f)
result, err := query.Labels(f)
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
return
@@ -67,9 +68,7 @@ func UpdateLabel(router *gin.RouterGroup, conf *config.Config) {
}
id := c.Param("uuid")
q := service.Query()
m, err := q.LabelByUUID(id)
m, err := query.LabelByUUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrLabelNotFound)
@@ -77,11 +76,11 @@ func UpdateLabel(router *gin.RouterGroup, conf *config.Config) {
}
m.SetName(f.LabelName)
conf.Db().Save(&m)
entity.Db().Save(&m)
event.Success("label saved")
PublishLabelEvent(EntityUpdated, id, c, q)
PublishLabelEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, m)
})
@@ -99,9 +98,7 @@ func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
}
id := c.Param("uuid")
q := service.Query()
label, err := q.LabelByUUID(id)
label, err := query.LabelByUUID(id)
if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": txt.UcFirst(err.Error())})
@@ -109,7 +106,7 @@ func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
}
label.LabelFavorite = true
conf.Db().Save(&label)
entity.Db().Save(&label)
if label.LabelPriority < 0 {
event.Publish("count.labels", event.Data{
@@ -117,7 +114,7 @@ func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
})
}
PublishLabelEvent(EntityUpdated, id, c, q)
PublishLabelEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, http.Response{})
})
@@ -135,9 +132,7 @@ func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
}
id := c.Param("uuid")
q := service.Query()
label, err := q.LabelByUUID(id)
label, err := query.LabelByUUID(id)
if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": txt.UcFirst(err.Error())})
@@ -145,7 +140,7 @@ func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
}
label.LabelFavorite = false
conf.Db().Save(&label)
entity.Db().Save(&label)
if label.LabelPriority < 0 {
event.Publish("count.labels", event.Data{
@@ -153,7 +148,7 @@ func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
})
}
PublishLabelEvent(EntityUpdated, id, c, q)
PublishLabelEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, http.Response{})
})
@@ -180,8 +175,6 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
return
}
q := service.Query()
gc := conf.Cache()
cacheKey := fmt.Sprintf("label-thumbnail:%s:%s", labelUUID, typeName)
@@ -191,7 +184,7 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
return
}
f, err := q.LabelThumbByUUID(labelUUID)
f, err := query.LabelThumbByUUID(labelUUID)
if err != nil {
log.Errorf(err.Error())

View File

@@ -39,10 +39,7 @@ func LinkAlbum(router *gin.RouterGroup, conf *config.Config) {
return
}
db := conf.Db()
q := query.New(db)
m, err := q.AlbumByUUID(c.Param("uuid"))
m, err := query.AlbumByUUID(c.Param("uuid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
@@ -53,7 +50,7 @@ func LinkAlbum(router *gin.RouterGroup, conf *config.Config) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
return
} else {
db.Model(&m).Association("Links").Append(link)
entity.Db().Model(&m).Association("Links").Append(link)
}
event.Success("created album share link")
@@ -70,10 +67,7 @@ func LinkPhoto(router *gin.RouterGroup, conf *config.Config) {
return
}
db := conf.Db()
q := query.New(db)
m, err := q.PhotoByUUID(c.Param("uuid"))
m, err := query.PhotoByUUID(c.Param("uuid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -84,7 +78,7 @@ func LinkPhoto(router *gin.RouterGroup, conf *config.Config) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
return
} else {
db.Model(&m).Association("Links").Append(link)
entity.Db().Model(&m).Association("Links").Append(link)
}
event.Success("created photo share link")
@@ -101,10 +95,7 @@ func LinkLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
db := conf.Db()
q := query.New(db)
m, err := q.LabelByUUID(c.Param("uuid"))
m, err := query.LabelByUUID(c.Param("uuid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrLabelNotFound)
@@ -115,7 +106,7 @@ func LinkLabel(router *gin.RouterGroup, conf *config.Config) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
return
} else {
db.Model(&m).Association("Links").Append(link)
entity.Db().Model(&m).Association("Links").Append(link)
}
event.Success("created label share link")

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/gin-gonic/gin"
@@ -18,9 +18,8 @@ func GetMomentsTime(router *gin.RouterGroup, conf *config.Config) {
return
}
q := service.Query()
result, err := query.GetMomentsTime()
result, err := q.GetMomentsTime()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
return

View File

@@ -11,7 +11,6 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/fs"
)
@@ -26,8 +25,7 @@ func GetPhoto(router *gin.RouterGroup, conf *config.Config) {
return
}
q := service.Query()
p, err := q.PreloadPhotoByUUID(c.Param("uuid"))
p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -46,11 +44,8 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
return
}
db := conf.Db()
uuid := c.Param("uuid")
q := query.New(db)
m, err := q.PhotoByUUID(uuid)
m, err := query.PhotoByUUID(uuid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -81,11 +76,15 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
return
}
PublishPhotoEvent(EntityUpdated, uuid, c, q)
if err := query.UpdatePhotoCounts(); err != nil {
log.Errorf("photo: %s", err)
}
PublishPhotoEvent(EntityUpdated, uuid, c)
event.Success("photo saved")
p, err := q.PreloadPhotoByUUID(uuid)
p, err := query.PreloadPhotoByUUID(uuid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -102,8 +101,7 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
// uuid: string PhotoUUID as returned by the API
func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
router.GET("/photos/:uuid/download", func(c *gin.Context) {
q := service.Query()
f, err := q.FileByPhotoUUID(c.Param("uuid"))
f, err := query.FileByPhotoUUID(c.Param("uuid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -142,8 +140,7 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
}
id := c.Param("uuid")
q := service.Query()
m, err := q.PhotoByUUID(id)
m, err := query.PhotoByUUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -158,7 +155,7 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
"count": 1,
})
PublishPhotoEvent(EntityUpdated, id, c, q)
PublishPhotoEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, gin.H{"photo": m})
})
@@ -176,8 +173,7 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
}
id := c.Param("uuid")
q := service.Query()
m, err := q.PhotoByUUID(id)
m, err := query.PhotoByUUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -186,13 +182,13 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
m.PhotoFavorite = false
m.PhotoQuality = m.QualityScore()
conf.Db().Save(&m)
entity.Db().Save(&m)
event.Publish("count.favorites", event.Data{
"count": -1,
})
PublishPhotoEvent(EntityUpdated, id, c, q)
PublishPhotoEvent(EntityUpdated, id, c)
c.JSON(http.StatusOK, gin.H{"photo": m})
})
@@ -209,23 +205,20 @@ func SetPhotoPrimary(router *gin.RouterGroup, conf *config.Config) {
return
}
db := conf.Db()
uuid := c.Param("uuid")
fileUUID := c.Param("file_uuid")
q := query.New(db)
err := q.SetPhotoPrimary(uuid, fileUUID)
err := query.SetPhotoPrimary(uuid, fileUUID)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
return
}
PublishPhotoEvent(EntityUpdated, uuid, c, q)
PublishPhotoEvent(EntityUpdated, uuid, c)
event.Success("photo saved")
p, err := q.PreloadPhotoByUUID(uuid)
p, err := query.PreloadPhotoByUUID(uuid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)

View File

@@ -10,7 +10,6 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -25,9 +24,7 @@ func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
q := service.Query()
m, err := q.PhotoByUUID(c.Param("uuid"))
db := conf.Db()
m, err := query.PhotoByUUID(c.Param("uuid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -55,14 +52,14 @@ func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
plm.Uncertainty = f.Uncertainty
plm.LabelSrc = entity.SrcManual
if err := db.Save(&plm).Error; err != nil {
if err := entity.Db().Save(&plm).Error; err != nil {
log.Errorf("label: %s", err)
}
}
db.Save(&lm)
entity.Db().Save(&lm)
p, err := q.PreloadPhotoByUUID(c.Param("uuid"))
p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -74,7 +71,7 @@ func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c, q)
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c)
event.Success("label updated")
@@ -94,9 +91,7 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
db := conf.Db()
q := query.New(db)
m, err := q.PhotoByUUID(c.Param("uuid"))
m, err := query.PhotoByUUID(c.Param("uuid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -110,7 +105,7 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
label, err := q.PhotoLabel(m.ID, uint(labelId))
label, err := query.PhotoLabel(m.ID, uint(labelId))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UcFirst(err.Error())})
@@ -118,13 +113,13 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
}
if label.LabelSrc == entity.SrcManual {
db.Delete(&label)
entity.Db().Delete(&label)
} else {
label.Uncertainty = 100
db.Save(&label)
entity.Db().Save(&label)
}
p, err := q.PreloadPhotoByUUID(c.Param("uuid"))
p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -136,7 +131,7 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c, q)
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c)
event.Success("label removed")
@@ -158,9 +153,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
// TODO: Code clean-up, simplify
db := conf.Db()
q := query.New(db)
m, err := q.PhotoByUUID(c.Param("uuid"))
m, err := query.PhotoByUUID(c.Param("uuid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -174,7 +167,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
label, err := q.PhotoLabel(m.ID, uint(labelId))
label, err := query.PhotoLabel(m.ID, uint(labelId))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UcFirst(err.Error())})
@@ -191,7 +184,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
p, err := q.PreloadPhotoByUUID(c.Param("uuid"))
p, err := query.PreloadPhotoByUUID(c.Param("uuid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@@ -203,7 +196,7 @@ func UpdatePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c, q)
PublishPhotoEvent(EntityUpdated, c.Param("uuid"), c)
event.Success("label saved")

View File

@@ -5,7 +5,7 @@ import (
"strconv"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/gin-gonic/gin"
@@ -36,7 +36,6 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) {
var f form.PhotoSearch
q := service.Query()
err := c.MustBindWith(&f, binding.Form)
if err != nil {
@@ -44,7 +43,7 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) {
return
}
result, count, err := q.Photos(f)
result, count, err := query.Photos(f)
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})

View File

@@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs"
@@ -31,9 +32,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
return
}
db := conf.Db()
q := query.New(db)
f, err := q.FileByHash(fileHash)
f, err := query.FileByHash(fileHash)
if err != nil {
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
@@ -53,7 +52,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
// Set missing flag so that the file doesn't show up in search results anymore
f.FileMissing = true
db.Save(&f)
entity.Db().Save(&f)
return
}

View File

@@ -13,7 +13,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs"
)
@@ -45,8 +45,7 @@ func GetPreview(router *gin.RouterGroup, conf *config.Config) {
f.Count = 12
f.Order = "relevance"
q := service.Query()
p, _, err := q.Photos(f)
p, _, err := query.Photos(f)
if err != nil {
log.Error(err)

View File

@@ -12,7 +12,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt"
@@ -47,8 +47,7 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) {
return
}
q := service.Query()
files, err := q.FilesByUUID(f.Photos, 1000, 0)
files, err := query.FilesByUUID(f.Photos, 1000, 0)
if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})

View File

@@ -162,7 +162,7 @@ func (c *Config) ClientConfig() ClientConfig {
Take(&count)
db.Table("places").
Select("(COUNT(*) - 1) AS places").
Select("(SUM(photo_count > 0) - 1) AS places").
Take(&count)
type country struct {

View File

@@ -62,9 +62,9 @@ func (c *Config) InitDb() {
}
// ResetDb drops all tables in the currently configured database and re-creates them.
func (c *Config) ResetDb(testFixtures bool) {
func (c *Config) ResetDb() {
entity.SetDbProvider(c)
entity.ResetDb(testFixtures)
entity.InitTestFixtures()
}
// connectToDatabase establishes a database connection.

View File

@@ -102,7 +102,7 @@ func NewTestConfig() *Config {
log.Fatalf("config: %s", err.Error())
}
c.ResetDb(true)
c.ResetDb()
thumb.Size = c.ThumbSize()
thumb.Limit = c.ThumbLimit()

View File

@@ -20,13 +20,18 @@ func SetDbProvider(provider DbProvider) {
dbProvider = provider
}
// Db() returns a database connection.
// HasDbProvider returns true if a db provider exists.
func HasDbProvider() bool {
return dbProvider != nil
}
// Db returns a database connection.
func Db() *gorm.DB {
return dbProvider.Db()
}
// Db() returns an unscoped database connection.
func Unscoped() *gorm.DB {
// UnscopedDb returns an unscoped database connection.
func UnscopedDb() *gorm.DB {
return Db().Unscoped()
}

View File

@@ -10,6 +10,7 @@ https://github.com/photoprism/photoprism/wiki/Storage
package entity
import (
"sync"
"time"
"github.com/jinzhu/gorm"
@@ -17,6 +18,7 @@ import (
)
var log = event.Log
var resetFixturesOnce sync.Once
func logError(result *gorm.DB) {
if result.Error != nil {
@@ -84,27 +86,38 @@ func ResetDb(testFixtures bool) {
DropTables()
// Make sure changes have been written to disk.
time.Sleep(100 * time.Millisecond)
time.Sleep(200 * time.Millisecond)
MigrateDb()
if testFixtures {
// Make sure changes have been written to disk.
time.Sleep(100 * time.Millisecond)
time.Sleep(200 * time.Millisecond)
CreateTestFixtures()
}
}
// InitTestFixtures resets the database and test fixtures once.
func InitTestFixtures() {
resetFixturesOnce.Do(func() {
ResetDb(true)
})
}
// InitTestDb connects to and completely initializes the test database incl fixtures.
func InitTestDb(dsn string) *Gorm {
if HasDbProvider() {
return nil
}
db := &Gorm{
Driver: "mysql",
Dsn: dsn,
}
SetDbProvider(db)
ResetDb(true)
InitTestFixtures()
return db
}

View File

@@ -19,7 +19,9 @@ func TestMain(m *testing.M) {
code := m.Run()
if db != nil {
db.Close()
}
os.Exit(code)
}

View File

@@ -18,6 +18,7 @@ type Place struct {
LocKeywords string `gorm:"type:varchar(255);"`
LocNotes string `gorm:"type:text;"`
LocFavorite bool
PhotoCount int
CreatedAt time.Time
UpdatedAt time.Time
New bool `gorm:"-"`
@@ -33,6 +34,7 @@ var UnknownPlace = Place{
LocKeywords: "",
LocNotes: "",
LocFavorite: false,
PhotoCount: -1,
}
// CreateUnknownPlace initializes default place in the database

View File

@@ -1,9 +1,10 @@
package entity
import (
"github.com/stretchr/testify/assert"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCreateUnknownPlace(t *testing.T) {
@@ -32,7 +33,20 @@ func TestPlace_Find(t *testing.T) {
assert.Nil(t, r)
})
t.Run("record does not exist", func(t *testing.T) {
place := &Place{"1110", "test", "testCity", "", "", "", "", false, time.Now(), time.Now(), false}
place := &Place{
ID: "1110",
LocLabel: "test",
LocCity: "testCity",
LocState: "",
LocCountry: "",
LocKeywords: "",
LocNotes: "",
LocFavorite: false,
PhotoCount: 0,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
New: false,
}
r := place.Find()
assert.Equal(t, "record not found", r.Error())
})

View File

@@ -1,6 +1,8 @@
package event
import "fmt"
import (
"fmt"
)
func PublishEntities(name, ev string, entities interface{}) {
SharedHub().Publish(Message{

View File

@@ -14,6 +14,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -42,7 +43,7 @@ func (imp *Import) originalsPath() string {
}
// Start imports media files from a directory and converts/indexes them as needed.
func (imp *Import) Start(opt ImportOptions) {
func (imp *Import) Start(opt ImportOptions) map[string]bool {
var directories []string
done := make(map[string]bool)
ind := imp.index
@@ -50,19 +51,19 @@ func (imp *Import) Start(opt ImportOptions) {
if !fs.PathExists(importPath) {
event.Error(fmt.Sprintf("import: %s does not exist", importPath))
return
return done
}
if err := mutex.Worker.Start(); err != nil {
event.Error(fmt.Sprintf("import: %s", err.Error()))
return
return done
}
defer mutex.Worker.Stop()
if err := ind.tensorFlow.Init(); err != nil {
log.Errorf("import: %s", err.Error())
return
return done
}
jobs := make(chan ImportJob)
@@ -194,7 +195,13 @@ func (imp *Import) Start(opt ImportOptions) {
log.Error(err.Error())
}
if err := query.UpdatePhotoCounts(); err != nil {
log.Errorf("import: %s", err)
}
runtime.GC()
return done
}
// Cancel stops the current import operation.

View File

@@ -171,6 +171,10 @@ func (ind *Index) Start(opt IndexOptions) map[string]bool {
log.Error(err.Error())
}
if err := query.UpdatePhotoCounts(); err != nil {
log.Errorf("index: %s", err)
}
runtime.GC()
return done

View File

@@ -14,6 +14,7 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/meta"
"github.com/photoprism/photoprism/internal/nsfw"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -426,7 +427,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
downloadedAs = originalName
}
if err := ind.q.SetDownloadFileID(downloadedAs, file.ID); err != nil {
if err := query.SetDownloadFileID(downloadedAs, file.ID); err != nil {
log.Errorf("index: %s", err)
}

View File

@@ -4,12 +4,21 @@ import (
"os"
"testing"
"github.com/photoprism/photoprism/internal/entity"
"github.com/sirupsen/logrus"
)
func TestMain(m *testing.M) {
log = logrus.StandardLogger()
log.SetLevel(logrus.DebugLevel)
db := entity.InitTestDb(os.Getenv("PHOTOPRISM_TEST_DSN"))
code := m.Run()
if db != nil {
db.Close()
}
os.Exit(code)
}

View File

@@ -64,13 +64,11 @@ func (prg *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPh
runtime.GC()
}()
q := query.New(prg.conf.Db())
limit := 500
offset := 0
for {
files, err := q.ExistingFiles(limit, offset, opt.Path)
files, err := query.ExistingFiles(limit, offset, opt.Path)
if err != nil {
return purgedFiles, purgedPhotos, err
@@ -118,7 +116,7 @@ func (prg *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPh
offset = 0
for {
photos, err := q.MissingPhotos(limit, offset)
photos, err := query.MissingPhotos(limit, offset)
if err != nil {
return purgedFiles, purgedPhotos, err
@@ -163,11 +161,17 @@ func (prg *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPh
offset += limit
}
err = q.ResetPhotosQuality()
if err := query.ResetPhotosQuality(); err != nil {
return purgedFiles, purgedPhotos, err
}
if err := query.UpdatePhotoCounts(); err != nil {
return purgedFiles, purgedPhotos, err
}
return purgedFiles, purgedPhotos, nil
}
// Cancel stops the current purge operation.
func (prg *Purge) Cancel() {
mutex.Worker.Cancel()

View File

@@ -6,10 +6,8 @@ import (
)
// AccountUploads a list of files for uploading to a remote account.
func (q *Query) AccountUploads(a entity.Account, limit int) (results []entity.File, err error) {
s := q.db
s = s.Where("files.file_missing = 0").
func AccountUploads(a entity.Account, limit int) (results []entity.File, err error) {
s := Db().Where("files.file_missing = 0").
Where("files.id NOT IN (SELECT file_id FROM files_sync WHERE file_id > 0 AND account_id = ?)", a.ID)
if !a.SyncRaw {

View File

@@ -5,19 +5,13 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/config"
)
func TestQuery_AccountUploads(t *testing.T) {
conf := config.TestConfig()
q := New(conf.Db())
func TestAccountUploads(t *testing.T) {
a := entity.Account{ID: 1, SyncRaw: false}
t.Run("find uploads", func(t *testing.T) {
results, err := q.AccountUploads(a, 10)
results, err := AccountUploads(a, 10)
if err != nil {
t.Fatal(err)

View File

@@ -6,8 +6,8 @@ import (
)
// Accounts returns a list of accounts.
func (q *Query) Accounts(f form.AccountSearch) (result []entity.Account, err error) {
s := q.db.Where(&entity.Account{})
func Accounts(f form.AccountSearch) (result []entity.Account, err error) {
s := Db().Where(&entity.Account{})
if f.Share {
s = s.Where("acc_share = 1")
@@ -37,8 +37,8 @@ func (q *Query) Accounts(f form.AccountSearch) (result []entity.Account, err err
}
// AccountByID finds an account by primary key.
func (q *Query) AccountByID(id uint) (result entity.Account, err error) {
if err := q.db.Where("id = ?", id).First(&result).Error; err != nil {
func AccountByID(id uint) (result entity.Account, err error) {
if err := Db().Where("id = ?", id).First(&result).Error; err != nil {
return result, err
}

View File

@@ -29,8 +29,8 @@ type AlbumResult struct {
}
// AlbumByUUID returns a Album based on the UUID.
func (q *Query) AlbumByUUID(albumUUID string) (album entity.Album, err error) {
if err := q.db.Where("album_uuid = ?", albumUUID).Preload("Links").First(&album).Error; err != nil {
func AlbumByUUID(albumUUID string) (album entity.Album, err error) {
if err := Db().Where("album_uuid = ?", albumUUID).Preload("Links").First(&album).Error; err != nil {
return album, err
}
@@ -38,8 +38,8 @@ func (q *Query) AlbumByUUID(albumUUID string) (album entity.Album, err error) {
}
// AlbumThumbByUUID returns a album preview file based on the uuid.
func (q *Query) AlbumThumbByUUID(albumUUID string) (file entity.File, err error) {
if err := q.db.Where("files.file_primary = 1 AND files.deleted_at IS NULL").
func AlbumThumbByUUID(albumUUID string) (file entity.File, err error) {
if err := Db().Where("files.file_primary = 1 AND files.deleted_at IS NULL").
Joins("JOIN albums ON albums.album_uuid = ?", albumUUID).
Joins("JOIN photos_albums pa ON pa.album_uuid = albums.album_uuid AND pa.photo_uuid = files.photo_uuid").
Joins("JOIN photos ON photos.id = files.photo_id AND photos.photo_private = 0 AND photos.deleted_at IS NULL").
@@ -52,14 +52,14 @@ func (q *Query) AlbumThumbByUUID(albumUUID string) (file entity.File, err error)
}
// Albums searches albums based on their name.
func (q *Query) Albums(f form.AlbumSearch) (results []AlbumResult, err error) {
func Albums(f form.AlbumSearch) (results []AlbumResult, err error) {
if err := f.ParseQueryString(); err != nil {
return results, err
}
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("albums: %+v", f)))
s := q.db.NewScope(nil).DB()
s := Db().NewScope(nil).DB()
s = s.Table("albums").
Select(`albums.*,

View File

@@ -1,20 +1,15 @@
package query
import (
form "github.com/photoprism/photoprism/internal/form"
"github.com/stretchr/testify/assert"
"testing"
"github.com/photoprism/photoprism/internal/config"
form "github.com/photoprism/photoprism/internal/form"
"github.com/stretchr/testify/assert"
)
func TestQuery_AlbumByUUID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
func TestAlbumByUUID(t *testing.T) {
t.Run("existing uuid", func(t *testing.T) {
album, err := search.AlbumByUUID("at9lxuqxpogaaba7")
album, err := AlbumByUUID("at9lxuqxpogaaba7")
if err != nil {
t.Fatal(err)
@@ -24,19 +19,15 @@ func TestQuery_AlbumByUUID(t *testing.T) {
})
t.Run("not existing uuid", func(t *testing.T) {
album, err := search.AlbumByUUID("3765")
album, err := AlbumByUUID("3765")
assert.Error(t, err, "record not found")
t.Log(album)
})
}
func TestQuery_AlbumThumbByUUID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
func TestAlbumThumbByUUID(t *testing.T) {
t.Run("existing uuid", func(t *testing.T) {
file, err := search.AlbumThumbByUUID("at9lxuqxpogaaba8")
file, err := AlbumThumbByUUID("at9lxuqxpogaaba8")
if err != nil {
t.Fatal(err)
@@ -46,20 +37,16 @@ func TestQuery_AlbumThumbByUUID(t *testing.T) {
})
t.Run("not existing uuid", func(t *testing.T) {
file, err := search.AlbumThumbByUUID("3765")
file, err := AlbumThumbByUUID("3765")
assert.Error(t, err, "record not found")
t.Log(file)
})
}
func TestQuery_Albums(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
func TestAlbums(t *testing.T) {
t.Run("search with string", func(t *testing.T) {
query := form.NewAlbumSearch("chr")
result, err := search.Albums(query)
result, err := Albums(query)
if err != nil {
t.Fatal(err)
@@ -70,7 +57,7 @@ func TestQuery_Albums(t *testing.T) {
t.Run("search with slug", func(t *testing.T) {
query := form.NewAlbumSearch("slug:holiday count:10")
result, err := search.Albums(query)
result, err := Albums(query)
if err != nil {
t.Fatal(err)
@@ -82,7 +69,7 @@ func TestQuery_Albums(t *testing.T) {
t.Run("favorites true", func(t *testing.T) {
query := form.NewAlbumSearch("favorites:true count:10000")
result, err := search.Albums(query)
result, err := Albums(query)
if err != nil {
t.Fatal(err)
@@ -93,7 +80,7 @@ func TestQuery_Albums(t *testing.T) {
t.Run("empty query", func(t *testing.T) {
query := form.NewAlbumSearch("order:slug")
result, err := search.Albums(query)
result, err := Albums(query)
if err != nil {
t.Fatal(err)
@@ -103,7 +90,7 @@ func TestQuery_Albums(t *testing.T) {
})
t.Run("search with invalid query string", func(t *testing.T) {
query := form.NewAlbumSearch("xxx:bla")
result, err := search.Albums(query)
result, err := Albums(query)
assert.Error(t, err, "unknown filter")
t.Log(result)
})

View File

@@ -9,8 +9,8 @@ type CategoryLabel struct {
Title string
}
func (q *Query) CategoryLabels(limit, offset int) (results []CategoryLabel) {
s := q.db.NewScope(nil).DB()
func CategoryLabels(limit, offset int) (results []CategoryLabel) {
s := Db().NewScope(nil).DB()
s = s.Table("categories").
Select("label_name AS name").

View File

@@ -0,0 +1,13 @@
package query
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCategoryLabels(t *testing.T) {
categories := CategoryLabels(1000, 0)
assert.GreaterOrEqual(t, 1, len(categories))
}

View File

@@ -1,18 +0,0 @@
package query
import (
"github.com/stretchr/testify/assert"
"testing"
"github.com/photoprism/photoprism/internal/config"
)
func TestQuery_CategoryLabels(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
categories := search.CategoryLabels(1000, 0)
assert.GreaterOrEqual(t, 1, len(categories))
}

View File

@@ -1,41 +0,0 @@
package query
import (
"testing"
"github.com/photoprism/photoprism/internal/entity"
"github.com/stretchr/testify/assert"
)
func TestNewCountry(t *testing.T) {
t.Run("name Fantasy code fy", func(t *testing.T) {
country := entity.NewCountry("fy", "Fantasy")
assert.Equal(t, "fy", country.ID)
assert.Equal(t, "Fantasy", country.CountryName)
assert.Equal(t, "fantasy", country.CountrySlug)
})
t.Run("name Unknown code Unknown", func(t *testing.T) {
country := entity.NewCountry("", "")
assert.Equal(t, "zz", country.ID)
assert.Equal(t, "Unknown", country.CountryName)
assert.Equal(t, "zz", country.CountrySlug)
})
}
func TestCountry_FirstOrCreate(t *testing.T) {
t.Run("country already existing", func(t *testing.T) {
country := entity.NewCountry("de", "Germany")
country.FirstOrCreate()
assert.Equal(t, "de", country.Code())
assert.Equal(t, "Germany", country.Name())
assert.Equal(t, "Country description", country.CountryDescription)
assert.Equal(t, "Country Notes", country.CountryNotes)
assert.Equal(t, uint(0), country.CountryPhotoID)
})
t.Run("country not yet existing", func(t *testing.T) {
country := entity.NewCountry("wl", "Wonder Land")
country.FirstOrCreate()
assert.Equal(t, "wl", country.Code())
assert.Equal(t, "Wonder Land", country.Name())
})
}

18
internal/query/counts.go Normal file
View File

@@ -0,0 +1,18 @@
package query
import "github.com/jinzhu/gorm"
// UpdatePhotoCounts updates photos count in related tables as needed.
func UpdatePhotoCounts() error {
/*
UPDATE places
SET
photo_count = (SELECT
COUNT(*) FROM
photos ph
WHERE places.id = ph.place_id AND ph.photo_quality >= 0 AND ph.deleted_at IS NULL)
*/
return Db().Table("places").
UpdateColumn("photo_count", gorm.Expr("(SELECT COUNT(*) FROM photos ph WHERE places.id = ph.place_id AND ph.photo_quality >= 0 AND ph.deleted_at IS NULL)")).Error
}

View File

@@ -0,0 +1,13 @@
package query
import (
"testing"
)
func TestUpdatePhotoCounts(t *testing.T) {
err := UpdatePhotoCounts()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -7,8 +7,8 @@ import (
)
// FileShares returns up to 100 file shares for a given account id and status.
func (q *Query) FileShares(accountId uint, status string) (result []entity.FileShare, err error) {
s := q.db.Where(&entity.FileShare{})
func FileShares(accountId uint, status string) (result []entity.FileShare, err error) {
s := Db().Where(&entity.FileShare{})
if accountId > 0 {
s = s.Where("account_id = ?", accountId)
@@ -31,12 +31,12 @@ func (q *Query) FileShares(accountId uint, status string) (result []entity.FileS
}
// ExpiredFileShares returns up to 100 expired file shares for a given account.
func (q *Query) ExpiredFileShares(account entity.Account) (result []entity.FileShare, err error) {
func ExpiredFileShares(account entity.Account) (result []entity.FileShare, err error) {
if account.ShareExpires <= 0 {
return result, nil
}
s := q.db.Where(&entity.FileShare{})
s := Db().Where(&entity.FileShare{})
exp := time.Now().Add(time.Duration(-1*account.ShareExpires) * time.Second)

View File

@@ -8,7 +8,7 @@ import (
)
// SetDownloadFileID updates the local file id for remote downloads.
func (q *Query) SetDownloadFileID(filename string, fileId uint) error {
func SetDownloadFileID(filename string, fileId uint) error {
if len(filename) == 0 {
return errors.New("sync: can't update, filename empty")
}
@@ -18,7 +18,7 @@ func (q *Query) SetDownloadFileID(filename string, fileId uint) error {
filename = string(os.PathSeparator) + filename
}
result := q.db.Model(entity.FileSync{}).
result := Db().Model(entity.FileSync{}).
Where("remote_name = ? AND status = ? AND file_id = 0", filename, entity.FileSyncDownloaded).
Update("file_id", fileId)

View File

@@ -5,8 +5,8 @@ import (
)
// FileSyncs returns a list of FileSync entities for a given account and status.
func (q *Query) FileSyncs(accountId uint, status string, limit int) (result []entity.FileSync, err error) {
s := q.db.Where(&entity.FileSync{})
func FileSyncs(accountId uint, status string, limit int) (result []entity.FileSync, err error) {
s := Db().Where(&entity.FileSync{})
if accountId > 0 {
s = s.Where("account_id = ?", accountId)

View File

@@ -7,12 +7,12 @@ import (
)
// ExistingFiles returns not-missing and not-deleted file entities in the range of limit and offset sorted by id.
func (q *Query) ExistingFiles(limit int, offset int, filePath string) (files []entity.File, err error) {
func ExistingFiles(limit int, offset int, filePath string) (files []entity.File, err error) {
if strings.HasPrefix(filePath, "/") {
filePath = filePath[1:]
}
stmt := q.db.Unscoped().Where("file_missing = 0 AND deleted_at IS NULL")
stmt := Db().Unscoped().Where("file_missing = 0 AND deleted_at IS NULL")
if filePath != "" {
stmt = stmt.Where("file_name LIKE ?", filePath+"/%")
@@ -24,8 +24,8 @@ func (q *Query) ExistingFiles(limit int, offset int, filePath string) (files []e
}
// FilesByUUID
func (q *Query) FilesByUUID(u []string, limit int, offset int) (files []entity.File, err error) {
if err := q.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 {
func FilesByUUID(u []string, limit int, offset int) (files []entity.File, err error) {
if err := 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
}
@@ -33,8 +33,8 @@ func (q *Query) FilesByUUID(u []string, limit int, offset int) (files []entity.F
}
// FileByPhotoUUID
func (q *Query) FileByPhotoUUID(u string) (file entity.File, err error) {
if err := q.db.Where("photo_uuid = ? AND file_primary = 1", u).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
func FileByPhotoUUID(u string) (file entity.File, err error) {
if err := Db().Where("photo_uuid = ? AND file_primary = 1", u).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
return file, err
}
@@ -42,8 +42,8 @@ func (q *Query) FileByPhotoUUID(u string) (file entity.File, err error) {
}
// FileByUUID returns the file entity for a given UUID.
func (q *Query) FileByUUID(uuid string) (file entity.File, err error) {
if err := q.db.Where("file_uuid = ?", uuid).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
func FileByUUID(uuid string) (file entity.File, err error) {
if err := Db().Where("file_uuid = ?", uuid).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
return file, err
}
@@ -51,8 +51,8 @@ func (q *Query) FileByUUID(uuid string) (file entity.File, err error) {
}
// FirstFileByHash finds a file with a given hash string.
func (q *Query) FileByHash(fileHash string) (file entity.File, err error) {
if err := q.db.Where("file_hash = ?", fileHash).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
func FileByHash(fileHash string) (file entity.File, err error) {
if err := Db().Where("file_hash = ?", fileHash).Preload("Links").Preload("Photo").First(&file).Error; err != nil {
return file, err
}
@@ -60,7 +60,7 @@ func (q *Query) FileByHash(fileHash string) (file entity.File, err error) {
}
// SetPhotoPrimary sets a new primary image file for a photo.
func (q *Query) SetPhotoPrimary(photoUUID, fileUUID string) error {
q.db.Model(entity.File{}).Where("photo_uuid = ? AND file_uuid <> ?", photoUUID, fileUUID).UpdateColumn("file_primary", false)
return q.db.Model(entity.File{}).Where("photo_uuid = ? AND file_uuid = ?", photoUUID, fileUUID).UpdateColumn("file_primary", true).Error
func SetPhotoPrimary(photoUUID, fileUUID string) error {
Db().Model(entity.File{}).Where("photo_uuid = ? AND file_uuid <> ?", photoUUID, fileUUID).UpdateColumn("file_primary", false)
return Db().Model(entity.File{}).Where("photo_uuid = ? AND file_uuid = ?", photoUUID, fileUUID).UpdateColumn("file_primary", true).Error
}

View File

@@ -1,32 +1,25 @@
package query
import (
"github.com/stretchr/testify/assert"
"testing"
"github.com/photoprism/photoprism/internal/config"
"github.com/stretchr/testify/assert"
)
func TestQuery_ExistingFiles(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
func TestExistingFiles(t *testing.T) {
t.Run("files found", func(t *testing.T) {
files, err := search.ExistingFiles(1000, 0, "/")
files, err := ExistingFiles(1000, 0, "/")
t.Logf("files: %+v", files)
assert.Nil(t, err)
assert.LessOrEqual(t, 5, len(files))
})
}
func TestQuery_FilesByUUID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
func TestFilesByUUID(t *testing.T) {
t.Run("files found", func(t *testing.T) {
files, err := search.FilesByUUID([]string{"ft8es39w45bnlqdw"}, 100, 0)
files, err := FilesByUUID([]string{"ft8es39w45bnlqdw"}, 100, 0)
assert.Nil(t, err)
assert.Equal(t, 1, len(files))
@@ -34,33 +27,25 @@ func TestQuery_FilesByUUID(t *testing.T) {
})
}
func TestQuery_FileByPhotoUUID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
func TestFileByPhotoUUID(t *testing.T) {
t.Run("files found", func(t *testing.T) {
file, err := search.FileByPhotoUUID("pt9jtdre2lvl0yh8")
file, err := FileByPhotoUUID("pt9jtdre2lvl0yh8")
assert.Nil(t, err)
assert.Equal(t, "exampleDNGFile.dng", file.FileName)
})
t.Run("no files found", func(t *testing.T) {
file, err := search.FileByPhotoUUID("111")
file, err := FileByPhotoUUID("111")
assert.Error(t, err, "record not found")
t.Log(file)
})
}
func TestQuery_FileByUUID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
func TestFileByUUID(t *testing.T) {
t.Run("files found", func(t *testing.T) {
file, err := search.FileByUUID("ft8es39w45bnlqdw")
file, err := FileByUUID("ft8es39w45bnlqdw")
if err != nil {
t.Fatal(err)
@@ -70,7 +55,7 @@ func TestQuery_FileByUUID(t *testing.T) {
})
t.Run("no files found", func(t *testing.T) {
file, err := search.FileByUUID("111")
file, err := FileByUUID("111")
if err == nil {
t.Fatal("error expected")
@@ -81,20 +66,16 @@ func TestQuery_FileByUUID(t *testing.T) {
})
}
func TestQuery_FileByHash(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
func TestFileByHash(t *testing.T) {
t.Run("files found", func(t *testing.T) {
file, err := search.FileByHash("2cad9168fa6acc5c5c2965ddf6ec465ca42fd818")
file, err := FileByHash("2cad9168fa6acc5c5c2965ddf6ec465ca42fd818")
assert.Nil(t, err)
assert.Equal(t, "exampleFileName.jpg", file.FileName)
})
t.Run("no files found", func(t *testing.T) {
file, err := search.FileByHash("111")
file, err := FileByHash("111")
assert.Error(t, err, "record not found")
t.Log(file)

View File

@@ -12,37 +12,15 @@ import (
"github.com/photoprism/photoprism/pkg/txt"
)
// GeoResult represents a photo for displaying it on a map.
type GeoResult struct {
ID string `json:"ID"`
PhotoLat float32 `json:"Lat"`
PhotoLng float32 `json:"Lng"`
PhotoUUID string `json:"PhotoUUID"`
PhotoTitle string `json:"PhotoTitle"`
PhotoFavorite bool `json:"PhotoFavorite"`
FileHash string `json:"FileHash"`
FileWidth int `json:"FileWidth"`
FileHeight int `json:"FileHeight"`
TakenAt time.Time `json:"TakenAt"`
}
func (g GeoResult) Lat() float64 {
return float64(g.PhotoLat)
}
func (g GeoResult) Lng() float64 {
return float64(g.PhotoLng)
}
// Geo searches for photos based on a Form and returns a PhotoResult slice.
func (q *Query) Geo(f form.GeoSearch) (results []GeoResult, err error) {
func Geo(f form.GeoSearch) (results []GeoResult, err error) {
if err := f.ParseQueryString(); err != nil {
return results, err
}
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("search: %+v", f)))
s := q.db.NewScope(nil).DB()
s := UnscopedDb()
s = s.Table("photos").
Select(`photos.id, photos.photo_uuid, photos.photo_lat, photos.photo_lng, photos.photo_title,

View File

@@ -0,0 +1,27 @@
package query
import (
"time"
)
// GeoResult represents a photo for displaying it on a map.
type GeoResult struct {
ID string `json:"ID"`
PhotoLat float32 `json:"Lat"`
PhotoLng float32 `json:"Lng"`
PhotoUUID string `json:"PhotoUUID"`
PhotoTitle string `json:"PhotoTitle"`
PhotoFavorite bool `json:"PhotoFavorite"`
FileHash string `json:"FileHash"`
FileWidth int `json:"FileWidth"`
FileHeight int `json:"FileHeight"`
TakenAt time.Time `json:"TakenAt"`
}
func (g GeoResult) Lat() float64 {
return float64(g.PhotoLat)
}
func (g GeoResult) Lng() float64 {
return float64(g.PhotoLng)
}

View File

@@ -1,21 +1,16 @@
package query
import (
"github.com/photoprism/photoprism/internal/form"
"github.com/stretchr/testify/assert"
"testing"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/form"
"github.com/stretchr/testify/assert"
)
func TestQuery_Geo(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
func TestGeo(t *testing.T) {
t.Run("search all photos", func(t *testing.T) {
query := form.NewGeoSearch("")
result, err := search.Geo(query)
result, err := Geo(query)
assert.Nil(t, err)
assert.Equal(t, 4, len(result))
@@ -24,7 +19,7 @@ func TestQuery_Geo(t *testing.T) {
t.Run("search for bridge", func(t *testing.T) {
query := form.NewGeoSearch("Query:bridge Before:3006-01-02")
result, err := search.Geo(query)
result, err := Geo(query)
assert.Nil(t, err)
assert.Equal(t, "Neckarbrücke", result[0].PhotoTitle)
@@ -33,7 +28,7 @@ func TestQuery_Geo(t *testing.T) {
t.Run("search for timeframe", func(t *testing.T) {
query := form.NewGeoSearch("After:2014-12-02 Before:3006-01-02")
result, err := search.Geo(query)
result, err := Geo(query)
assert.Nil(t, err)
t.Log(result)

View File

@@ -0,0 +1,23 @@
package query
import (
"time"
)
// LabelResult contains found labels
type LabelResult struct {
// Label
ID uint
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
LabelUUID string
LabelSlug string
CustomSlug string
LabelName string
LabelPriority int
LabelCount int
LabelFavorite bool
LabelDescription string
LabelNotes string
}

View File

@@ -12,27 +12,9 @@ import (
"github.com/photoprism/photoprism/pkg/txt"
)
// LabelResult contains found labels
type LabelResult struct {
// Label
ID uint
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
LabelUUID string
LabelSlug string
CustomSlug string
LabelName string
LabelPriority int
LabelCount int
LabelFavorite bool
LabelDescription string
LabelNotes string
}
// PhotoLabel returns a photo label entity if exists.
func (q *Query) PhotoLabel(photoID, labelID uint) (label entity.PhotoLabel, err error) {
if err := q.db.Where("photo_id = ? AND label_id = ?", photoID, labelID).Preload("Photo").Preload("Label").First(&label).Error; err != nil {
func PhotoLabel(photoID, labelID uint) (label entity.PhotoLabel, err error) {
if err := Db().Where("photo_id = ? AND label_id = ?", photoID, labelID).Preload("Photo").Preload("Label").First(&label).Error; err != nil {
return label, err
}
@@ -40,8 +22,8 @@ func (q *Query) PhotoLabel(photoID, labelID uint) (label entity.PhotoLabel, err
}
// LabelBySlug returns a Label based on the slug name.
func (q *Query) LabelBySlug(labelSlug string) (label entity.Label, err error) {
if err := q.db.Where("label_slug = ? OR custom_slug = ?", labelSlug, labelSlug).Preload("Links").First(&label).Error; err != nil {
func LabelBySlug(labelSlug string) (label entity.Label, err error) {
if err := Db().Where("label_slug = ? OR custom_slug = ?", labelSlug, labelSlug).Preload("Links").First(&label).Error; err != nil {
return label, err
}
@@ -49,8 +31,8 @@ func (q *Query) LabelBySlug(labelSlug string) (label entity.Label, err error) {
}
// LabelByUUID returns a Label based on the label UUID.
func (q *Query) LabelByUUID(labelUUID string) (label entity.Label, err error) {
if err := q.db.Where("label_uuid = ?", labelUUID).Preload("Links").First(&label).Error; err != nil {
func LabelByUUID(labelUUID string) (label entity.Label, err error) {
if err := Db().Where("label_uuid = ?", labelUUID).Preload("Links").First(&label).Error; err != nil {
return label, err
}
@@ -58,8 +40,8 @@ func (q *Query) LabelByUUID(labelUUID string) (label entity.Label, err error) {
}
// LabelThumbBySlug returns a label preview file based on the slug name.
func (q *Query) LabelThumbBySlug(labelSlug string) (file entity.File, err error) {
if err := q.db.Where("files.file_primary AND files.deleted_at IS NULL").
func LabelThumbBySlug(labelSlug string) (file entity.File, err error) {
if err := Db().Where("files.file_primary AND files.deleted_at IS NULL").
Joins("JOIN labels ON labels.label_slug = ?", labelSlug).
Joins("JOIN photos_labels ON photos_labels.label_id = labels.id AND photos_labels.photo_id = files.photo_id").
Joins("JOIN photos ON photos.id = files.photo_id AND photos.photo_private = 0 AND photos.deleted_at IS NULL").
@@ -72,9 +54,9 @@ func (q *Query) LabelThumbBySlug(labelSlug string) (file entity.File, err error)
}
// LabelThumbByUUID returns a label preview file based on the label UUID.
func (q *Query) LabelThumbByUUID(labelUUID string) (file entity.File, err error) {
func LabelThumbByUUID(labelUUID string) (file entity.File, err error) {
// Search matching label
err = q.db.Where("files.file_primary AND files.deleted_at IS NULL").
err = Db().Where("files.file_primary AND files.deleted_at IS NULL").
Joins("JOIN labels ON labels.label_uuid = ?", labelUUID).
Joins("JOIN photos_labels ON photos_labels.label_id = labels.id AND photos_labels.photo_id = files.photo_id").
Joins("JOIN photos ON photos.id = files.photo_id AND photos.photo_private = 0 AND photos.deleted_at IS NULL").
@@ -86,7 +68,7 @@ func (q *Query) LabelThumbByUUID(labelUUID string) (file entity.File, err error)
}
// If failed, search for category instead
err = q.db.Where("files.file_primary AND files.deleted_at IS NULL").
err = Db().Where("files.file_primary AND files.deleted_at IS NULL").
Joins("JOIN photos_labels ON photos_labels.photo_id = files.photo_id").
Joins("JOIN categories c ON photos_labels.label_id = c.label_id").
Joins("JOIN labels ON c.category_id = labels.id AND labels.label_uuid= ?", labelUUID).
@@ -98,14 +80,14 @@ func (q *Query) LabelThumbByUUID(labelUUID string) (file entity.File, err error)
}
// Labels searches labels based on their name.
func (q *Query) Labels(f form.LabelSearch) (results []LabelResult, err error) {
func Labels(f form.LabelSearch) (results []LabelResult, err error) {
if err := f.ParseQueryString(); err != nil {
return results, err
}
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("labels: %+v", f)))
s := q.db.NewScope(nil).DB()
s := UnscopedDb()
// s.LogMode(true)
@@ -132,14 +114,14 @@ func (q *Query) Labels(f form.LabelSearch) (results []LabelResult, err error) {
slugString := slug.Make(f.Query)
likeString := "%" + strings.ToLower(f.Query) + "%"
if result := q.db.First(&label, "label_slug = ? OR custom_slug = ?", slugString, slugString); result.Error != nil {
if result := Db().First(&label, "label_slug = ? OR custom_slug = ?", slugString, slugString); result.Error != nil {
log.Infof("search: label %s not found", txt.Quote(f.Query))
s = s.Where("LOWER(labels.label_name) LIKE ?", likeString)
} else {
labelIds = append(labelIds, label.ID)
q.db.Where("category_id = ?", label.ID).Find(&categories)
Db().Where("category_id = ?", label.ID).Find(&categories)
for _, category := range categories {
labelIds = append(labelIds, category.LabelID)

View File

@@ -1,21 +1,16 @@
package query
import (
"testing"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/stretchr/testify/assert"
"testing"
"github.com/photoprism/photoprism/internal/config"
)
func TestQuery_LabelBySlug(t *testing.T) {
conf := config.TestConfig()
q := New(conf.Db())
func TestLabelBySlug(t *testing.T) {
t.Run("files found", func(t *testing.T) {
label, err := q.LabelBySlug("flower")
label, err := LabelBySlug("flower")
if err != nil {
t.Fatal(err)
@@ -25,20 +20,16 @@ func TestQuery_LabelBySlug(t *testing.T) {
})
t.Run("no files found", func(t *testing.T) {
label, err := q.LabelBySlug("111")
label, err := LabelBySlug("111")
assert.Error(t, err, "record not found")
assert.Empty(t, label.ID)
})
}
func TestQuery_LabelByUUID(t *testing.T) {
conf := config.TestConfig()
q := New(conf.Db())
func TestLabelByUUID(t *testing.T) {
t.Run("files found", func(t *testing.T) {
label, err := q.LabelByUUID("lt9k3pw1wowuy3c5")
label, err := LabelByUUID("lt9k3pw1wowuy3c5")
if err != nil {
t.Fatal(err)
@@ -48,20 +39,16 @@ func TestQuery_LabelByUUID(t *testing.T) {
})
t.Run("no files found", func(t *testing.T) {
label, err := q.LabelByUUID("111")
label, err := LabelByUUID("111")
assert.Error(t, err, "record not found")
assert.Empty(t, label.ID)
})
}
func TestQuery_LabelThumbBySlug(t *testing.T) {
conf := config.TestConfig()
q := New(conf.Db())
func TestLabelThumbBySlug(t *testing.T) {
t.Run("files found", func(t *testing.T) {
file, err := q.LabelThumbBySlug("flower")
file, err := LabelThumbBySlug("flower")
if err != nil {
t.Fatal(err)
@@ -71,20 +58,16 @@ func TestQuery_LabelThumbBySlug(t *testing.T) {
})
t.Run("no files found", func(t *testing.T) {
file, err := q.LabelThumbBySlug("cow")
file, err := LabelThumbBySlug("cow")
assert.Error(t, err, "record not found")
t.Log(file)
})
}
func TestQuery_LabelThumbByUUID(t *testing.T) {
conf := config.TestConfig()
q := New(conf.Db())
func TestLabelThumbByUUID(t *testing.T) {
t.Run("files found", func(t *testing.T) {
file, err := q.LabelThumbByUUID("lt9k3pw1wowuy3c4")
file, err := LabelThumbByUUID("lt9k3pw1wowuy3c4")
if err != nil {
t.Fatal(err)
@@ -94,20 +77,17 @@ func TestQuery_LabelThumbByUUID(t *testing.T) {
})
t.Run("no files found", func(t *testing.T) {
file, err := q.LabelThumbByUUID("14")
file, err := LabelThumbByUUID("14")
assert.Error(t, err, "record not found")
t.Log(file)
})
}
func TestQuery_Labels(t *testing.T) {
conf := config.TestConfig()
q := New(conf.Db())
func TestLabels(t *testing.T) {
t.Run("search with query", func(t *testing.T) {
query := form.NewLabelSearch("Query:C Count:1005 Order:slug")
result, err := q.Labels(query)
result, err := Labels(query)
if err != nil {
t.Fatal(err)
@@ -134,7 +114,7 @@ func TestQuery_Labels(t *testing.T) {
t.Run("search for favorites", func(t *testing.T) {
query := form.NewLabelSearch("Favorites:true")
result, err := q.Labels(query)
result, err := Labels(query)
if err != nil {
t.Fatal(err)
@@ -160,7 +140,7 @@ func TestQuery_Labels(t *testing.T) {
t.Run("search with empty query", func(t *testing.T) {
query := form.NewLabelSearch("")
result, err := q.Labels(query)
result, err := Labels(query)
if err != nil {
t.Fatal(err)
@@ -171,7 +151,7 @@ func TestQuery_Labels(t *testing.T) {
t.Run("search with invalid query string", func(t *testing.T) {
query := form.NewLabelSearch("xxx:bla")
result, err := q.Labels(query)
result, err := Labels(query)
assert.Error(t, err, "unknown filter")
assert.Empty(t, result)

View File

@@ -8,8 +8,8 @@ type MomentsTimeResult struct {
}
// GetMomentsTime counts photos per month and year
func (q *Query) GetMomentsTime() (results []MomentsTimeResult, err error) {
s := q.db.NewScope(nil).DB()
func GetMomentsTime() (results []MomentsTimeResult, err error) {
s := UnscopedDb()
s = s.Table("photos").
Where("deleted_at IS NULL").

View File

@@ -1,19 +1,14 @@
package query
import (
"github.com/stretchr/testify/assert"
"testing"
"github.com/photoprism/photoprism/internal/config"
"github.com/stretchr/testify/assert"
)
func TestQuery_GetMomentsTime(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
func TestGetMomentsTime(t *testing.T) {
t.Run("result found", func(t *testing.T) {
result, err := search.GetMomentsTime()
result, err := GetMomentsTime()
assert.Nil(t, err)
assert.Equal(t, 2790, result[0].PhotoYear)

View File

@@ -1,407 +1,13 @@
package query
import (
"fmt"
"strings"
"time"
"github.com/gosimple/slug"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/capture"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/ulule/deepcopier"
)
// PhotoResult contains found photos and their main file plus other meta data.
type PhotoResult struct {
// Photo
ID uint
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
TakenAt time.Time
TakenAtLocal time.Time
TakenSrc string
TimeZone string
PhotoUUID string
PhotoPath string
PhotoName string
PhotoTitle string
PhotoYear int
PhotoMonth int
PhotoCountry string
PhotoFavorite bool
PhotoPrivate bool
PhotoLat float32
PhotoLng float32
PhotoAltitude int
PhotoIso int
PhotoFocalLength int
PhotoFNumber float32
PhotoExposure string
PhotoQuality int
PhotoResolution int
Merged bool
// Camera
CameraID uint
CameraModel string
CameraMake string
// Lens
LensID uint
LensModel string
LensMake string
// Location
LocationID string
PlaceID string
LocLabel string
LocCity string
LocState string
LocCountry string
// File
FileID uint
FileUUID string
FilePrimary bool
FileMissing bool
FileName string
FileHash string
FileType string
FileMime string
FileWidth int
FileHeight int
FileOrientation int
FileAspectRatio float32
FileColors string // todo: remove from result?
FileChroma uint8 // todo: remove from result?
FileLuminance string // todo: remove from result?
FileDiff uint32 // todo: remove from result?
Files []entity.File
}
type PhotoResults []PhotoResult
func (m PhotoResults) Merged() (PhotoResults, int, error) {
count := len(m)
merged := make([]PhotoResult, 0, count)
var lastId uint
var i int
for _, res := range m {
file := entity.File{}
if err := deepcopier.Copy(&file).From(res); err != nil {
return merged, count, err
}
file.ID = res.FileID
if lastId == res.ID && i > 0 {
merged[i-1].Files = append(merged[i-1].Files, file)
merged[i-1].Merged = true
continue
}
lastId = res.ID
res.Files = append(res.Files, file)
merged = append(merged, res)
i++
}
return merged, count, nil
}
func (m *PhotoResult) ShareFileName() string {
var name string
if m.PhotoTitle != "" {
name = strings.Title(slug.MakeLang(m.PhotoTitle, "en"))
} else {
name = m.PhotoUUID
}
taken := m.TakenAtLocal.Format("20060102-150405")
token := rnd.Token(3)
result := fmt.Sprintf("%s-%s-%s.%s", taken, name, token, m.FileType)
return result
}
// Photos searches for photos based on a Form and returns a PhotoResult slice.
func (q *Query) Photos(f form.PhotoSearch) (results PhotoResults, count int, err error) {
if err := f.ParseQueryString(); err != nil {
return results, 0, err
}
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("photos: %+v", f)))
s := q.db.NewScope(nil).DB()
// s.LogMode(true)
s = s.Table("photos").
Select(`photos.*,
files.id AS file_id, files.file_uuid, files.file_primary, files.file_missing, files.file_name, files.file_hash,
files.file_type, files.file_mime, files.file_width, files.file_height, files.file_aspect_ratio,
files.file_orientation, files.file_main_color, files.file_colors, files.file_luminance, files.file_chroma,
files.file_diff,
cameras.camera_make, cameras.camera_model,
lenses.lens_make, lenses.lens_model,
places.loc_label, places.loc_city, places.loc_state, places.loc_country
`).
Joins("JOIN files ON files.photo_id = photos.id AND files.file_type = 'jpg' AND files.file_missing = 0 AND files.deleted_at IS NULL").
Joins("JOIN cameras ON cameras.id = photos.camera_id").
Joins("JOIN lenses ON lenses.id = photos.lens_id").
Joins("JOIN places ON photos.place_id = places.id").
Joins("LEFT JOIN photos_labels ON photos_labels.photo_id = photos.id AND photos_labels.uncertainty < 100").
Group("photos.id, files.id")
if f.ID != "" {
s = s.Where("photos.photo_uuid = ?", f.ID)
s = s.Order("files.file_primary DESC")
if result := s.Scan(&results); result.Error != nil {
return results, 0, result.Error
}
if f.Merged {
return results.Merged()
}
return results, len(results), nil
}
var categories []entity.Category
var label entity.Label
var labelIds []uint
if f.Label != "" {
slugString := strings.ToLower(f.Label)
if result := q.db.First(&label, "label_slug =? OR custom_slug = ?", slugString, slugString); result.Error != nil {
log.Errorf("search: label %s not found", txt.Quote(f.Label))
return results, 0, fmt.Errorf("label %s not found", txt.Quote(f.Label))
} else {
labelIds = append(labelIds, label.ID)
q.db.Where("category_id = ?", label.ID).Find(&categories)
for _, category := range categories {
labelIds = append(labelIds, category.LabelID)
}
s = s.Where("photos_labels.label_id IN (?)", labelIds)
}
}
if f.Location == true {
s = s.Where("location_id > 0")
if f.Query != "" {
s = s.Joins("LEFT JOIN photos_keywords ON photos_keywords.photo_id = photos.id").
Joins("LEFT JOIN keywords ON photos_keywords.keyword_id = keywords.id").
Where("keywords.keyword LIKE ?", strings.ToLower(txt.Clip(f.Query, txt.ClipKeyword))+"%")
}
} else if f.Query != "" {
if len(f.Query) < 2 {
return results, 0, fmt.Errorf("query too short")
}
slugString := slug.Make(f.Query)
lowerString := strings.ToLower(f.Query)
likeString := txt.Clip(lowerString, txt.ClipKeyword) + "%"
s = s.Joins("LEFT JOIN photos_keywords ON photos_keywords.photo_id = photos.id").
Joins("LEFT JOIN keywords ON photos_keywords.keyword_id = keywords.id")
if result := q.db.First(&label, "label_slug = ? OR custom_slug = ?", slugString, slugString); result.Error != nil {
log.Infof("search: label %s not found, using fuzzy search", txt.Quote(f.Query))
s = s.Where("keywords.keyword LIKE ?", likeString)
} else {
labelIds = append(labelIds, label.ID)
q.db.Where("category_id = ?", label.ID).Find(&categories)
for _, category := range categories {
labelIds = append(labelIds, category.LabelID)
}
log.Infof("search: label %s includes %d categories", txt.Quote(label.LabelName), len(labelIds))
s = s.Where("photos_labels.label_id IN (?) OR keywords.keyword LIKE ?", labelIds, likeString)
}
}
if f.Archived {
s = s.Where("photos.deleted_at IS NOT NULL")
} else {
s = s.Where("photos.deleted_at IS NULL")
if f.Private {
s = s.Where("photos.photo_private = 1")
} else if f.Public {
s = s.Where("photos.photo_private = 0")
}
if f.Review {
s = s.Where("photos.photo_quality < 3")
} else if f.Quality != 0 && f.Private == false {
s = s.Where("photos.photo_quality >= ?", f.Quality)
}
}
if f.Error {
s = s.Where("files.file_error <> ''")
}
if f.Album != "" {
s = s.Joins("JOIN photos_albums ON photos_albums.photo_uuid = photos.photo_uuid").Where("photos_albums.album_uuid = ?", f.Album)
}
if f.Camera > 0 {
s = s.Where("photos.camera_id = ?", f.Camera)
}
if f.Lens > 0 {
s = s.Where("photos.lens_id = ?", f.Lens)
}
if f.Year > 0 {
s = s.Where("photos.photo_year = ?", f.Year)
}
if f.Month > 0 {
s = s.Where("photos.photo_month = ?", f.Month)
}
if f.Color != "" {
s = s.Where("files.file_main_color = ?", strings.ToLower(f.Color))
}
if f.Favorites {
s = s.Where("photos.photo_favorite = 1")
}
if f.Story {
s = s.Where("photos.photo_story = 1")
}
if f.Country != "" {
s = s.Where("photos.photo_country = ?", f.Country)
}
if f.Title != "" {
s = s.Where("LOWER(photos.photo_title) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Title)))
}
if f.Hash != "" {
s = s.Where("files.file_hash = ?", f.Hash)
}
if f.Duplicate {
s = s.Where("files.file_duplicate = 1")
}
if f.Portrait {
s = s.Where("files.file_portrait = 1")
}
if f.Mono {
s = s.Where("files.file_chroma = 0")
} else if f.Chroma > 9 {
s = s.Where("files.file_chroma > ?", f.Chroma)
} else if f.Chroma > 0 {
s = s.Where("files.file_chroma > 0 AND files.file_chroma <= ?", f.Chroma)
}
if f.Diff != 0 {
s = s.Where("files.file_diff = ?", f.Diff)
}
if f.Fmin > 0 {
s = s.Where("photos.photo_f_number >= ?", f.Fmin)
}
if f.Fmax > 0 {
s = s.Where("photos.photo_f_number <= ?", f.Fmax)
}
if f.Dist == 0 {
f.Dist = 20
} else if f.Dist > 5000 {
f.Dist = 5000
}
// Inaccurate distance search, but probably 'good enough' for now
if f.Lat > 0 {
latMin := f.Lat - SearchRadius*float32(f.Dist)
latMax := f.Lat + SearchRadius*float32(f.Dist)
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latMin, latMax)
}
if f.Lng > 0 {
lngMin := f.Lng - SearchRadius*float32(f.Dist)
lngMax := f.Lng + SearchRadius*float32(f.Dist)
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngMin, lngMax)
}
if !f.Before.IsZero() {
s = s.Where("photos.taken_at <= ?", f.Before.Format("2006-01-02"))
}
if !f.After.IsZero() {
s = s.Where("photos.taken_at >= ?", f.After.Format("2006-01-02"))
}
switch f.Order {
case entity.SortOrderRelevance:
if f.Label != "" {
s = s.Order("photo_quality DESC, photos_labels.uncertainty ASC, taken_at DESC, files.file_primary DESC")
} else {
s = s.Order("photo_quality DESC, taken_at DESC, files.file_primary DESC")
}
case entity.SortOrderNewest:
s = s.Order("taken_at DESC, photos.photo_uuid, files.file_primary DESC")
case entity.SortOrderOldest:
s = s.Order("taken_at, photos.photo_uuid, files.file_primary DESC")
case entity.SortOrderImported:
s = s.Order("photos.id DESC, files.file_primary DESC")
case entity.SortOrderSimilar:
s = s.Order("files.file_main_color, photos.location_id, files.file_diff, taken_at DESC, files.file_primary DESC")
default:
s = s.Order("taken_at DESC, photos.photo_uuid, files.file_primary DESC")
}
if f.Count > 0 && f.Count <= 1000 {
s = s.Limit(f.Count).Offset(f.Offset)
} else {
s = s.Limit(100).Offset(0)
}
if result := s.Scan(&results); result.Error != nil {
return results, 0, result.Error
}
if f.Merged {
return results.Merged()
}
return results, len(results), nil
}
// PhotoByID returns a Photo based on the ID.
func (q *Query) PhotoByID(photoID uint64) (photo entity.Photo, err error) {
if err := q.db.Unscoped().Where("id = ?", photoID).
func PhotoByID(photoID uint64) (photo entity.Photo, err error) {
if err := UnscopedDb().Where("id = ?", photoID).
Preload("Links").
Preload("Description").
Preload("Location").
@@ -418,8 +24,8 @@ func (q *Query) PhotoByID(photoID uint64) (photo entity.Photo, err error) {
}
// PhotoByUUID returns a Photo based on the UUID.
func (q *Query) PhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
if err := q.db.Unscoped().Where("photo_uuid = ?", photoUUID).
func PhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
if err := UnscopedDb().Where("photo_uuid = ?", photoUUID).
Preload("Links").
Preload("Description").
Preload("Location").
@@ -436,8 +42,8 @@ func (q *Query) PhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
}
// PreloadPhotoByUUID returns a Photo based on the UUID with all dependencies preloaded.
func (q *Query) PreloadPhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
if err := q.db.Unscoped().Where("photo_uuid = ?", photoUUID).
func PreloadPhotoByUUID(photoUUID string) (photo entity.Photo, err error) {
if err := UnscopedDb().Where("photo_uuid = ?", photoUUID).
Preload("Labels", func(db *gorm.DB) *gorm.DB {
return db.Order("photos_labels.uncertainty ASC, photos_labels.label_id DESC")
}).
@@ -458,8 +64,8 @@ func (q *Query) PreloadPhotoByUUID(photoUUID string) (photo entity.Photo, err er
}
// MissingPhotos returns photo entities without existing files.
func (q *Query) MissingPhotos(limit int, offset int) (entities []entity.Photo, err error) {
err = q.db.
func MissingPhotos(limit int, offset int) (entities []entity.Photo, err error) {
err = Db().
Select("photos.*").
Joins("JOIN files a ON photos.id = a.photo_id ").
Joins("LEFT JOIN files b ON a.photo_id = b.photo_id AND a.id != b.id AND b.file_missing = 0").
@@ -471,8 +77,8 @@ func (q *Query) MissingPhotos(limit int, offset int) (entities []entity.Photo, e
}
// ResetPhotosQuality resets the quality of photos without primary file to -1.
func (q *Query) ResetPhotosQuality() error {
return q.db.Table("photos").
func ResetPhotosQuality() error {
return Db().Table("photos").
Where("id IN (SELECT photos.id FROM photos LEFT JOIN files ON photos.id = files.photo_id AND files.file_primary = 1 WHERE files.id IS NULL GROUP BY photos.id)").
Update("photo_quality", -1).Error
}

View File

@@ -0,0 +1,134 @@
package query
import (
"fmt"
"strings"
"time"
"github.com/gosimple/slug"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/ulule/deepcopier"
)
// PhotoResult contains found photos and their main file plus other meta data.
type PhotoResult struct {
// Photo
ID uint
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
TakenAt time.Time
TakenAtLocal time.Time
TakenSrc string
TimeZone string
PhotoUUID string
PhotoPath string
PhotoName string
PhotoTitle string
PhotoYear int
PhotoMonth int
PhotoCountry string
PhotoFavorite bool
PhotoPrivate bool
PhotoLat float32
PhotoLng float32
PhotoAltitude int
PhotoIso int
PhotoFocalLength int
PhotoFNumber float32
PhotoExposure string
PhotoQuality int
PhotoResolution int
Merged bool
// Camera
CameraID uint
CameraModel string
CameraMake string
// Lens
LensID uint
LensModel string
LensMake string
// Location
LocationID string
PlaceID string
LocLabel string
LocCity string
LocState string
LocCountry string
// File
FileID uint
FileUUID string
FilePrimary bool
FileMissing bool
FileName string
FileHash string
FileType string
FileMime string
FileWidth int
FileHeight int
FileOrientation int
FileAspectRatio float32
FileColors string // todo: remove from result?
FileChroma uint8 // todo: remove from result?
FileLuminance string // todo: remove from result?
FileDiff uint32 // todo: remove from result?
Files []entity.File
}
type PhotoResults []PhotoResult
func (m PhotoResults) Merged() (PhotoResults, int, error) {
count := len(m)
merged := make([]PhotoResult, 0, count)
var lastId uint
var i int
for _, res := range m {
file := entity.File{}
if err := deepcopier.Copy(&file).From(res); err != nil {
return merged, count, err
}
file.ID = res.FileID
if lastId == res.ID && i > 0 {
merged[i-1].Files = append(merged[i-1].Files, file)
merged[i-1].Merged = true
continue
}
lastId = res.ID
res.Files = append(res.Files, file)
merged = append(merged, res)
i++
}
return merged, count, nil
}
func (m *PhotoResult) ShareFileName() string {
var name string
if m.PhotoTitle != "" {
name = strings.Title(slug.MakeLang(m.PhotoTitle, "en"))
} else {
name = m.PhotoUUID
}
taken := m.TakenAtLocal.Format("20060102-150405")
token := rnd.Token(3)
result := fmt.Sprintf("%s-%s-%s.%s", taken, name, token, m.FileType)
return result
}

View File

@@ -4,9 +4,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/form"
)
/*func TestQuery_Photos(t *testing.T) {
@@ -28,324 +25,44 @@ import (
})
}*/
func TestQuery_PhotoByID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
func TestPhotoByID(t *testing.T) {
t.Run("photo found", func(t *testing.T) {
result, err := search.PhotoByID(1000000)
result, err := PhotoByID(1000000)
assert.Nil(t, err)
assert.Equal(t, 2790, result.PhotoYear)
})
t.Run("no photo found", func(t *testing.T) {
result, err := search.PhotoByID(99999)
result, err := PhotoByID(99999)
assert.Error(t, err, "record not found")
t.Log(result)
})
}
func TestQuery_PhotoByUUID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
func TestPhotoByUUID(t *testing.T) {
t.Run("photo found", func(t *testing.T) {
result, err := search.PhotoByUUID("pt9jtdre2lvl0y12")
result, err := PhotoByUUID("pt9jtdre2lvl0y12")
assert.Nil(t, err)
assert.Equal(t, "Reunion", result.PhotoTitle)
})
t.Run("no photo found", func(t *testing.T) {
result, err := search.PhotoByUUID("99999")
result, err := PhotoByUUID("99999")
assert.Error(t, err, "record not found")
t.Log(result)
})
}
func TestQuery_PreloadPhotoByUUID(t *testing.T) {
conf := config.TestConfig()
search := New(conf.Db())
func TestPreloadPhotoByUUID(t *testing.T) {
t.Run("photo found", func(t *testing.T) {
result, err := search.PreloadPhotoByUUID("pt9jtdre2lvl0y12")
result, err := PreloadPhotoByUUID("pt9jtdre2lvl0y12")
assert.Nil(t, err)
assert.Equal(t, "Reunion", result.PhotoTitle)
})
t.Run("no photo found", func(t *testing.T) {
result, err := search.PreloadPhotoByUUID("99999")
result, err := PreloadPhotoByUUID("99999")
assert.Error(t, err, "record not found")
t.Log(result)
})
}
func TestSearch_Photos(t *testing.T) {
conf := config.TestConfig()
conf.CreateDirectories()
search := New(conf.Db())
t.Run("normal query", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("label query", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "label:dog"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
// TODO: Add database fixtures to avoid failing queries
t.Logf("query failed: %s", err.Error())
// t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("invalid label query", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "label:xxx"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
assert.Error(t, err)
assert.Empty(t, photos)
if err != nil {
assert.Equal(t, err.Error(), "label xxx not found")
}
})
t.Run("form.location true", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
f.Location = true
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.camera", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
f.Camera = 2
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.color", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
f.Color = "blue"
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.favorites", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "favorites:true"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.country", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "country:de"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.title", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "title:Pug Dog"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.hash", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "hash:xxx"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.duplicate", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "duplicate:true"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.portrait", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "portrait:true"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.mono", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "mono:true"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.chroma", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "chroma:50"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.fmin and Order:oldest", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Fmin:5 Order:oldest"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.fmax and Order:newest", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Fmax:2 Order:newest"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.Lat and form.Lng and Order:imported", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Lat:33.45343166666667 Lng:25.764711666666667 Dist:2000 Order:imported"
f.Count = 3
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.Before and form.After", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Before:2005-01-01 After:2003-01-01"
f.Count = 5000
f.Offset = 0
photos, _, err := search.Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
}

275
internal/query/photos.go Normal file
View File

@@ -0,0 +1,275 @@
package query
import (
"fmt"
"strings"
"time"
"github.com/gosimple/slug"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/capture"
"github.com/photoprism/photoprism/pkg/txt"
)
// Photos searches for photos based on a Form and returns a PhotoResult slice.
func Photos(f form.PhotoSearch) (results PhotoResults, count int, err error) {
if err := f.ParseQueryString(); err != nil {
return results, 0, err
}
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("photos: %+v", f)))
s := UnscopedDb()
// s.LogMode(true)
s = s.Table("photos").
Select(`photos.*,
files.id AS file_id, files.file_uuid, files.file_primary, files.file_missing, files.file_name, files.file_hash,
files.file_type, files.file_mime, files.file_width, files.file_height, files.file_aspect_ratio,
files.file_orientation, files.file_main_color, files.file_colors, files.file_luminance, files.file_chroma,
files.file_diff,
cameras.camera_make, cameras.camera_model,
lenses.lens_make, lenses.lens_model,
places.loc_label, places.loc_city, places.loc_state, places.loc_country
`).
Joins("JOIN files ON files.photo_id = photos.id AND files.file_type = 'jpg' AND files.file_missing = 0 AND files.deleted_at IS NULL").
Joins("JOIN cameras ON cameras.id = photos.camera_id").
Joins("JOIN lenses ON lenses.id = photos.lens_id").
Joins("JOIN places ON photos.place_id = places.id").
Joins("LEFT JOIN photos_labels ON photos_labels.photo_id = photos.id AND photos_labels.uncertainty < 100").
Group("photos.id, files.id")
if f.ID != "" {
s = s.Where("photos.photo_uuid = ?", f.ID)
s = s.Order("files.file_primary DESC")
if result := s.Scan(&results); result.Error != nil {
return results, 0, result.Error
}
if f.Merged {
return results.Merged()
}
return results, len(results), nil
}
var categories []entity.Category
var label entity.Label
var labelIds []uint
if f.Label != "" {
slugString := strings.ToLower(f.Label)
if result := Db().First(&label, "label_slug =? OR custom_slug = ?", slugString, slugString); result.Error != nil {
log.Errorf("search: label %s not found", txt.Quote(f.Label))
return results, 0, fmt.Errorf("label %s not found", txt.Quote(f.Label))
} else {
labelIds = append(labelIds, label.ID)
Db().Where("category_id = ?", label.ID).Find(&categories)
for _, category := range categories {
labelIds = append(labelIds, category.LabelID)
}
s = s.Where("photos_labels.label_id IN (?)", labelIds)
}
}
if f.Location == true {
s = s.Where("location_id > 0")
if f.Query != "" {
s = s.Joins("LEFT JOIN photos_keywords ON photos_keywords.photo_id = photos.id").
Joins("LEFT JOIN keywords ON photos_keywords.keyword_id = keywords.id").
Where("keywords.keyword LIKE ?", strings.ToLower(txt.Clip(f.Query, txt.ClipKeyword))+"%")
}
} else if f.Query != "" {
if len(f.Query) < 2 {
return results, 0, fmt.Errorf("query too short")
}
slugString := slug.Make(f.Query)
lowerString := strings.ToLower(f.Query)
likeString := txt.Clip(lowerString, txt.ClipKeyword) + "%"
s = s.Joins("LEFT JOIN photos_keywords ON photos_keywords.photo_id = photos.id").
Joins("LEFT JOIN keywords ON photos_keywords.keyword_id = keywords.id")
if result := Db().First(&label, "label_slug = ? OR custom_slug = ?", slugString, slugString); result.Error != nil {
log.Infof("search: label %s not found, using fuzzy search", txt.Quote(f.Query))
s = s.Where("keywords.keyword LIKE ?", likeString)
} else {
labelIds = append(labelIds, label.ID)
Db().Where("category_id = ?", label.ID).Find(&categories)
for _, category := range categories {
labelIds = append(labelIds, category.LabelID)
}
log.Infof("search: label %s includes %d categories", txt.Quote(label.LabelName), len(labelIds))
s = s.Where("photos_labels.label_id IN (?) OR keywords.keyword LIKE ?", labelIds, likeString)
}
}
if f.Archived {
s = s.Where("photos.deleted_at IS NOT NULL")
} else {
s = s.Where("photos.deleted_at IS NULL")
if f.Private {
s = s.Where("photos.photo_private = 1")
} else if f.Public {
s = s.Where("photos.photo_private = 0")
}
if f.Review {
s = s.Where("photos.photo_quality < 3")
} else if f.Quality != 0 && f.Private == false {
s = s.Where("photos.photo_quality >= ?", f.Quality)
}
}
if f.Error {
s = s.Where("files.file_error <> ''")
}
if f.Album != "" {
s = s.Joins("JOIN photos_albums ON photos_albums.photo_uuid = photos.photo_uuid").Where("photos_albums.album_uuid = ?", f.Album)
}
if f.Camera > 0 {
s = s.Where("photos.camera_id = ?", f.Camera)
}
if f.Lens > 0 {
s = s.Where("photos.lens_id = ?", f.Lens)
}
if f.Year > 0 {
s = s.Where("photos.photo_year = ?", f.Year)
}
if f.Month > 0 {
s = s.Where("photos.photo_month = ?", f.Month)
}
if f.Color != "" {
s = s.Where("files.file_main_color = ?", strings.ToLower(f.Color))
}
if f.Favorites {
s = s.Where("photos.photo_favorite = 1")
}
if f.Story {
s = s.Where("photos.photo_story = 1")
}
if f.Country != "" {
s = s.Where("photos.photo_country = ?", f.Country)
}
if f.Title != "" {
s = s.Where("LOWER(photos.photo_title) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Title)))
}
if f.Hash != "" {
s = s.Where("files.file_hash = ?", f.Hash)
}
if f.Duplicate {
s = s.Where("files.file_duplicate = 1")
}
if f.Portrait {
s = s.Where("files.file_portrait = 1")
}
if f.Mono {
s = s.Where("files.file_chroma = 0")
} else if f.Chroma > 9 {
s = s.Where("files.file_chroma > ?", f.Chroma)
} else if f.Chroma > 0 {
s = s.Where("files.file_chroma > 0 AND files.file_chroma <= ?", f.Chroma)
}
if f.Diff != 0 {
s = s.Where("files.file_diff = ?", f.Diff)
}
if f.Fmin > 0 {
s = s.Where("photos.photo_f_number >= ?", f.Fmin)
}
if f.Fmax > 0 {
s = s.Where("photos.photo_f_number <= ?", f.Fmax)
}
if f.Dist == 0 {
f.Dist = 20
} else if f.Dist > 5000 {
f.Dist = 5000
}
// Inaccurate distance search, but probably 'good enough' for now
if f.Lat > 0 {
latMin := f.Lat - SearchRadius*float32(f.Dist)
latMax := f.Lat + SearchRadius*float32(f.Dist)
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latMin, latMax)
}
if f.Lng > 0 {
lngMin := f.Lng - SearchRadius*float32(f.Dist)
lngMax := f.Lng + SearchRadius*float32(f.Dist)
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngMin, lngMax)
}
if !f.Before.IsZero() {
s = s.Where("photos.taken_at <= ?", f.Before.Format("2006-01-02"))
}
if !f.After.IsZero() {
s = s.Where("photos.taken_at >= ?", f.After.Format("2006-01-02"))
}
switch f.Order {
case entity.SortOrderRelevance:
if f.Label != "" {
s = s.Order("photo_quality DESC, photos_labels.uncertainty ASC, taken_at DESC, files.file_primary DESC")
} else {
s = s.Order("photo_quality DESC, taken_at DESC, files.file_primary DESC")
}
case entity.SortOrderNewest:
s = s.Order("taken_at DESC, photos.photo_uuid, files.file_primary DESC")
case entity.SortOrderOldest:
s = s.Order("taken_at, photos.photo_uuid, files.file_primary DESC")
case entity.SortOrderImported:
s = s.Order("photos.id DESC, files.file_primary DESC")
case entity.SortOrderSimilar:
s = s.Order("files.file_main_color, photos.location_id, files.file_diff, taken_at DESC, files.file_primary DESC")
default:
s = s.Order("taken_at DESC, photos.photo_uuid, files.file_primary DESC")
}
if f.Count > 0 && f.Count <= 1000 {
s = s.Limit(f.Count).Offset(f.Offset)
} else {
s = s.Limit(100).Offset(0)
}
if result := s.Scan(&results); result.Error != nil {
return results, 0, result.Error
}
if f.Merged {
return results.Merged()
}
return results, len(results), nil
}

View File

@@ -0,0 +1,271 @@
package query
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/form"
)
func TestPhotos(t *testing.T) {
t.Run("normal query", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("label query", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "label:dog"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
// TODO: Add database fixtures to avoid failing queries
t.Logf("query failed: %s", err.Error())
// t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("invalid label query", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "label:xxx"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
assert.Error(t, err)
assert.Empty(t, photos)
if err != nil {
assert.Equal(t, err.Error(), "label xxx not found")
}
})
t.Run("form.location true", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
f.Location = true
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.camera", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
f.Camera = 2
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.color", func(t *testing.T) {
var f form.PhotoSearch
f.Query = ""
f.Count = 3
f.Offset = 0
f.Color = "blue"
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.favorites", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "favorites:true"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.country", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "country:de"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.title", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "title:Pug Dog"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.hash", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "hash:xxx"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.duplicate", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "duplicate:true"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.portrait", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "portrait:true"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.mono", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "mono:true"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.chroma", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "chroma:50"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.fmin and Order:oldest", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Fmin:5 Order:oldest"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.fmax and Order:newest", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Fmax:2 Order:newest"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.Lat and form.Lng and Order:imported", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Lat:33.45343166666667 Lng:25.764711666666667 Dist:2000 Order:imported"
f.Count = 3
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
t.Run("form.Before and form.After", func(t *testing.T) {
var f form.PhotoSearch
f.Query = "Before:2005-01-01 After:2003-01-01"
f.Count = 5000
f.Offset = 0
photos, _, err := Photos(f)
if err != nil {
t.Fatal(err)
}
t.Logf("results: %+v", photos)
})
}

View File

@@ -8,6 +8,7 @@ https://github.com/photoprism/photoprism/wiki
package query
import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/jinzhu/gorm"
@@ -36,3 +37,13 @@ func New(db *gorm.DB) *Query {
return q
}
// Db returns a database connection instance.
func Db() *gorm.DB {
return entity.Db()
}
// UnscopedDb returns an unscoped database connection instance.
func UnscopedDb() *gorm.DB {
return entity.Db().Unscoped()
}

View File

@@ -4,6 +4,7 @@ import (
"os"
"testing"
"github.com/photoprism/photoprism/internal/entity"
"github.com/sirupsen/logrus"
)
@@ -11,11 +12,13 @@ func TestMain(m *testing.M) {
log = logrus.StandardLogger()
log.SetLevel(logrus.DebugLevel)
// db := entity.InitTestDb(os.Getenv("PHOTOPRISM_TEST_DSN"))
db := entity.InitTestDb(os.Getenv("PHOTOPRISM_TEST_DSN"))
code := m.Run()
// db.Close()
if db != nil {
db.Close()
}
os.Exit(code)
}

View File

@@ -8,12 +8,12 @@ import (
)
// PhotoSelection returns all selected photos.
func (q *Query) PhotoSelection(f form.Selection) (results []entity.Photo, err error) {
func PhotoSelection(f form.Selection) (results []entity.Photo, err error) {
if f.Empty() {
return results, errors.New("no photos selected")
}
s := q.db.NewScope(nil).DB()
s := Db().NewScope(nil).DB()
s = s.Table("photos").
Select("photos.*").

View File

@@ -39,11 +39,8 @@ func (s *Share) Start() (err error) {
Share: true,
}
db := s.conf.Db()
q := query.New(db)
// Find accounts for which sharing is enabled
accounts, err := q.Accounts(f)
accounts, err := query.Accounts(f)
// Upload newly shared files
for _, a := range accounts {
@@ -55,7 +52,7 @@ func (s *Share) Start() (err error) {
continue
}
files, err := q.FileShares(a.ID, entity.FileShareNew)
files, err := query.FileShares(a.ID, entity.FileShareNew)
if err != nil {
log.Errorf("share: %s", err.Error())
@@ -121,7 +118,7 @@ func (s *Share) Start() (err error) {
return nil
}
if err := db.Save(&file).Error; err != nil {
if err := entity.Db().Save(&file).Error; err != nil {
log.Errorf("share: %s", err.Error())
}
}
@@ -137,7 +134,7 @@ func (s *Share) Start() (err error) {
continue
}
files, err := q.ExpiredFileShares(a)
files, err := query.ExpiredFileShares(a)
if err != nil {
log.Errorf("share: %s", err.Error())
@@ -166,7 +163,7 @@ func (s *Share) Start() (err error) {
file.Status = entity.FileShareRemoved
}
if err := db.Save(&file).Error; err != nil {
if err := entity.Db().Save(&file).Error; err != nil {
log.Errorf("share: %s", err.Error())
}
}

View File

@@ -16,14 +16,12 @@ import (
// Sync represents a sync worker.
type Sync struct {
conf *config.Config
q *query.Query
}
// NewSync returns a new service sync worker.
func NewSync(conf *config.Config) *Sync {
return &Sync{
conf: conf,
q: query.New(conf.Db()),
}
}
@@ -40,10 +38,7 @@ func (s *Sync) Start() (err error) {
Sync: true,
}
db := s.conf.Db()
q := s.q
accounts, err := q.Accounts(f)
accounts, err := query.Accounts(f)
for _, a := range accounts {
if a.AccType != remote.ServiceWebDAV {
@@ -117,7 +112,7 @@ func (s *Sync) Start() (err error) {
return nil
}
if err := db.First(&a, a.ID).Error; err != nil {
if err := entity.Db().First(&a, a.ID).Error; err != nil {
log.Errorf("sync: %s", err.Error())
return err
}
@@ -128,7 +123,7 @@ func (s *Sync) Start() (err error) {
a.SyncStatus = syncStatus
a.SyncDate = syncDate
if err := db.Save(&a).Error; err != nil {
if err := entity.Db().Save(&a).Error; err != nil {
log.Errorf("sync: %s", err.Error())
} else if synced {
event.Publish("sync.synced", event.Data{"account": a})

View File

@@ -8,6 +8,7 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/remote/webdav"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/fs"
@@ -20,12 +21,13 @@ func (s *Sync) downloadPath() string {
return s.conf.TempPath() + "/sync"
}
// relatedDownloads returns files to be downloaded grouped by prefix.
func (s *Sync) relatedDownloads(a entity.Account) (result Downloads, err error) {
result = make(Downloads)
maxResults := 1000
// Get remote files from database
files, err := s.q.FileSyncs(a.ID, entity.FileSyncNew, maxResults)
files, err := query.FileSyncs(a.ID, entity.FileSyncNew, maxResults)
if err != nil {
return result, err
@@ -48,10 +50,9 @@ func (s *Sync) relatedDownloads(a entity.Account) (result Downloads, err error)
// Downloads remote files in batches and imports / indexes them
func (s *Sync) download(a entity.Account) (complete bool, err error) {
db := s.conf.Db()
// Set up index worker
indexJobs := make(chan photoprism.IndexJob)
go photoprism.IndexWorker(indexJobs)
defer close(indexJobs)
@@ -118,7 +119,7 @@ func (s *Sync) download(a entity.Account) (complete bool, err error) {
}
}
if err := db.Save(&file).Error; err != nil {
if err := entity.Db().Save(&file).Error; err != nil {
log.Errorf("sync: %s", err.Error())
} else {
files[i] = file

View File

@@ -8,17 +8,16 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/remote/webdav"
)
// Uploads local files to a remote account
func (s *Sync) upload(a entity.Account) (complete bool, err error) {
db := s.conf.Db()
q := s.q
maxResults := 250
// Get upload file list from database
files, err := q.AccountUploads(a, maxResults)
files, err := query.AccountUploads(a, maxResults)
if err != nil {
return false, err
@@ -68,7 +67,7 @@ func (s *Sync) upload(a entity.Account) (complete bool, err error) {
return false, nil
}
if err := db.Save(&fileSync).Error; err != nil {
if err := entity.Db().Save(&fileSync).Error; err != nil {
log.Errorf("sync: %s", err.Error())
}
}