Albums: Improve parameter validation for database queries #3320

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2023-04-01 14:25:05 +02:00
parent 9a3d61c99f
commit b6378a5c1f
12 changed files with 186 additions and 113 deletions

View file

@ -82,7 +82,7 @@ func CreateAlbum(router *gin.RouterGroup) {
albumMutex.Lock()
defer albumMutex.Unlock()
a := entity.NewUserAlbum(f.AlbumTitle, entity.AlbumDefault, s.UserUID)
a := entity.NewUserAlbum(f.AlbumTitle, entity.AlbumManual, s.UserUID)
a.AlbumFavorite = f.AlbumFavorite
// Existing album?
@ -108,9 +108,9 @@ func CreateAlbum(router *gin.RouterGroup) {
}
}
// Publish event and create/update YAML backup.
UpdateClientConfig()
// PublishAlbumEvent(EntityCreated, a.AlbumUID, c)
// Update album YAML backup.
SaveAlbumAsYaml(*a)
// Return as JSON.
@ -162,8 +162,7 @@ func UpdateAlbum(router *gin.RouterGroup) {
UpdateClientConfig()
// PublishAlbumEvent(EntityUpdated, uid, c)
// Update album YAML backup.
SaveAlbumAsYaml(a)
c.JSON(http.StatusOK, a)
@ -212,6 +211,7 @@ func DeleteAlbum(router *gin.RouterGroup) {
UpdateClientConfig()
// Update album YAML backup.
SaveAlbumAsYaml(a)
c.JSON(http.StatusOK, a)
@ -250,6 +250,7 @@ func LikeAlbum(router *gin.RouterGroup) {
PublishAlbumEvent(EntityUpdated, id, c)
// Update album YAML backup.
SaveAlbumAsYaml(a)
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgChangesSaved))
@ -288,6 +289,7 @@ func DislikeAlbum(router *gin.RouterGroup) {
PublishAlbumEvent(EntityUpdated, id, c)
// Update album YAML backup.
SaveAlbumAsYaml(a)
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgChangesSaved))
@ -344,6 +346,7 @@ func CloneAlbums(router *gin.RouterGroup) {
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
// Update album YAML backup.
SaveAlbumAsYaml(a)
}
@ -375,6 +378,12 @@ func AddPhotosToAlbum(router *gin.RouterGroup) {
if err != nil {
AbortAlbumNotFound(c)
return
} else if !a.HasID() {
AbortAlbumNotFound(c)
return
} else if f.Empty() {
Abort(c, http.StatusBadRequest, i18n.ErrNoItemsSelected)
return
}
// Fetch selection from index.
@ -399,6 +408,7 @@ func AddPhotosToAlbum(router *gin.RouterGroup) {
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
// Update album YAML backup.
SaveAlbumAsYaml(a)
}
@ -434,6 +444,9 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup) {
if err != nil {
AbortAlbumNotFound(c)
return
} else if !a.HasID() {
AbortAlbumNotFound(c)
return
}
removed := a.RemovePhotos(f.Photos)
@ -449,6 +462,7 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup) {
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
// Update album YAML backup.
SaveAlbumAsYaml(a)
}

View file

@ -555,13 +555,13 @@ func (c *Config) ClientUser(withSettings bool) ClientConfig {
Table("albums").
Select("SUM(album_type = ?) AS albums, SUM(album_type = ?) AS moments, SUM(album_type = ?) AS months, SUM(album_type = ?) AS states, SUM(album_type = ?) AS folders, "+
"SUM(album_type = ? AND album_private = 1) AS private_albums, SUM(album_type = ? AND album_private = 1) AS private_moments, SUM(album_type = ? AND album_private = 1) AS private_months, SUM(album_type = ? AND album_private = 1) AS private_states, SUM(album_type = ? AND album_private = 1) AS private_folders",
entity.AlbumDefault, entity.AlbumMoment, entity.AlbumMonth, entity.AlbumState, entity.AlbumFolder, entity.AlbumDefault, entity.AlbumMoment, entity.AlbumMonth, entity.AlbumState, entity.AlbumFolder).
entity.AlbumManual, entity.AlbumMoment, entity.AlbumMonth, entity.AlbumState, entity.AlbumFolder, entity.AlbumManual, entity.AlbumMoment, entity.AlbumMonth, entity.AlbumState, entity.AlbumFolder).
Where("deleted_at IS NULL AND (albums.album_type <> 'folder' OR albums.album_path IN (SELECT photos.photo_path FROM photos WHERE photos.photo_private = 0 AND photos.deleted_at IS NULL))").
Take(&cfg.Count)
} else {
c.Db().
Table("albums").
Select("SUM(album_type = ?) AS albums, SUM(album_type = ?) AS moments, SUM(album_type = ?) AS months, SUM(album_type = ?) AS states, SUM(album_type = ?) AS folders", entity.AlbumDefault, entity.AlbumMoment, entity.AlbumMonth, entity.AlbumState, entity.AlbumFolder).
Select("SUM(album_type = ?) AS albums, SUM(album_type = ?) AS moments, SUM(album_type = ?) AS months, SUM(album_type = ?) AS states, SUM(album_type = ?) AS folders", entity.AlbumManual, entity.AlbumMoment, entity.AlbumMonth, entity.AlbumState, entity.AlbumFolder).
Where("deleted_at IS NULL AND (albums.album_type <> 'folder' OR albums.album_path IN (SELECT photos.photo_path FROM photos WHERE photos.deleted_at IS NULL))").
Take(&cfg.Count)
}

