Camera and lens can now also be searched by name. Escaping and parsing of albums has been improved so that albums whose names start with and/or contain numbers will be found.
This commit is contained in:
parent
57dd9015e2
commit
9d110e8b80
20 changed files with 422 additions and 71 deletions
|
@ -52,6 +52,7 @@ func SearchPhotos(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
f.UID = ""
|
||||
f.Albums = ""
|
||||
f.Public = true
|
||||
f.Private = false
|
||||
f.Hidden = false
|
||||
|
|
|
@ -76,6 +76,39 @@ var PhotoAlbumFixtures = PhotoAlbumMap{
|
|||
Photo: PhotoFixtures.Pointer("Photo03"),
|
||||
Album: AlbumFixtures.Pointer("berlin-2019"),
|
||||
},
|
||||
"6": {
|
||||
PhotoUID: "pt9jtdre2lvl0yh0",
|
||||
AlbumUID: "at9lxuqxpogaaba8",
|
||||
Hidden: false,
|
||||
Missing: false,
|
||||
Order: 0,
|
||||
CreatedAt: time.Date(2020, 2, 6, 2, 6, 51, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2020, 4, 28, 14, 6, 0, 0, time.UTC),
|
||||
Photo: PhotoFixtures.Pointer("Photo03"),
|
||||
Album: AlbumFixtures.Pointer("berlin-2019"),
|
||||
},
|
||||
"7": {
|
||||
PhotoUID: "pt9jtdre2lvl0y21",
|
||||
AlbumUID: "at9lxuqxpogaaba7",
|
||||
Hidden: false,
|
||||
Missing: false,
|
||||
Order: 1,
|
||||
CreatedAt: time.Date(2020, 3, 6, 2, 6, 51, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2020, 5, 28, 14, 6, 0, 0, time.UTC),
|
||||
Photo: PhotoFixtures.Pointer("Photo14"),
|
||||
Album: AlbumFixtures.Pointer("berlin-2019"),
|
||||
},
|
||||
"8": {
|
||||
PhotoUID: "pt9jtdre2lvl0y21",
|
||||
AlbumUID: "at9lxuqxpogaaba8",
|
||||
Hidden: false,
|
||||
Missing: false,
|
||||
Order: 1,
|
||||
CreatedAt: time.Date(2020, 3, 6, 2, 6, 51, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2020, 5, 28, 14, 6, 0, 0, time.UTC),
|
||||
Photo: PhotoFixtures.Pointer("Photo14"),
|
||||
Album: AlbumFixtures.Pointer("berlin-2019"),
|
||||
},
|
||||
}
|
||||
|
||||
// CreatePhotoAlbumFixtures inserts known entities into the database for testing.
|
||||
|
|
|
@ -63,8 +63,8 @@ type SearchPhotos struct {
|
|||
Faces string `form:"faces"` // Find or exclude faces if detected.
|
||||
Quality int `form:"quality"`
|
||||
Review bool `form:"review"`
|
||||
Camera int `form:"camera"`
|
||||
Lens int `form:"lens"`
|
||||
Camera string `form:"camera"`
|
||||
Lens string `form:"lens"`
|
||||
Before time.Time `form:"before" time_format:"2006-01-02"`
|
||||
After time.Time `form:"after" time_format:"2006-01-02"`
|
||||
Count int `form:"count" binding:"required" serialize:"-"`
|
||||
|
|
|
@ -104,7 +104,7 @@ func TestParseQueryString(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "cat", form.Label)
|
||||
assert.Equal(t, "fooBar baz", form.Query)
|
||||
assert.Equal(t, 23, form.Camera)
|
||||
assert.Equal(t, "23", form.Camera)
|
||||
assert.Equal(t, time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), form.Before)
|
||||
assert.Equal(t, false, form.Favorite)
|
||||
assert.Equal(t, uint(0x61a8), form.Dist)
|
||||
|
@ -204,18 +204,27 @@ func TestParseQueryString(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "strconv.Atoi: parsing \"cat\": invalid syntax", err.Error())
|
||||
})
|
||||
t.Run("query for camera with invalid type", func(t *testing.T) {
|
||||
t.Run("CameraString", func(t *testing.T) {
|
||||
form := &SearchPhotos{Query: "camera:cat"}
|
||||
|
||||
err := form.ParseQueryString()
|
||||
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// log.Debugf("%+v\n", form)
|
||||
assert.Equal(t, "cat", form.Camera)
|
||||
})
|
||||
t.Run("LensString", func(t *testing.T) {
|
||||
form := &SearchPhotos{Query: "lens:cat"}
|
||||
|
||||
assert.Equal(t, "strconv.Atoi: parsing \"cat\": invalid syntax", err.Error())
|
||||
err := form.ParseQueryString()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "cat", form.Lens)
|
||||
})
|
||||
t.Run("query for before with invalid type", func(t *testing.T) {
|
||||
form := &SearchPhotos{Query: "before:cat"}
|
||||
|
|
|
@ -32,7 +32,7 @@ func TestAlbumCoverByUID(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "2790/07/27900704_070228_D6D51B6C.jpg", file.FileName)
|
||||
assert.Equal(t, "1990/04/bridge2.jpg", file.FileName)
|
||||
})
|
||||
|
||||
t.Run("existing uid folder album", func(t *testing.T) {
|
||||
|
|
|
@ -194,13 +194,12 @@ func Geo(f form.SearchGeo) (results GeoResults, err error) {
|
|||
}
|
||||
} else if f.Unsorted && f.Filter == "" {
|
||||
s = s.Where("photos.photo_uid NOT IN (SELECT photo_uid FROM photos_albums pa WHERE pa.hidden = 0)")
|
||||
} else if f.Albums != "" || f.Album != "" {
|
||||
if f.Albums == "" {
|
||||
f.Albums = f.Album
|
||||
}
|
||||
|
||||
} else if txt.NotEmpty(f.Album) {
|
||||
v := strings.Trim(f.Album, "*%") + "%"
|
||||
s = s.Where("photos.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid AND pa.hidden = 0 WHERE (a.album_title LIKE ? OR a.album_slug LIKE ?))", v, v)
|
||||
} else if txt.NotEmpty(f.Albums) {
|
||||
for _, where := range LikeAnyWord("a.album_title", f.Albums) {
|
||||
s = s.Where("photos.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid WHERE (?))", gorm.Expr(where))
|
||||
s = s.Where("photos.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid AND pa.hidden = 0 WHERE (?))", gorm.Expr(where))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -361,9 +361,9 @@ func TestGeo(t *testing.T) {
|
|||
|
||||
assert.GreaterOrEqual(t, len(photos), 1)
|
||||
})
|
||||
t.Run("albums", func(t *testing.T) {
|
||||
t.Run("Album", func(t *testing.T) {
|
||||
var f form.SearchGeo
|
||||
f.Albums = "2030"
|
||||
f.Album = "Berlin"
|
||||
|
||||
photos, err := Geo(f)
|
||||
|
||||
|
@ -371,9 +371,21 @@ func TestGeo(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.GreaterOrEqual(t, len(photos), 10)
|
||||
assert.Equal(t, 1, len(photos))
|
||||
})
|
||||
t.Run("path or path", func(t *testing.T) {
|
||||
t.Run("Albums", func(t *testing.T) {
|
||||
var f form.SearchGeo
|
||||
f.Albums = "Holiday|Christmas"
|
||||
|
||||
photos, err := Geo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 2, len(photos))
|
||||
})
|
||||
t.Run("PathOrPath", func(t *testing.T) {
|
||||
var f form.SearchGeo
|
||||
f.Path = "1990/04" + "|" + "2015/11"
|
||||
|
||||
|
@ -591,7 +603,7 @@ func TestGeo(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f.Query = "albums:Berlin&Holiday"
|
||||
f.Query = "albums:\"Berlin&Holiday|Christmas\""
|
||||
|
||||
photos2, err2 := Geo(f)
|
||||
|
||||
|
|
|
@ -5,10 +5,9 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/jinzhu/inflection"
|
||||
)
|
||||
|
||||
|
@ -37,7 +36,7 @@ func LikeAny(col, s string, keywords, exact bool) (wheres []string) {
|
|||
if keywords {
|
||||
words = txt.UniqueKeywords(k)
|
||||
} else {
|
||||
words = txt.UniqueWords(txt.Words(k))
|
||||
words = txt.UniqueWords(strings.Fields(k))
|
||||
}
|
||||
|
||||
if len(words) == 0 {
|
||||
|
@ -46,9 +45,9 @@ func LikeAny(col, s string, keywords, exact bool) (wheres []string) {
|
|||
|
||||
for _, w := range words {
|
||||
if wildcardThreshold > 0 && len(w) >= wildcardThreshold {
|
||||
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%s%%'", col, w))
|
||||
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%s%%'", col, SqlLike(w)))
|
||||
} else {
|
||||
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%s'", col, w))
|
||||
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%s'", col, SqlLike(w)))
|
||||
}
|
||||
|
||||
if !keywords || !txt.ContainsASCIILetters(w) {
|
||||
|
@ -58,7 +57,7 @@ func LikeAny(col, s string, keywords, exact bool) (wheres []string) {
|
|||
singular := inflection.Singular(w)
|
||||
|
||||
if singular != w {
|
||||
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%s'", col, singular))
|
||||
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%s'", col, SqlLike(singular)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,7 +92,7 @@ func LikeAll(col, s string, keywords, exact bool) (wheres []string) {
|
|||
words = txt.UniqueKeywords(s)
|
||||
wildcardThreshold = 4
|
||||
} else {
|
||||
words = txt.UniqueWords(txt.Words(s))
|
||||
words = txt.UniqueWords(strings.Fields(s))
|
||||
wildcardThreshold = 2
|
||||
}
|
||||
|
||||
|
@ -105,9 +104,9 @@ func LikeAll(col, s string, keywords, exact bool) (wheres []string) {
|
|||
|
||||
for _, w := range words {
|
||||
if wildcardThreshold > 0 && len(w) >= wildcardThreshold {
|
||||
wheres = append(wheres, fmt.Sprintf("%s LIKE '%s%%'", col, w))
|
||||
wheres = append(wheres, fmt.Sprintf("%s LIKE '%s%%'", col, SqlLike(w)))
|
||||
} else {
|
||||
wheres = append(wheres, fmt.Sprintf("%s LIKE '%s'", col, w))
|
||||
wheres = append(wheres, fmt.Sprintf("%s LIKE '%s'", col, SqlLike(w)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,9 +141,9 @@ func LikeAllNames(cols Cols, s string) (wheres []string) {
|
|||
|
||||
for _, c := range cols {
|
||||
if strings.Contains(w, txt.Space) {
|
||||
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%s%%'", c, w))
|
||||
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%s%%'", c, SqlLike(w)))
|
||||
} else {
|
||||
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%%%s%%'", c, w))
|
||||
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%%%s%%'", c, SqlLike(w)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -191,7 +190,7 @@ func AnySlug(col, search, sep string) (where string) {
|
|||
}
|
||||
|
||||
for _, w := range words {
|
||||
wheres = append(wheres, fmt.Sprintf("%s = '%s'", col, w))
|
||||
wheres = append(wheres, fmt.Sprintf("%s = '%s'", col, SqlLike(w)))
|
||||
}
|
||||
|
||||
return strings.Join(wheres, " OR ")
|
||||
|
@ -233,7 +232,7 @@ func AnyInt(col, numbers, sep string, min, max int) (where string) {
|
|||
|
||||
// OrLike returns a where condition and values for finding multiple terms combined with OR.
|
||||
func OrLike(col, s string) (where string, values []interface{}) {
|
||||
if col == "" || s == "" {
|
||||
if txt.IsEmpty(col) || txt.IsEmpty(s) {
|
||||
return "", []interface{}{}
|
||||
}
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ func TestLikeAnyKeyword(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLikeAnyWord(t *testing.T) {
|
||||
t.Run("and_or_search", func(t *testing.T) {
|
||||
t.Run("SearchAndOr", func(t *testing.T) {
|
||||
if w := LikeAnyWord("k.keyword", "table spoon & usa | img json"); len(w) != 2 {
|
||||
t.Fatal("two where conditions expected")
|
||||
} else {
|
||||
|
@ -110,7 +110,7 @@ func TestLikeAnyWord(t *testing.T) {
|
|||
assert.Equal(t, "k.keyword LIKE 'img%' OR k.keyword LIKE 'json%' OR k.keyword LIKE 'usa%'", w[1])
|
||||
}
|
||||
})
|
||||
t.Run("and_or_search_en", func(t *testing.T) {
|
||||
t.Run("SearchAndOrEnglish", func(t *testing.T) {
|
||||
if w := LikeAnyWord("k.keyword", "table spoon and usa or img json"); len(w) != 2 {
|
||||
t.Fatal("two where conditions expected")
|
||||
} else {
|
||||
|
@ -118,6 +118,14 @@ func TestLikeAnyWord(t *testing.T) {
|
|||
assert.Equal(t, "k.keyword LIKE 'img%' OR k.keyword LIKE 'json%' OR k.keyword LIKE 'usa%'", w[1])
|
||||
}
|
||||
})
|
||||
t.Run("EscapeSql", func(t *testing.T) {
|
||||
if w := LikeAnyWord("k.keyword", "table% | 'spoon' & \"usa"); len(w) != 2 {
|
||||
t.Fatalf("two where conditions expected: %#v", w)
|
||||
} else {
|
||||
assert.Equal(t, "k.keyword LIKE 'spoon%' OR k.keyword LIKE 'table%'", w[0])
|
||||
assert.Equal(t, "k.keyword LIKE '\\\"usa%'", w[1])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLikeAll(t *testing.T) {
|
||||
|
|
|
@ -91,7 +91,7 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
|||
s = s.Where("files.file_primary = 1")
|
||||
}
|
||||
|
||||
if f.UID != "" {
|
||||
if txt.NotEmpty(f.UID) {
|
||||
s = s.Where("photos.photo_uid IN (?)", strings.Split(strings.ToLower(f.UID), txt.Or))
|
||||
|
||||
// Take shortcut?
|
||||
|
@ -117,7 +117,7 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
|||
var labels []entity.Label
|
||||
var labelIds []uint
|
||||
|
||||
if f.Label != "" {
|
||||
if txt.NotEmpty(f.Label) {
|
||||
if err := Db().Where(AnySlug("label_slug", f.Label, txt.Or)).Or(AnySlug("custom_slug", f.Label, txt.Or)).Find(&labels).Error; len(labels) == 0 || err != nil {
|
||||
log.Debugf("search: label %s not found", txt.LogParamLower(f.Label))
|
||||
return PhotoResults{}, 0, nil
|
||||
|
@ -225,7 +225,7 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
|||
}
|
||||
|
||||
// Search for one or more keywords?
|
||||
if f.Keywords != "" {
|
||||
if txt.NotEmpty(f.Keywords) {
|
||||
for _, where := range LikeAnyWord("k.keyword", f.Keywords) {
|
||||
s = s.Where("photos.id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))", gorm.Expr(where))
|
||||
}
|
||||
|
@ -261,7 +261,7 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
|||
}
|
||||
|
||||
// Filter for one or more subjects?
|
||||
if f.Subject != "" {
|
||||
if txt.NotEmpty(f.Subject) {
|
||||
for _, subj := range strings.Split(strings.ToLower(f.Subject), txt.And) {
|
||||
if subjects := strings.Split(subj, txt.Or); rnd.ContainsUIDs(subjects, 'j') {
|
||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE subj_uid IN (?))",
|
||||
|
@ -271,7 +271,7 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
|||
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(AnySlug("s.subj_slug", subj, txt.Or)))
|
||||
}
|
||||
}
|
||||
} else if f.Subjects != "" {
|
||||
} else if txt.NotEmpty(f.Subjects) {
|
||||
for _, where := range LikeAllNames(Cols{"subj_name", "subj_alias"}, f.Subjects) {
|
||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 JOIN %s s ON s.subj_uid = m.subj_uid WHERE (?))",
|
||||
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(where))
|
||||
|
@ -301,14 +301,20 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Filter by camera?
|
||||
if f.Camera > 0 {
|
||||
s = s.Where("photos.camera_id = ?", f.Camera)
|
||||
// Filter by camera id or name?
|
||||
if txt.IsPosInt(f.Camera) {
|
||||
s = s.Where("photos.camera_id = ?", txt.UInt(f.Camera))
|
||||
} else if txt.NotEmpty(f.Camera) {
|
||||
v := strings.Trim(f.Camera, "*%") + "%"
|
||||
s = s.Where("cameras.camera_make LIKE ? OR cameras.camera_model LIKE ? OR cameras.camera_slug LIKE ?", v, v, v)
|
||||
}
|
||||
|
||||
// Filter by camera lens?
|
||||
if f.Lens > 0 {
|
||||
s = s.Where("photos.lens_id = ?", f.Lens)
|
||||
// Filter by lens id or name?
|
||||
if txt.IsPosInt(f.Lens) {
|
||||
s = s.Where("photos.lens_id = ?", txt.UInt(f.Lens))
|
||||
} else if txt.NotEmpty(f.Lens) {
|
||||
v := strings.Trim(f.Lens, "*%") + "%"
|
||||
s = s.Where("lenses.lens_make LIKE ? OR lenses.lens_model LIKE ? OR lenses.lens_slug LIKE ?", v, v, v)
|
||||
}
|
||||
|
||||
// Filter by year?
|
||||
|
@ -358,23 +364,23 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
|||
}
|
||||
|
||||
// Filter by location country?
|
||||
if f.Country != "" {
|
||||
if txt.NotEmpty(f.Country) {
|
||||
s = s.Where("photos.photo_country IN (?)", strings.Split(strings.ToLower(f.Country), txt.Or))
|
||||
}
|
||||
|
||||
// Filter by location state?
|
||||
if f.State != "" {
|
||||
if txt.NotEmpty(f.State) {
|
||||
s = s.Where("places.place_state IN (?)", strings.Split(f.State, txt.Or))
|
||||
}
|
||||
|
||||
// Filter by location category?
|
||||
if f.Category != "" {
|
||||
if txt.NotEmpty(f.Category) {
|
||||
s = s.Joins("JOIN cells ON photos.cell_id = cells.id").
|
||||
Where("cells.cell_category IN (?)", strings.Split(strings.ToLower(f.Category), txt.Or))
|
||||
}
|
||||
|
||||
// Filter by media type?
|
||||
if f.Type != "" {
|
||||
if txt.NotEmpty(f.Type) {
|
||||
s = s.Where("photos.photo_type IN (?)", strings.Split(strings.ToLower(f.Type), txt.Or))
|
||||
} else if f.Video {
|
||||
s = s.Where("photos.photo_type = 'video'")
|
||||
|
@ -387,7 +393,7 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
|||
}
|
||||
|
||||
// Filter by storage path?
|
||||
if f.Path != "" {
|
||||
if txt.NotEmpty(f.Path) {
|
||||
p := f.Path
|
||||
|
||||
if strings.HasPrefix(p, "/") {
|
||||
|
@ -403,7 +409,7 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
|||
}
|
||||
|
||||
// Filter by primary file name without path and extension.
|
||||
if f.Name != "" {
|
||||
if txt.NotEmpty(f.Name) {
|
||||
where, names := OrLike("photos.photo_name", f.Name)
|
||||
|
||||
// Omit file path and known extensions.
|
||||
|
@ -415,25 +421,25 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
|||
}
|
||||
|
||||
// Filter by complete file names?
|
||||
if f.Filename != "" {
|
||||
if txt.NotEmpty(f.Filename) {
|
||||
where, values := OrLike("files.file_name", f.Filename)
|
||||
s = s.Where(where, values...)
|
||||
}
|
||||
|
||||
// Filter by original file name?
|
||||
if f.Original != "" {
|
||||
if txt.NotEmpty(f.Original) {
|
||||
where, values := OrLike("photos.original_name", f.Original)
|
||||
s = s.Where(where, values...)
|
||||
}
|
||||
|
||||
// Filter by photo title?
|
||||
if f.Title != "" {
|
||||
if txt.NotEmpty(f.Title) {
|
||||
where, values := OrLike("photos.photo_title", f.Title)
|
||||
s = s.Where(where, values...)
|
||||
}
|
||||
|
||||
// Filter by file hash?
|
||||
if f.Hash != "" {
|
||||
if txt.NotEmpty(f.Hash) {
|
||||
s = s.Where("files.file_hash IN (?)", strings.Split(strings.ToLower(f.Hash), txt.Or))
|
||||
}
|
||||
|
||||
|
@ -498,11 +504,10 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
|||
}
|
||||
} else if f.Unsorted && f.Filter == "" {
|
||||
s = s.Where("photos.photo_uid NOT IN (SELECT photo_uid FROM photos_albums pa WHERE pa.hidden = 0)")
|
||||
} else if f.Albums != "" || f.Album != "" {
|
||||
if f.Albums == "" {
|
||||
f.Albums = f.Album
|
||||
}
|
||||
|
||||
} else if txt.NotEmpty(f.Album) {
|
||||
v := strings.Trim(f.Album, "*%") + "%"
|
||||
s = s.Where("photos.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid AND pa.hidden = 0 WHERE (a.album_title LIKE ? OR a.album_slug LIKE ?))", v, v)
|
||||
} else if txt.NotEmpty(f.Albums) {
|
||||
for _, where := range LikeAnyWord("a.album_title", f.Albums) {
|
||||
s = s.Where("photos.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid AND pa.hidden = 0 WHERE (?))", gorm.Expr(where))
|
||||
}
|
||||
|
|
|
@ -295,7 +295,7 @@ func TestPhotos(t *testing.T) {
|
|||
f.Query = ""
|
||||
f.Count = 10
|
||||
f.Offset = 0
|
||||
f.Camera = 1000003
|
||||
f.Camera = "1000003"
|
||||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
|
@ -603,12 +603,42 @@ func TestPhotos(t *testing.T) {
|
|||
}
|
||||
assert.LessOrEqual(t, 1, len(photos))
|
||||
})
|
||||
t.Run("search for camera name", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
f.Query = ""
|
||||
f.Count = 1
|
||||
f.Offset = 0
|
||||
f.Camera = "canon"
|
||||
f.Lens = ""
|
||||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.LessOrEqual(t, 1, len(photos))
|
||||
})
|
||||
t.Run("search for lens name", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
f.Query = ""
|
||||
f.Count = 1
|
||||
f.Offset = 0
|
||||
f.Camera = ""
|
||||
f.Lens = "apple"
|
||||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.LessOrEqual(t, 1, len(photos))
|
||||
})
|
||||
t.Run("search for lens, month, year, album", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
f.Query = ""
|
||||
f.Count = 5000
|
||||
f.Offset = 0
|
||||
f.Lens = 1000000
|
||||
f.Lens = "1000000"
|
||||
f.Month = strconv.Itoa(7)
|
||||
f.Year = strconv.Itoa(2790)
|
||||
f.Album = "at9lxuqxpogaaba8"
|
||||
|
@ -1393,7 +1423,7 @@ func TestPhotos(t *testing.T) {
|
|||
|
||||
assert.Greater(t, len(photos), len(photos2))
|
||||
})
|
||||
t.Run("albums and and or search", func(t *testing.T) {
|
||||
t.Run("AlbumsOrSearch", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
f.Query = "albums:Holiday|Berlin"
|
||||
|
||||
|
@ -1403,14 +1433,19 @@ func TestPhotos(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f.Query = "albums:Berlin&Holiday"
|
||||
assert.Greater(t, len(photos), 5)
|
||||
})
|
||||
t.Run("AlbumsAndSearch", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
|
||||
photos2, _, err2 := Photos(f)
|
||||
f.Query = "albums:\"Berlin&Holiday\""
|
||||
|
||||
if err2 != nil {
|
||||
t.Fatal(err2)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Greater(t, len(photos), len(photos2))
|
||||
assert.Greater(t, len(photos), 0)
|
||||
})
|
||||
t.Run("people and and or search", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
|
|
12
internal/search/sql.go
Normal file
12
internal/search/sql.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||
)
|
||||
|
||||
// SqlLike escapes a string for use in an SQL query.
|
||||
func SqlLike(s string) string {
|
||||
return strings.Trim(sanitize.SqlString(s), " |&*%")
|
||||
}
|
25
internal/search/sql_test.go
Normal file
25
internal/search/sql_test.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSqlLike(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.Equal(t, "", SqlLike(""))
|
||||
})
|
||||
t.Run("Special", func(t *testing.T) {
|
||||
s := "' \" \t \n %_''"
|
||||
exp := "\\' \\\" %\\_\\'\\'"
|
||||
result := SqlLike(s)
|
||||
t.Logf("String..: %s", s)
|
||||
t.Logf("Expected: %s", exp)
|
||||
t.Logf("Result..: %s", result)
|
||||
assert.Equal(t, exp, result)
|
||||
})
|
||||
t.Run("Alnum", func(t *testing.T) {
|
||||
assert.Equal(t, "123ABCabc", SqlLike(" 123ABCabc%* "))
|
||||
})
|
||||
}
|
40
pkg/sanitize/sql.go
Normal file
40
pkg/sanitize/sql.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package sanitize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// sqlSpecialBytes contains special bytes to escape in SQL search queries.
|
||||
var sqlSpecialBytes = []byte{34, 39, 92, 95}
|
||||
|
||||
// SqlString escapes a string for use in an SQL query.
|
||||
func SqlString(s string) string {
|
||||
var i int
|
||||
for i = 0; i < len(s); i++ {
|
||||
if bytes.Contains(sqlSpecialBytes, []byte{s[i]}) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// No special characters found, return original string.
|
||||
if i >= len(s) {
|
||||
return s
|
||||
}
|
||||
|
||||
b := make([]byte, 2*len(s)-i)
|
||||
copy(b, s[:i])
|
||||
j := i
|
||||
for ; i < len(s); i++ {
|
||||
if s[i] < 31 {
|
||||
// Ignore control chars.
|
||||
continue
|
||||
}
|
||||
if bytes.Contains(sqlSpecialBytes, []byte{s[i]}) {
|
||||
b[j] = '\\'
|
||||
j++
|
||||
}
|
||||
b[j] = s[i]
|
||||
j++
|
||||
}
|
||||
return string(b[:j])
|
||||
}
|
25
pkg/sanitize/sql_test.go
Normal file
25
pkg/sanitize/sql_test.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package sanitize
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSqlString(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.Equal(t, "", SqlString(""))
|
||||
})
|
||||
t.Run("Special", func(t *testing.T) {
|
||||
s := "' \" \t \n %_''"
|
||||
exp := "\\' \\\" %\\_\\'\\'"
|
||||
result := SqlString(s)
|
||||
t.Logf("String..: %s", s)
|
||||
t.Logf("Expected: %s", exp)
|
||||
t.Logf("Result..: %s", result)
|
||||
assert.Equal(t, exp, result)
|
||||
})
|
||||
t.Run("Alnum", func(t *testing.T) {
|
||||
assert.Equal(t, "123ABCabc", SqlString("123ABCabc"))
|
||||
})
|
||||
}
|
23
pkg/txt/empty.go
Normal file
23
pkg/txt/empty.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package txt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsEmpty tests if a string represents an empty/invalid value.
|
||||
func IsEmpty(s string) bool {
|
||||
s = strings.Trim(strings.TrimSpace(s), "%*")
|
||||
|
||||
if s == "" || s == "0" || s == "-1" {
|
||||
return true
|
||||
}
|
||||
|
||||
s = strings.ToLower(s)
|
||||
|
||||
return s == "nil" || s == "null" || s == "nan"
|
||||
}
|
||||
|
||||
// NotEmpty tests if a string does not represent an empty/invalid value.
|
||||
func NotEmpty(s string) bool {
|
||||
return !IsEmpty(s)
|
||||
}
|
97
pkg/txt/empty_test.go
Normal file
97
pkg/txt/empty_test.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
package txt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsEmpty(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.Equal(t, true, IsEmpty(""))
|
||||
})
|
||||
t.Run("EnNew", func(t *testing.T) {
|
||||
assert.Equal(t, false, IsEmpty(EnNew))
|
||||
})
|
||||
t.Run("Spaces", func(t *testing.T) {
|
||||
assert.Equal(t, false, IsEmpty(" new "))
|
||||
})
|
||||
t.Run("Uppercase", func(t *testing.T) {
|
||||
assert.Equal(t, false, IsEmpty("NEW"))
|
||||
})
|
||||
t.Run("Lowercase", func(t *testing.T) {
|
||||
assert.Equal(t, false, IsEmpty("new"))
|
||||
})
|
||||
t.Run("True", func(t *testing.T) {
|
||||
assert.Equal(t, false, IsEmpty("New"))
|
||||
})
|
||||
t.Run("False", func(t *testing.T) {
|
||||
assert.Equal(t, false, IsEmpty("non"))
|
||||
})
|
||||
t.Run("0", func(t *testing.T) {
|
||||
assert.Equal(t, true, IsEmpty("0"))
|
||||
})
|
||||
t.Run("-1", func(t *testing.T) {
|
||||
assert.Equal(t, true, IsEmpty("-1"))
|
||||
})
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
assert.Equal(t, true, IsEmpty("nil"))
|
||||
})
|
||||
t.Run("NaN", func(t *testing.T) {
|
||||
assert.Equal(t, true, IsEmpty("NaN"))
|
||||
})
|
||||
t.Run("NULL", func(t *testing.T) {
|
||||
assert.Equal(t, true, IsEmpty("NULL"))
|
||||
})
|
||||
t.Run("*", func(t *testing.T) {
|
||||
assert.Equal(t, true, IsEmpty("*"))
|
||||
})
|
||||
t.Run("%", func(t *testing.T) {
|
||||
assert.Equal(t, true, IsEmpty("%"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNotEmpty(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.Equal(t, false, NotEmpty(""))
|
||||
})
|
||||
t.Run("EnNew", func(t *testing.T) {
|
||||
assert.Equal(t, true, NotEmpty(EnNew))
|
||||
})
|
||||
t.Run("Spaces", func(t *testing.T) {
|
||||
assert.Equal(t, true, NotEmpty(" new "))
|
||||
})
|
||||
t.Run("Uppercase", func(t *testing.T) {
|
||||
assert.Equal(t, true, NotEmpty("NEW"))
|
||||
})
|
||||
t.Run("Lowercase", func(t *testing.T) {
|
||||
assert.Equal(t, true, NotEmpty("new"))
|
||||
})
|
||||
t.Run("True", func(t *testing.T) {
|
||||
assert.Equal(t, true, NotEmpty("New"))
|
||||
})
|
||||
t.Run("False", func(t *testing.T) {
|
||||
assert.Equal(t, true, NotEmpty("non"))
|
||||
})
|
||||
t.Run("0", func(t *testing.T) {
|
||||
assert.Equal(t, false, NotEmpty("0"))
|
||||
})
|
||||
t.Run("-1", func(t *testing.T) {
|
||||
assert.Equal(t, false, NotEmpty("-1"))
|
||||
})
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
assert.Equal(t, false, NotEmpty("nil"))
|
||||
})
|
||||
t.Run("NaN", func(t *testing.T) {
|
||||
assert.Equal(t, false, NotEmpty("NaN"))
|
||||
})
|
||||
t.Run("NULL", func(t *testing.T) {
|
||||
assert.Equal(t, false, NotEmpty("NULL"))
|
||||
})
|
||||
t.Run("*", func(t *testing.T) {
|
||||
assert.Equal(t, false, NotEmpty("*"))
|
||||
})
|
||||
t.Run("%", func(t *testing.T) {
|
||||
assert.Equal(t, false, NotEmpty("%"))
|
||||
})
|
||||
}
|
|
@ -49,3 +49,18 @@ func IsUInt(s string) bool {
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsPosInt checks if a string represents an integer greater than 0.
|
||||
func IsPosInt(s string) bool {
|
||||
if s == "" || s == " " || s == "0" || s == "-1" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r := range s {
|
||||
if r < 48 || r > 57 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -38,6 +38,19 @@ func TestIsUInt(t *testing.T) {
|
|||
assert.True(t, IsUInt("123"))
|
||||
}
|
||||
|
||||
func TestIsPosInt(t *testing.T) {
|
||||
assert.False(t, IsPosInt(""))
|
||||
assert.False(t, IsPosInt("12 3"))
|
||||
assert.True(t, IsPosInt("123"))
|
||||
assert.False(t, IsPosInt(" "))
|
||||
assert.False(t, IsPosInt("-1"))
|
||||
assert.False(t, IsPosInt("0"))
|
||||
assert.False(t, IsPosInt("0.1"))
|
||||
assert.False(t, IsPosInt("0,1"))
|
||||
assert.True(t, IsPosInt("1"))
|
||||
assert.True(t, IsPosInt("99943546356"))
|
||||
}
|
||||
|
||||
func TestUInt(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
result := UInt("")
|
||||
|
|
|
@ -15,7 +15,7 @@ func Bool(s string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// Yes returns true if a string represents "yes".
|
||||
// Yes tests if a string represents "yes".
|
||||
func Yes(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
|
@ -26,7 +26,7 @@ func Yes(s string) bool {
|
|||
return strings.IndexAny(s, "ytjposiд") == 0
|
||||
}
|
||||
|
||||
// No returns true if a string represents "no".
|
||||
// No tests if a string represents "no".
|
||||
func No(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
|
@ -37,7 +37,7 @@ func No(s string) bool {
|
|||
return strings.IndexAny(s, "0nhufeн") == 0
|
||||
}
|
||||
|
||||
// New returns true if a string represents "new".
|
||||
// New tests if a string represents "new".
|
||||
func New(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
|
|
Loading…
Reference in a new issue