diff --git a/internal/api/albums.go b/internal/api/albums.go index d01f65511..b107673e5 100644 --- a/internal/api/albums.go +++ b/internal/api/albums.go @@ -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) } diff --git a/internal/config/client_config.go b/internal/config/client_config.go index 974fa3df8..f263c1d44 100644 --- a/internal/config/client_config.go +++ b/internal/config/client_config.go @@ -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) } diff --git a/internal/entity/album.go b/internal/entity/album.go index d99d77d57..0663550ff 100644 --- a/internal/entity/album.go +++ b/internal/entity/album.go @@ -20,12 +20,12 @@ import ( ) const ( - AlbumUID = byte('a') - AlbumDefault = "album" - AlbumFolder = "folder" - AlbumMoment = "moment" - AlbumMonth = "month" - AlbumState = "state" + AlbumUID = byte('a') + AlbumManual = "album" + AlbumFolder = "folder" + AlbumMoment = "moment" + AlbumMonth = "month" + AlbumState = "state" ) type Albums []Album @@ -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) } - stmt = stmt.Where("album_slug IN (?) OR album_filter IN (?)", slugs, filters) + 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 != "" { - stmt = stmt.Where("album_slug = ? OR album_filter = ?", find.AlbumSlug, 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) diff --git a/internal/entity/album_fixtures.go b/internal/entity/album_fixtures.go index 016fd2031..e742f024d 100644 --- a/internal/entity/album_fixtures.go +++ b/internal/entity/album_fixtures.go @@ -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: "", diff --git a/internal/entity/album_test.go b/internal/entity/album_test.go index 6709a42c9..679b20acb 100644 --- a/internal/entity/album_test.go +++ b/internal/entity/album_test.go @@ -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) }) } diff --git a/internal/photoprism/moments.go b/internal/photoprism/moments.go index 33719f939..1420fd75f 100644 --- a/internal/photoprism/moments.go +++ b/internal/photoprism/moments.go @@ -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 { diff --git a/internal/query/albums.go b/internal/query/albums.go index 66fb4b40e..cda4ab64d 100644 --- a/internal/query/albums.go +++ b/internal/query/albums.go @@ -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 } diff --git a/internal/query/counts.go b/internal/query/counts.go index cdcafbb83..c3777af82 100644 --- a/internal/query/counts.go +++ b/internal/query/counts.go @@ -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) diff --git a/internal/query/covers.go b/internal/query/covers.go index 3dd52a04a..b14ded6a8 100644 --- a/internal/query/covers.go +++ b/internal/query/covers.go @@ -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: diff --git a/internal/query/moments.go b/internal/query/moments.go index 6719cd88f..abbc36e08 100644 --- a/internal/query/moments.go +++ b/internal/query/moments.go @@ -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) diff --git a/internal/search/albums.go b/internal/search/albums.go index e9f6b3358..a466a350f 100644 --- a/internal/search/albums.go +++ b/internal/search/albums.go @@ -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)) diff --git a/internal/search/albums_test.go b/internal/search/albums_test.go index a7ac47721..c92de4561 100644 --- a/internal/search/albums_test.go +++ b/internal/search/albums_test.go @@ -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: "",