View file

@ -21,7 +21,7 @@ import (
const (
AlbumUID = byte('a')
AlbumDefault = "album"
AlbumManual = "album"
AlbumFolder = "folder"
AlbumMoment = "moment"
AlbumMonth = "month"
@ -108,7 +108,7 @@ func AddPhotoToUserAlbums(photoUid string, albums []string, userUid string) (err
if rnd.IsUID(album, AlbumUID) {
albumUid = album
} else {
a := NewUserAlbum(album, AlbumDefault, userUid)
a := NewUserAlbum(album, AlbumManual, userUid)
if found := a.Find(); found != nil {
albumUid = found.AlbumUID
@ -142,7 +142,7 @@ func NewUserAlbum(albumTitle, albumType, userUid string) *Album {
// Set default type.
if albumType == "" {
albumType = AlbumDefault
albumType = AlbumManual
}
// Set default values.
@ -237,7 +237,7 @@ func NewMonthAlbum(albumTitle, albumSlug string, year, month int) *Album {
albumTitle = strings.TrimSpace(albumTitle)
albumSlug = strings.TrimSpace(albumSlug)
if albumTitle == "" || albumSlug == "" || year == 0 || month == 0 {
if albumTitle == "" || albumSlug == "" || year < 1 || month < 1 || month > 12 {
return nil
}
@ -269,6 +269,10 @@ func NewMonthAlbum(albumTitle, albumSlug string, year, month int) *Album {
func FindMonthAlbum(year, month int) *Album {
m := Album{}
if year < 1 || month < 1 || month > 12 {
return nil
}
if UnscopedDb().First(&m, "album_year = ? AND album_month = ? AND album_type = ?", year, month, AlbumMonth).RecordNotFound() {
return nil
}
@ -280,6 +284,10 @@ func FindMonthAlbum(year, month int) *Album {
func FindAlbumBySlug(albumSlug, albumType string) *Album {
m := Album{}
if albumSlug == "" {
return nil
}
if UnscopedDb().First(&m, "album_slug = ? AND album_type = ?", albumSlug, albumType).RecordNotFound() {
return nil
}
@ -291,13 +299,21 @@ func FindAlbumBySlug(albumSlug, albumType string) *Album {
func FindAlbumByAttr(slugs, filters []string, albumType string) *Album {
m := Album{}
if len(slugs) == 0 && len(filters) == 0 {
return nil
}
stmt := UnscopedDb()
if albumType != "" {
stmt = stmt.Where("album_type = ?", albumType)
}
if len(filters) == 0 {
stmt = stmt.Where("album_slug IN (?)", slugs)
} else {
stmt = stmt.Where("album_slug IN (?) OR album_filter IN (?)", slugs, filters)
}
if stmt.First(&m).RecordNotFound() {
return nil
@ -350,8 +366,12 @@ func FindAlbum(find Album) *Album {
stmt := UnscopedDb().Where("album_type = ?", find.AlbumType)
// Search by slug and filter or title.
if find.AlbumType != AlbumDefault && find.AlbumFilter != "" {
if find.AlbumType != AlbumManual {
if find.AlbumFilter != "" {
stmt = stmt.Where("album_slug = ? OR album_filter = ?", find.AlbumSlug, find.AlbumFilter)
} else {
stmt = stmt.Where("album_slug = ?", find.AlbumSlug)
}
} else {
stmt = stmt.Where("album_slug = ? OR album_title LIKE ?", find.AlbumSlug, find.AlbumTitle)
}
@ -374,6 +394,11 @@ func FindAlbum(find Album) *Album {
return &m
}
// HasID tests if the album has a valid id and uid.
func (m *Album) HasID() bool {
return m.ID > 0 && rnd.IsUID(m.AlbumUID, AlbumUID)
}
// Find retrieves the matching record from the database and updates the entity.
func (m *Album) Find() *Album {
return FindAlbum(*m)
@ -419,7 +444,7 @@ func (m *Album) IsState() bool {
// IsDefault tests if the album is a regular album.
func (m *Album) IsDefault() bool {
return m.AlbumType == AlbumDefault
return m.AlbumType == AlbumManual
}
// SetTitle changes the album name.
@ -434,7 +459,7 @@ func (m *Album) SetTitle(title string) *Album {
m.AlbumTitle = title
if m.AlbumType == AlbumDefault || m.AlbumSlug == "" {
if m.AlbumType == AlbumManual || m.AlbumSlug == "" {
if len(m.AlbumTitle) < txt.ClipSlug {
m.AlbumSlug = txt.Slug(m.AlbumTitle)
} else {
@ -581,8 +606,8 @@ func (m *Album) UpdateFolder(albumPath, albumFilter string) error {
albumPath = strings.Trim(albumPath, string(os.PathSeparator))
albumSlug := txt.Slug(albumPath)
if albumSlug == "" {
return nil
if albumSlug == "" || albumPath == "" || albumFilter == "" || !m.HasID() {
return fmt.Errorf("folder album must have a path and filter")
}
if err := UnscopedDb().Model(m).Updates(map[string]interface{}{
@ -591,7 +616,7 @@ func (m *Album) UpdateFolder(albumPath, albumFilter string) error {
"AlbumSlug": albumSlug,
}).Error; err != nil {
return err
} else if err = UnscopedDb().Exec("UPDATE albums SET album_path = NULL WHERE album_path = ? AND id <> ?", albumPath, m.ID).Error; err != nil {
} else if err = UnscopedDb().Exec("UPDATE albums SET album_path = NULL WHERE album_type = ? AND album_path = ? AND id <> ?", AlbumFolder, albumPath, m.ID).Error; err != nil {
return err
}
@ -625,7 +650,7 @@ func (m *Album) PublishCountChange(n int) {
data := event.Data{"count": n}
switch m.AlbumType {
case AlbumDefault:
case AlbumManual:
event.Publish("count.albums", data)
case AlbumMoment:
event.Publish("count.moments", data)

View file

@ -11,7 +11,7 @@ func (m AlbumMap) Get(name string) Album {
return result
}
return *NewAlbum(name, AlbumDefault)
return *NewAlbum(name, AlbumManual)
}
func (m AlbumMap) Pointer(name string) *Album {
@ -19,7 +19,7 @@ func (m AlbumMap) Pointer(name string) *Album {
return &result
}
return NewAlbum(name, AlbumDefault)
return NewAlbum(name, AlbumManual)
}
var AlbumFixtures = AlbumMap{
@ -28,7 +28,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at9lxuqxpogaaba7",
AlbumSlug: "christmas-2030",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Christmas 2030",
AlbumLocation: "",
AlbumCategory: "",
@ -53,7 +53,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at9lxuqxpogaaba8",
AlbumSlug: "holiday-2030",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Holiday 2030",
AlbumLocation: "",
AlbumCategory: "",
@ -78,7 +78,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at9lxuqxpogaaba9",
AlbumSlug: "berlin-2019",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Berlin 2019",
AlbumLocation: "Berlin",
AlbumCategory: "City",
@ -128,7 +128,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at6axuzitogaaiax",
AlbumSlug: "import",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Import Album",
AlbumLocation: "",
AlbumCategory: "",
@ -298,7 +298,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab19",
AlbumSlug: "&ilikefood",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "&IlikeFood",
AlbumLocation: "",
AlbumCategory: "",
@ -323,7 +323,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab20",
AlbumSlug: "i-love-%-dog",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "I love % dog",
AlbumLocation: "",
AlbumCategory: "",
@ -348,7 +348,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab21",
AlbumSlug: "%gold",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "%gold",
AlbumLocation: "",
AlbumCategory: "",
@ -373,7 +373,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab22",
AlbumSlug: "sale%",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "sale%",
AlbumLocation: "",
AlbumCategory: "",
@ -398,7 +398,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab23",
AlbumSlug: "pest&dogs",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Pets & Dogs",
AlbumLocation: "",
AlbumCategory: "",
@ -423,7 +423,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab24",
AlbumSlug: "light&",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Light&",
AlbumLocation: "",
AlbumCategory: "",
@ -448,7 +448,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab25",
AlbumSlug: "'family",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "'Family",
AlbumLocation: "",
AlbumCategory: "",
@ -473,7 +473,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab26",
AlbumSlug: "father's-day",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Father's Day",
AlbumLocation: "",
AlbumCategory: "",
@ -498,7 +498,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab27",
AlbumSlug: "ice-cream'",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Ice Cream'",
AlbumLocation: "",
AlbumCategory: "",
@ -523,7 +523,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab28",
AlbumSlug: "*forrest",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "*Forrest",
AlbumLocation: "",
AlbumCategory: "",
@ -548,7 +548,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab29",
AlbumSlug: "my*kids",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "My*Kids",
AlbumLocation: "",
AlbumCategory: "",
@ -573,7 +573,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab30",
AlbumSlug: "yoga***",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Yoga***",
AlbumLocation: "",
AlbumCategory: "",
@ -598,7 +598,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab31",
AlbumSlug: "|banana",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "|Banana",
AlbumLocation: "",
AlbumCategory: "",
@ -623,7 +623,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab33",
AlbumSlug: "blue|",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Blue|",
AlbumLocation: "",
AlbumCategory: "",
@ -648,7 +648,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab34",
AlbumSlug: "345-shirt",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "345 Shirt",
AlbumLocation: "",
AlbumCategory: "",
@ -673,7 +673,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab35",
AlbumSlug: "color-555-blue",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Color555 Blue",
AlbumLocation: "",
AlbumCategory: "",
@ -698,7 +698,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab36",
AlbumSlug: "route-66",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Route 66",
AlbumLocation: "",
AlbumCategory: "",
@ -723,7 +723,7 @@ var AlbumFixtures = AlbumMap{
AlbumUID: "at1lxuqipotaab32",
AlbumSlug: "red|green",
AlbumPath: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Red|Green",
AlbumLocation: "",
AlbumCategory: "",

View file

@ -13,12 +13,12 @@ import (
func TestNewAlbum(t *testing.T) {
t.Run("name Christmas 2018", func(t *testing.T) {
album := NewAlbum("Christmas 2018", AlbumDefault)
album := NewAlbum("Christmas 2018", AlbumManual)
assert.Equal(t, "Christmas 2018", album.AlbumTitle)
assert.Equal(t, "christmas-2018", album.AlbumSlug)
})
t.Run("name empty", func(t *testing.T) {
album := NewAlbum("", AlbumDefault)
album := NewAlbum("", AlbumManual)
defaultName := time.Now().Format("January 2006")
defaultSlug := txt.Slug(defaultName)
@ -35,7 +35,7 @@ func TestNewAlbum(t *testing.T) {
func TestAlbum_SetName(t *testing.T) {
t.Run("valid name", func(t *testing.T) {
album := NewAlbum("initial name", AlbumDefault)
album := NewAlbum("initial name", AlbumManual)
assert.Equal(t, "initial name", album.AlbumTitle)
assert.Equal(t, "initial-name", album.AlbumSlug)
album.SetTitle("New Album \"Name\"")
@ -43,7 +43,7 @@ func TestAlbum_SetName(t *testing.T) {
assert.Equal(t, "new-album-name", album.AlbumSlug)
})
t.Run("empty name", func(t *testing.T) {
album := NewAlbum("initial name", AlbumDefault)
album := NewAlbum("initial name", AlbumManual)
assert.Equal(t, "initial name", album.AlbumTitle)
assert.Equal(t, "initial-name", album.AlbumSlug)
@ -63,7 +63,7 @@ The discrepancy of 1 second meridian arc length between equator and pole is abou
is an oblate spheroid.`
expected := txt.Shorten(longName, txt.ClipDefault, txt.Ellipsis)
slugExpected := txt.Clip(longName, txt.ClipSlug)
album := NewAlbum(longName, AlbumDefault)
album := NewAlbum(longName, AlbumManual)
assert.Equal(t, expected, album.AlbumTitle)
assert.Contains(t, album.AlbumSlug, txt.Slug(slugExpected))
})
@ -128,7 +128,7 @@ func TestAlbum_UpdateState(t *testing.T) {
func TestAlbum_SaveForm(t *testing.T) {
t.Run("success", func(t *testing.T) {
album := NewAlbum("Old Name", AlbumDefault)
album := NewAlbum("Old Name", AlbumManual)
assert.Equal(t, "Old Name", album.AlbumTitle)
assert.Equal(t, "old-name", album.AlbumSlug)
@ -284,7 +284,7 @@ func TestNewMonthAlbum(t *testing.T) {
func TestFindAlbumBySlug(t *testing.T) {
t.Run("1 result", func(t *testing.T) {
result := FindAlbumBySlug("holiday-2030", AlbumDefault)
result := FindAlbumBySlug("holiday-2030", AlbumManual)
if result == nil {
t.Fatal("album should not be nil")
@ -317,7 +317,7 @@ func TestAlbum_String(t *testing.T) {
album := Album{
AlbumUID: "abc123",
AlbumSlug: "test-slug",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Test Title",
}
assert.Equal(t, "test-slug", album.String())
@ -326,7 +326,7 @@ func TestAlbum_String(t *testing.T) {
album := Album{
AlbumUID: "abc123",
AlbumSlug: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Test Title",
}
assert.Contains(t, album.String(), "Test Title")
@ -335,7 +335,7 @@ func TestAlbum_String(t *testing.T) {
album := Album{
AlbumUID: "abc123",
AlbumSlug: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "",
}
assert.Equal(t, "abc123", album.String())
@ -344,7 +344,7 @@ func TestAlbum_String(t *testing.T) {
album := Album{
AlbumUID: "",
AlbumSlug: "",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "",
}
assert.Equal(t, "[unknown album]", album.String())
@ -356,7 +356,7 @@ func TestAlbum_IsMoment(t *testing.T) {
album := Album{
AlbumUID: "abc123",
AlbumSlug: "test-slug",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Test Title",
}
assert.False(t, album.IsMoment())
@ -377,7 +377,7 @@ func TestAlbum_Update(t *testing.T) {
album := Album{
AlbumUID: "abc123",
AlbumSlug: "test-slug",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Test Title",
}
assert.Equal(t, "test-slug", album.AlbumSlug)
@ -413,7 +413,7 @@ func TestAlbum_Save(t *testing.T) {
func TestAlbum_Create(t *testing.T) {
t.Run("album", func(t *testing.T) {
album := Album{
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
}
err := album.Create()
@ -462,7 +462,7 @@ func TestAlbum_Title(t *testing.T) {
album := Album{
AlbumUID: "abc123",
AlbumSlug: "test-slug",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Test Title",
}
assert.Equal(t, "Test Title", album.Title())
@ -482,7 +482,7 @@ func TestAlbum_AddPhotos(t *testing.T) {
album := Album{
AlbumUID: "abc123",
AlbumSlug: "test-slug",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Test Title",
}
added := album.AddPhotos([]string{"ab", "cd"})
@ -495,7 +495,7 @@ func TestAlbum_RemovePhotos(t *testing.T) {
album := Album{
AlbumUID: "abc123",
AlbumSlug: "test-slug",
AlbumType: AlbumDefault,
AlbumType: AlbumManual,
AlbumTitle: "Test Title",
}
removed := album.RemovePhotos([]string{"ab", "cd"})
@ -528,8 +528,8 @@ func TestAlbum_Find(t *testing.T) {
}
func TestAlbum_UpdateFolder(t *testing.T) {
t.Run("success", func(t *testing.T) {
a := Album{AlbumUID: "at6axuzitogaaxxx"}
t.Run("Success", func(t *testing.T) {
a := Album{ID: 99999, AlbumUID: "at6axuzitogaaxxx"}
assert.Empty(t, a.AlbumPath)
assert.Empty(t, a.AlbumFilter)
if err := a.UpdateFolder("2222/07", "month:07"); err != nil {
@ -538,16 +538,19 @@ func TestAlbum_UpdateFolder(t *testing.T) {
assert.Equal(t, "2222/07", a.AlbumPath)
assert.Equal(t, "month:07", a.AlbumFilter)
})
t.Run("empty path", func(t *testing.T) {
a := Album{AlbumUID: "at6axuzitogaaxxy"}
t.Run("EmptyPath", func(t *testing.T) {
a := Album{ID: 99999, AlbumUID: "at6axuzitogaaxxy"}
assert.Empty(t, a.AlbumPath)
assert.Empty(t, a.AlbumFilter)
if err := a.UpdateFolder("", "month:07"); err != nil {
t.Fatal(err)
}
err := a.UpdateFolder("", "month:07")
assert.Error(t, err)
})
t.Run("EmptyFilter", func(t *testing.T) {
a := Album{ID: 99999, AlbumUID: "at6axuzitogaaxxy"}
assert.Empty(t, a.AlbumPath)
assert.Empty(t, a.AlbumFilter)
err := a.UpdateFolder("2222/07", "")
assert.Error(t, err)
})
}

View file

@ -266,7 +266,7 @@ func (w *Moments) Start() (err error) {
}
// Make sure that the albums have been backed up before, otherwise back up all albums.
if fs.PathExists(filepath.Join(w.conf.AlbumsPath(), entity.AlbumDefault)) &&
if fs.PathExists(filepath.Join(w.conf.AlbumsPath(), entity.AlbumManual)) &&
fs.PathExists(filepath.Join(w.conf.AlbumsPath(), entity.AlbumMonth)) {
// Skip.
} else if count, err := BackupAlbums(w.conf.AlbumsPath(), false); err != nil {

View file

@ -10,6 +10,7 @@ import (
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/media"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/sortby"
)
@ -21,17 +22,31 @@ func Albums(offset, limit int) (results entity.Albums, err error) {
// AlbumByUID returns a Album based on the UID.
func AlbumByUID(albumUID string) (album entity.Album, err error) {
if rnd.InvalidUID(albumUID, entity.AlbumUID) {
return album, fmt.Errorf("invalid album uid")
}
return entity.CachedAlbumByUID(albumUID)
}
// AlbumCoverByUID returns an album cover file based on the uid.
func AlbumCoverByUID(uid string, public bool) (file entity.File, err error) {
if rnd.InvalidUID(uid, entity.AlbumUID) {
return file, fmt.Errorf("invalid album uid")
}
a := entity.Album{}
// Find album.
if a, err = AlbumByUID(uid); err != nil {
return file, err
} else if a.AlbumType != entity.AlbumDefault { // TODO: Optimize
} else if !a.HasID() {
return file, fmt.Errorf("album uid %s is invalid", clean.Log(uid))
} else if a.AlbumType != entity.AlbumManual { // TODO: Optimize
if a.AlbumFilter == "" {
return file, fmt.Errorf("smart album %s has no filter specified", a.AlbumUID)
}
f := form.SearchPhotos{Album: a.AlbumUID, Filter: a.AlbumFilter, Order: sortby.Relevance, Count: 1, Offset: 0, Merged: false}
if err = f.ParseQueryString(); err != nil {
@ -71,8 +86,8 @@ func AlbumCoverByUID(uid string, public bool) (file entity.File, err error) {
// Build query.
stmt := Db().Where("files.file_primary = 1 AND files.file_missing = 0 AND files.file_type IN (?) AND files.deleted_at IS NULL", media.PreviewExpr).
Joins("JOIN albums ON albums.album_uid = ?", uid).
Joins("JOIN photos_albums pa ON pa.album_uid = albums.album_uid AND pa.photo_uid = files.photo_uid AND pa.hidden = 0").
Joins("JOIN albums a ON a.album_uid = ?", uid).
Joins("JOIN photos_albums pa ON pa.album_uid = a.album_uid AND pa.photo_uid = files.photo_uid AND pa.hidden = 0 AND pa.missing = 0").
Joins("JOIN photos ON photos.id = files.photo_id AND photos.deleted_at IS NULL")
// Public pictures only?
@ -81,7 +96,7 @@ func AlbumCoverByUID(uid string, public bool) (file entity.File, err error) {
}
// Find first picture.
if err := stmt.Order("photos.photo_quality DESC, photos.taken_at DESC").
if err = stmt.Order("photos.photo_quality DESC, photos.taken_at DESC").
First(&file).Error; err != nil {
return file, err
}
@ -96,11 +111,11 @@ func UpdateAlbumDates() error {
switch DbDialect() {
case MySQL:
return UnscopedDb().Exec(`UPDATE albums
INNER JOIN
(SELECT photo_path, MAX(taken_at_local) AS taken_max
return UnscopedDb().Exec(`UPDATE albums INNER JOIN (
SELECT photo_path, MAX(taken_at_local) AS taken_max
FROM photos WHERE taken_src = 'meta' AND photos.photo_quality >= 3 AND photos.deleted_at IS NULL
GROUP BY photo_path) AS p ON albums.album_path = p.photo_path
GROUP BY photo_path
) AS p ON albums.album_path = p.photo_path
SET albums.album_year = YEAR(taken_max), albums.album_month = MONTH(taken_max), albums.album_day = DAY(taken_max)
WHERE albums.album_type = 'folder' AND albums.album_path IS NOT NULL AND p.taken_max IS NOT NULL`).Error
default:
@ -122,6 +137,10 @@ func UpdateMissingAlbumEntries() error {
// AlbumEntryFound removes the missing flag from album entries.
func AlbumEntryFound(uid string) error {
if rnd.InvalidUID(uid, entity.PhotoUID) {
return fmt.Errorf("invalid photo uid")
}
switch DbDialect() {
default:
return UnscopedDb().Exec(`UPDATE photos_albums SET missing = 0 WHERE photo_uid = ?`, uid).Error
@ -131,6 +150,12 @@ func AlbumEntryFound(uid string) error {
// AlbumsPhotoUIDs returns up to 100000 photo UIDs that belong to the specified albums.
func AlbumsPhotoUIDs(albums []string, includeDefault, includePrivate bool) (photos []string, err error) {
for _, albumUid := range albums {
if rnd.InvalidUID(albumUid, entity.AlbumUID) {
// Should never happen.
log.Debugf("query: album uid %s is invalid", clean.Log(albumUid))
continue
}
a, err := AlbumByUID(albumUid)
if err != nil {
@ -138,7 +163,11 @@ func AlbumsPhotoUIDs(albums []string, includeDefault, includePrivate bool) (phot
continue
}
if a.IsDefault() && !includeDefault {
if a.IsDefault() && !includeDefault || !a.HasID() {
continue
} else if !a.IsDefault() && a.AlbumFilter == "" {
// Should never happen.
log.Debugf("query: smart album %s has empty filter", clean.Log(a.AlbumUID))
continue
}

View file

@ -47,7 +47,7 @@ func (c *Counts) Refresh() {
Take(c)
Db().Table("albums").
Select("SUM(album_type = ?) AS albums, SUM(album_type = ?) AS moments, SUM(album_type = ?) AS folders", entity.AlbumDefault, entity.AlbumMoment, entity.AlbumFolder).
Select("SUM(album_type = ?) AS albums, SUM(album_type = ?) AS moments, SUM(album_type = ?) AS folders", entity.AlbumManual, entity.AlbumMoment, entity.AlbumFolder).
Where("deleted_at IS NULL").
Take(c)

View file

@ -22,7 +22,7 @@ func UpdateAlbumDefaultCovers() (err error) {
var res *gorm.DB
condition := gorm.Expr("album_type = ? AND thumb_src = ?", entity.AlbumDefault, entity.SrcAuto)
condition := gorm.Expr("album_type = ? AND thumb_src = ?", entity.AlbumManual, entity.SrcAuto)
switch DbDialect() {
case MySQL:

View file

@ -187,20 +187,20 @@ type Moments []Moment
// MomentsTime counts photos by month and year.
func MomentsTime(threshold int, public bool) (results Moments, err error) {
db := UnscopedDb().Table("photos").
stmt := UnscopedDb().Table("photos").
Select("photos.photo_year AS year, photos.photo_month AS month, COUNT(*) AS photo_count").
Where("photos.photo_quality >= 3 AND deleted_at IS NULL AND photos.photo_year > 0 AND photos.photo_month > 0")
// Ignore private pictures?
if public {
db = db.Where("photo_private = 0")
stmt = stmt.Where("photo_private = 0")
}
db = db.Group("photos.photo_year, photos.photo_month").
stmt = stmt.Group("photos.photo_year, photos.photo_month").
Order("photos.photo_year DESC, photos.photo_month DESC").
Having("photo_count >= ?", threshold)
if err := db.Scan(&results).Error; err != nil {
if err = stmt.Scan(&results).Error; err != nil {
return results, err
}
@ -209,19 +209,19 @@ func MomentsTime(threshold int, public bool) (results Moments, err error) {
// MomentsCountries returns the most popular countries by year.
func MomentsCountries(threshold int, public bool) (results Moments, err error) {
db := UnscopedDb().Table("photos").
stmt := UnscopedDb().Table("photos").
Select("photo_year AS year, photo_country AS country, COUNT(*) AS photo_count").
Where("photos.photo_quality >= 3 AND deleted_at IS NULL AND photo_country <> 'zz' AND photo_year > 0")
// Ignore private pictures?
if public {
db = db.Where("photo_private = 0")
stmt = stmt.Where("photo_private = 0")
}
db = db.Group("photo_year, photo_country").
stmt = stmt.Group("photo_year, photo_country").
Having("photo_count >= ?", threshold)
if err := db.Scan(&results).Error; err != nil {
if err = stmt.Scan(&results).Error; err != nil {
return results, err
}
@ -230,20 +230,20 @@ func MomentsCountries(threshold int, public bool) (results Moments, err error) {
// MomentsStates returns the most popular states and countries by year.
func MomentsStates(threshold int, public bool) (results Moments, err error) {
db := UnscopedDb().Table("photos").
stmt := UnscopedDb().Table("photos").
Select("p.place_country AS country, p.place_state AS state, COUNT(*) AS photo_count").
Joins("JOIN places p ON p.id = photos.place_id").
Where("photos.photo_quality >= 3 AND photos.deleted_at IS NULL AND p.place_state <> '' AND p.place_country <> 'zz'")
// Ignore private pictures?
if public {
db = db.Where("photo_private = 0")
stmt = stmt.Where("photo_private = 0")
}
db = db.Group("p.place_country, p.place_state").
stmt = stmt.Group("p.place_country, p.place_state").
Having("photo_count >= ?", threshold)
if err := db.Scan(&results).Error; err != nil {
if err = stmt.Scan(&results).Error; err != nil {
return results, err
}
@ -260,7 +260,7 @@ func MomentsLabels(threshold int, public bool) (results Moments, err error) {
m := Moments{}
db := UnscopedDb().Table("photos").
stmt := UnscopedDb().Table("photos").
Select("l.label_slug AS label, COUNT(*) AS photo_count").
Joins("JOIN photos_labels pl ON pl.photo_id = photos.id AND pl.uncertainty < 100").
Joins("JOIN labels l ON l.id = pl.label_id").
@ -268,13 +268,13 @@ func MomentsLabels(threshold int, public bool) (results Moments, err error) {
// Ignore private pictures?
if public {
db = db.Where("photo_private = 0")
stmt = stmt.Where("photo_private = 0")
}
db = db.Group("l.label_slug").
stmt = stmt.Group("l.label_slug").
Having("photo_count >= ?", threshold)
if err := db.Scan(&m).Error; err != nil {
if err = stmt.Scan(&m).Error; err != nil {
return m, err
}
@ -299,16 +299,18 @@ func MomentsLabels(threshold int, public bool) (results Moments, err error) {
// RemoveDuplicateMoments deletes generated albums with duplicate slug or filter.
func RemoveDuplicateMoments() (removed int, err error) {
if res := UnscopedDb().Exec(`DELETE FROM links WHERE share_uid
IN (SELECT a.album_uid FROM albums a JOIN albums b ON a.album_type = b.album_type
AND a.album_type <> ? AND a.id > b.id WHERE (a.album_slug = b.album_slug
OR a.album_filter = b.album_filter) GROUP BY a.album_uid)`, entity.AlbumDefault); res.Error != nil {
IN (SELECT a.album_uid FROM albums a JOIN albums b ON a.album_type <> ?
AND a.album_type = b.album_type AND a.id > b.id
WHERE (a.album_slug = b.album_slug OR a.album_filter = b.album_filter)
GROUP BY a.album_uid)`, entity.AlbumManual); res.Error != nil {
return removed, res.Error
}
if res := UnscopedDb().Exec(`DELETE FROM albums WHERE id
IN (SELECT a.id FROM albums a JOIN albums b ON a.album_type = b.album_type
AND a.album_type <> ? AND a.id > b.id WHERE (a.album_slug = b.album_slug
OR a.album_filter = b.album_filter) GROUP BY a.album_uid)`, entity.AlbumDefault); res.Error != nil {
IN (SELECT a.id FROM albums a JOIN albums b ON a.album_type <> ?
AND a.album_type = b.album_type AND a.id > b.id
WHERE (a.album_slug = b.album_slug OR a.album_filter = b.album_filter)
GROUP BY a.album_uid)`, entity.AlbumManual); res.Error != nil {
return removed, res.Error
} else if res.RowsAffected > 0 {
removed = int(res.RowsAffected)

View file

@ -44,7 +44,7 @@ func UserAlbums(f form.SearchAlbums, sess *entity.Session) (results AlbumResults
// Determine resource to check.
var aclResource acl.Resource
switch f.Type {
case entity.AlbumDefault:
case entity.AlbumManual:
aclResource = acl.ResourceAlbums
case entity.AlbumFolder:
aclResource = acl.ResourceFolders
@ -85,7 +85,7 @@ func UserAlbums(f form.SearchAlbums, sess *entity.Session) (results AlbumResults
case sortby.Count:
s = s.Order("photo_count DESC, albums.album_title, albums.album_uid DESC")
case sortby.Moment, sortby.Newest:
if f.Type == entity.AlbumDefault || f.Type == entity.AlbumState {
if f.Type == entity.AlbumManual || f.Type == entity.AlbumState {
s = s.Order("albums.album_uid DESC")
} else if f.Type == entity.AlbumMoment {
s = s.Order("has_year, albums.album_year DESC, albums.album_month DESC, albums.album_day DESC, albums.album_title, albums.album_uid DESC")
@ -93,7 +93,7 @@ func UserAlbums(f form.SearchAlbums, sess *entity.Session) (results AlbumResults
s = s.Order("albums.album_year DESC, albums.album_month DESC, albums.album_day DESC, albums.album_title, albums.album_uid DESC")
}
case sortby.Oldest:
if f.Type == entity.AlbumDefault || f.Type == entity.AlbumState {
if f.Type == entity.AlbumManual || f.Type == entity.AlbumState {
s = s.Order("albums.album_uid ASC")
} else if f.Type == entity.AlbumMoment {
s = s.Order("has_year, albums.album_year ASC, albums.album_month ASC, albums.album_day ASC, albums.album_title, albums.album_uid ASC")
@ -190,7 +190,7 @@ func UserAlbums(f form.SearchAlbums, sess *entity.Session) (results AlbumResults
if txt.NotEmpty(f.Year) {
// Filter by the pictures included if it is a manually managed album, as these do not have an explicit
// year assigned to them, unlike calendar albums and moments for example.
if f.Type == entity.AlbumDefault {
if f.Type == entity.AlbumManual {
s = s.Where("? OR albums.album_uid IN (SELECT DISTINCT pay.album_uid FROM photos_albums pay "+
"JOIN photos py ON pay.photo_uid = py.photo_uid WHERE py.photo_year IN (?) AND pay.hidden = 0 AND pay.missing = 0)",
gorm.Expr(AnyInt("albums.album_year", f.Year, txt.Or, entity.UnknownYear, txt.YearMax)), strings.Split(f.Year, txt.Or))

View file

@ -156,7 +156,7 @@ func TestAlbums(t *testing.T) {
})
t.Run("SearchAlbumForYear", func(t *testing.T) {
f := form.SearchAlbums{
Type: entity.AlbumDefault,
Type: entity.AlbumManual,
Year: "2018",
Month: "",
Day: "",