diff --git a/frontend/src/component/photo/clipboard.vue b/frontend/src/component/photo/clipboard.vue
index 1cdfed2d4..287b56188 100644
--- a/frontend/src/component/photo/clipboard.vue
+++ b/frontend/src/component/photo/clipboard.vue
@@ -163,6 +163,10 @@ import Photo from "model/photo";
export default {
name: 'PPhotoClipboard',
props: {
+ context: {
+ type: String,
+ default: 'photos',
+ },
selection: {
type: Array,
default: () => [],
@@ -175,10 +179,6 @@ export default {
type: Object,
default: () => {},
},
- context: {
- type: String,
- default: '',
- },
},
data() {
const features = this.$config.settings().features;
diff --git a/frontend/src/component/photo/toolbar.vue b/frontend/src/component/photo/toolbar.vue
index e54c9b0db..3a3f767b1 100644
--- a/frontend/src/component/photo/toolbar.vue
+++ b/frontend/src/component/photo/toolbar.vue
@@ -32,7 +32,11 @@
view_column
-
+ delete
+
+
cloud_upload
@@ -166,15 +170,23 @@
+
diff --git a/frontend/src/page/photos.vue b/frontend/src/page/photos.vue
index 74919981a..60dffebaa 100644
--- a/frontend/src/page/photos.vue
+++ b/frontend/src/page/photos.vue
@@ -3,7 +3,7 @@
:infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="scrollDistance"
:infinite-scroll-listen-for-event="'scrollRefresh'">
-
@@ -12,7 +12,7 @@
-
+
0 {
+ log.Infof("archive: deleting %s", english.Plural(len(photos), "photo", "photos"))
+ } else {
+ Abort(c, http.StatusBadRequest, i18n.ErrNoItemsSelected)
return
}
@@ -398,8 +416,8 @@ func BatchPhotosDelete(router *gin.RouterGroup) {
}
}
- if numFiles > 0 {
- log.Infof("delete: removed %s [%s]", english.Plural(numFiles, "file", "files"), time.Since(deleteStart))
+ if numFiles > 0 || len(deleted) > 0 {
+ log.Infof("archive: deleted %s and %s [%s]", english.Plural(numFiles, "file", "files"), english.Plural(len(deleted), "photo", "photos"), time.Since(deleteStart))
}
// Any photos deleted?
diff --git a/internal/form/selection.go b/internal/form/selection.go
index 92ef810ba..360b31672 100644
--- a/internal/form/selection.go
+++ b/internal/form/selection.go
@@ -2,7 +2,9 @@ package form
import "strings"
+// Selection represents items selected in the user interface.
type Selection struct {
+ All bool `json:"all"`
Files []string `json:"files"`
Photos []string `json:"photos"`
Albums []string `json:"albums"`
@@ -11,6 +13,7 @@ type Selection struct {
Subjects []string `json:"subjects"`
}
+// Empty checks if any specific items were selected.
func (f Selection) Empty() bool {
switch {
case len(f.Files) > 0:
@@ -30,7 +33,8 @@ func (f Selection) Empty() bool {
return true
}
-func (f Selection) All() []string {
+// Get returns a string slice with the selected item UIDs.
+func (f Selection) Get() []string {
var all []string
copy(all, f.Files)
@@ -44,6 +48,7 @@ func (f Selection) All() []string {
return all
}
+// String returns a string containing all selected item UIDs.
func (f Selection) String() string {
- return strings.Join(f.All(), ", ")
+ return strings.Join(f.Get(), ", ")
}
diff --git a/internal/form/selection_test.go b/internal/form/selection_test.go
index 59d190505..16284b9e6 100644
--- a/internal/form/selection_test.go
+++ b/internal/form/selection_test.go
@@ -38,10 +38,10 @@ func TestSelection_Empty(t *testing.T) {
})
}
-func TestSelection_All(t *testing.T) {
+func TestSelection_Get(t *testing.T) {
t.Run("success", func(t *testing.T) {
sel := Selection{Photos: []string{"p123", "p456"}, Albums: []string{"a123"}, Labels: []string{"l123", "l456", "l789"}, Files: []string{"f567", "f111"}, Places: []string{"p568"}, Subjects: []string{"jqzkpo13j8ngpgv4"}}
- assert.Equal(t, []string{"p123", "p456", "a123", "l123", "l456", "l789", "p568", "jqzkpo13j8ngpgv4"}, sel.All())
+ assert.Equal(t, []string{"p123", "p456", "a123", "l123", "l456", "l789", "p568", "jqzkpo13j8ngpgv4"}, sel.Get())
})
}
diff --git a/internal/photoprism/purge.go b/internal/photoprism/purge.go
index 0c62c467b..ff66e31c1 100644
--- a/internal/photoprism/purge.go
+++ b/internal/photoprism/purge.go
@@ -215,7 +215,7 @@ func (w *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPhot
limit = 10000
offset = 0
for {
- photos, err := query.PhotosMissing(limit, offset)
+ photos, err := query.MissingPhotos(limit, offset)
if err != nil {
return purgedFiles, purgedPhotos, updates(), err
diff --git a/internal/query/photo.go b/internal/query/photo.go
index 3281616a3..81c9253a4 100644
--- a/internal/query/photo.go
+++ b/internal/query/photo.go
@@ -72,13 +72,26 @@ func PhotoPreloadByUID(photoUID string) (photo entity.Photo, err error) {
return photo, nil
}
-// PhotosMissing returns photo entities without existing files.
-func PhotosMissing(limit int, offset int) (entities entity.Photos, err error) {
+// MissingPhotos returns photo entities without existing files.
+func MissingPhotos(limit int, offset int) (entities entity.Photos, err error) {
err = Db().
Select("photos.*").
Where("id NOT IN (SELECT photo_id FROM files WHERE file_missing = 0 AND file_root = '/' AND deleted_at IS NULL)").
Where("photos.photo_type <> ?", entity.MediaText).
- Group("photos.id").
+ Order("photos.id").
+ Limit(limit).Offset(offset).Find(&entities).Error
+
+ return entities, err
+}
+
+// ArchivedPhotos finds and returns archived photos.
+func ArchivedPhotos(limit int, offset int) (entities entity.Photos, err error) {
+ err = UnscopedDb().
+ Select("photos.*").
+ Where("photos.photo_quality > -1").
+ Where("photos.deleted_at IS NOT NULL").
+ Where("photos.photo_type <> ?", entity.MediaText).
+ Order("photos.id").
Limit(limit).Offset(offset).Find(&entities).Error
return entities, err
diff --git a/internal/query/photo_test.go b/internal/query/photo_test.go
index dd6363f42..e7bac2ee4 100644
--- a/internal/query/photo_test.go
+++ b/internal/query/photo_test.go
@@ -58,7 +58,7 @@ func TestPreloadPhotoByUID(t *testing.T) {
}
func TestMissingPhotos(t *testing.T) {
- result, err := PhotosMissing(15, 0)
+ result, err := MissingPhotos(15, 0)
if err != nil {
t.Fatal(err)
@@ -67,6 +67,22 @@ func TestMissingPhotos(t *testing.T) {
assert.LessOrEqual(t, 1, len(result))
}
+func TestArchivedPhotos(t *testing.T) {
+ results, err := ArchivedPhotos(15, 0)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assert.Equal(t, 1, len(results))
+
+ if len(results) > 1 {
+ result := results[0]
+ assert.Equal(t, "image", result.PhotoType)
+ assert.Equal(t, "pt9jtdre2lvl0y25", result.PhotoUID)
+ }
+}
+
func TestPhotosMetadataUpdate(t *testing.T) {
interval := entity.MetadataUpdateInterval
result, err := PhotosMetadataUpdate(10, 0, time.Second, interval)