diff --git a/cmd/nonshy/main.go b/cmd/nonshy/main.go
index 21f99b1..1a58de0 100644
--- a/cmd/nonshy/main.go
+++ b/cmd/nonshy/main.go
@@ -242,6 +242,20 @@ func main() {
},
},
},
+ {
+ Name: "vacuum",
+ Usage: "Run database maintenance tasks (clean up broken links, remove orphaned comment photos, etc.) for data consistency.",
+ Flags: []cli.Flag{
+ &cli.BoolFlag{
+ Name: "dryrun",
+ Usage: "don't actually delete anything",
+ },
+ },
+ Action: func(c *cli.Context) error {
+ initdb(c)
+ return worker.Vacuum(c.Bool("dryrun"))
+ },
+ },
},
}
diff --git a/pkg/controller/api/orphaned_comment_photos.go b/pkg/controller/api/orphaned_comment_photos.go
index c52fbb3..c0547da 100644
--- a/pkg/controller/api/orphaned_comment_photos.go
+++ b/pkg/controller/api/orphaned_comment_photos.go
@@ -1,12 +1,10 @@
package api
import (
- "fmt"
"net/http"
"code.nonshy.com/nonshy/website/pkg/config"
- "code.nonshy.com/nonshy/website/pkg/models"
- "code.nonshy.com/nonshy/website/pkg/photo"
+ "code.nonshy.com/nonshy/website/pkg/worker"
)
// RemoveOrphanedCommentPhotos API.
@@ -24,7 +22,7 @@ func RemoveOrphanedCommentPhotos() http.HandlerFunc {
OK bool `json:"OK"`
Error string `json:"error,omitempty"`
Total int64 `json:"total"`
- Removed int `json:"removed"`
+ Removed int64 `json:"removed"`
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -56,38 +54,19 @@ func RemoveOrphanedCommentPhotos() http.HandlerFunc {
}
// Do the needful.
- photos, total, err := models.GetOrphanedCommentPhotos()
+ total, err := worker.VacuumOrphanedCommentPhotos(false)
if err != nil {
SendJSON(w, http.StatusInternalServerError, Response{
- OK: false,
- Error: fmt.Sprintf("GetOrphanedCommentPhotos: %s", err),
+ Error: err.Error(),
})
return
}
- for _, row := range photos {
- if err := photo.Delete(row.Filename); err != nil {
- SendJSON(w, http.StatusInternalServerError, Response{
- OK: false,
- Error: fmt.Sprintf("Photo ID %d: removing file %s: %s", row.ID, row.Filename, err),
- })
- return
- }
-
- if err := row.Delete(); err != nil {
- SendJSON(w, http.StatusInternalServerError, Response{
- OK: false,
- Error: fmt.Sprintf("DeleteOrphanedCommentPhotos(%d): %s", row.ID, err),
- })
- return
- }
- }
-
// Send success response.
SendJSON(w, http.StatusOK, Response{
OK: true,
Total: total,
- Removed: len(photos),
+ Removed: total,
})
})
}
diff --git a/pkg/controller/photo/site_gallery.go b/pkg/controller/photo/site_gallery.go
index 39a4963..d5a7b3f 100644
--- a/pkg/controller/photo/site_gallery.go
+++ b/pkg/controller/photo/site_gallery.go
@@ -64,13 +64,18 @@ func SiteGallery() http.HandlerFunc {
// They didn't post a "Whose photos" filter, restore it from their last saved default.
who = currentUser.GetProfileField("site_gallery_default")
}
- if who != "friends" && who != "everybody" && who != "friends+private" && who != "likes" {
+ if who != "friends" && who != "everybody" && who != "friends+private" && who != "likes" && who != "uncertified" {
// Default Who setting should be Friends-only, unless you have no friends.
if myFriendCount > 0 {
who = "friends"
} else {
who = "everybody"
}
+
+ // Admin only who option.
+ if who == "uncertified" && !currentUser.HasAdminScope(config.ScopePhotoModerator) {
+ who = "friends"
+ }
}
// Store their "Whose photos" filter on their page to default it for next time.
@@ -95,6 +100,7 @@ func SiteGallery() http.HandlerFunc {
FriendsOnly: who == "friends",
IsShy: isShy || who == "friends+private",
MyLikes: who == "likes",
+ Uncertified: who == "uncertified",
}, pager)
// Bulk load the users associated with these photos.
diff --git a/pkg/models/photo.go b/pkg/models/photo.go
index e29aa32..e99189f 100644
--- a/pkg/models/photo.go
+++ b/pkg/models/photo.go
@@ -174,6 +174,28 @@ func CountPhotos(userID uint64) int64 {
return count
}
+// GetOrphanedPhotos gets all photos having no user ID associated.
+func GetOrphanedPhotos() ([]*Photo, int64, error) {
+ var (
+ count int64
+ ps = []*Photo{}
+ )
+
+ query := DB.Model(&Photo{}).Where(`
+ NOT EXISTS (
+ SELECT 1 FROM users WHERE users.id = photos.user_id
+ )
+ OR photos.user_id = 0
+ `)
+ query.Count(&count)
+ res := query.Find(&ps)
+ if res.Error != nil {
+ return nil, 0, res.Error
+ }
+
+ return ps, count, res.Error
+}
+
/*
IsSiteGalleryThrottled returns whether the user is throttled from marking additional pictures for the Site Gallery.
@@ -477,6 +499,7 @@ type Gallery struct {
IsShy bool // Current user is like a Shy Account (or: show self/friends and private photo grants only)
FriendsOnly bool // Only show self/friends instead of everybody's pics
MyLikes bool // Filter to photos I have liked
+ Uncertified bool // Filter for non-certified members only
}
/*
@@ -627,9 +650,15 @@ func PaginateGalleryPhotos(user *User, conf Gallery, pager *Pagination) ([]*Phot
}
// Only certified (and not banned) user photos.
- wheres = append(wheres,
- "EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND certified = true AND status='active')",
- )
+ if conf.Uncertified {
+ wheres = append(wheres,
+ "EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND certified IS NOT true AND status='active')",
+ )
+ } else {
+ wheres = append(wheres,
+ "EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND certified = true AND status='active')",
+ )
+ }
// Exclude private users' photos.
wheres = append(wheres,
@@ -647,6 +676,11 @@ func PaginateGalleryPhotos(user *User, conf Gallery, pager *Pagination) ([]*Phot
if filterExplicit != "" {
query = query.Where("explicit = ?", filterExplicit == "true")
}
+ if conf.Uncertified {
+ query = query.Where(
+ "EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND certified IS NOT true AND status='active')",
+ )
+ }
} else {
query = DB.Where(
strings.Join(wheres, " AND "),
diff --git a/pkg/worker/vacuum.go b/pkg/worker/vacuum.go
new file mode 100644
index 0000000..3bc8a7e
--- /dev/null
+++ b/pkg/worker/vacuum.go
@@ -0,0 +1,84 @@
+package worker
+
+import (
+ "fmt"
+
+ "code.nonshy.com/nonshy/website/pkg/log"
+ "code.nonshy.com/nonshy/website/pkg/models"
+ "code.nonshy.com/nonshy/website/pkg/photo"
+)
+
+// Vacuum runs database cleanup tasks for data consistency. Run it like `nonshy vacuum` from the CLI.
+func Vacuum(dryrun bool) error {
+ log.Warn("Vacuum: Orphaned Comment Photos")
+ if total, err := VacuumOrphanedCommentPhotos(dryrun); err != nil {
+ log.Error("Orphaned Comment Photos: %s", err)
+ } else {
+ log.Info("Removed %d photo(s)", total)
+ }
+
+ log.Warn("Vacuum: Orphaned Gallery Photos")
+ if total, err := VacuumOrphanedPhotos(dryrun); err != nil {
+ log.Error("Orphaned Gallery Photos: %s", err)
+ } else {
+ log.Info("Removed %d photo(s)", total)
+ }
+
+ return nil
+}
+
+// VacuumOrphanedPhotos removes any lingering photo from failed account deletion.
+func VacuumOrphanedPhotos(dryrun bool) (int64, error) {
+ photos, count, err := models.GetOrphanedPhotos()
+ if err != nil {
+ return count, err
+ }
+
+ if dryrun {
+ return count, nil
+ }
+
+ for _, row := range photos {
+ log.Info(" #%d: %s", row.ID, row.Filename)
+ if err := photo.Delete(row.Filename); err != nil {
+ return count, fmt.Errorf("photo ID %d: removing file %s: %s", row.ID, row.Filename, err)
+ }
+
+ if row.CroppedFilename != "" {
+ if err := photo.Delete(row.CroppedFilename); err != nil {
+ return count, fmt.Errorf("photo ID %d: removing file %s: %s", row.ID, row.Filename, err)
+ }
+ }
+
+ if err := row.Delete(); err != nil {
+ return count, fmt.Errorf("deleting orphaned photo (%d): %s", row.ID, err)
+ }
+ }
+
+ return count, nil
+}
+
+// VacuumOrphanedCommentPhotos cleans up comment photos that weren't associated to a post, returning the count removed.
+func VacuumOrphanedCommentPhotos(dryrun bool) (int64, error) {
+ // Do the needful.
+ photos, total, err := models.GetOrphanedCommentPhotos()
+ if err != nil {
+ return total, err
+ }
+
+ if dryrun {
+ return total, nil
+ }
+
+ for _, row := range photos {
+ if err := photo.Delete(row.Filename); err != nil {
+ return total, fmt.Errorf("photo ID %d: removing file %s: %s", row.ID, row.Filename, err)
+ }
+
+ if err := row.Delete(); err != nil {
+ return total, fmt.Errorf("deleting orphaned comment photo (%d): %s", row.ID, err)
+ }
+ }
+
+ return total, nil
+}
diff --git a/web/templates/photo/gallery.html b/web/templates/photo/gallery.html
index c7d1733..35353ed 100644
--- a/web/templates/photo/gallery.html
+++ b/web/templates/photo/gallery.html
@@ -378,6 +378,9 @@
+ {{if .CurrentUser.HasAdminScope "social.moderator.photo"}}
+
+ {{end}}