From 8fb71366f83912807d7fdcb98e3e66c2d9e22176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Tue, 28 Jul 2020 20:28:02 -0700 Subject: [PATCH] API: Delete users asynchronously Deleting large users might lock the tables in the hosted offering --- api/user.go | 5 +-- storage/user.go | 88 +++++++++++++++++++++++++++++++++++++++++++++++ ui/user_remove.go | 14 +++++--- 3 files changed, 101 insertions(+), 6 deletions(-) diff --git a/api/user.go b/api/user.go index 62b13bd3..01b32a6e 100644 --- a/api/user.go +++ b/api/user.go @@ -169,10 +169,11 @@ func (h *handler) removeUser(w http.ResponseWriter, r *http.Request) { return } - if err := h.store.RemoveUser(user.ID); err != nil { - json.BadRequest(w, r, errors.New("Unable to remove this user from the database")) + if user.ID == request.UserID(r) { + json.BadRequest(w, r, errors.New("You cannot remove yourself")) return } + h.store.RemoveUserAsync(user.ID) json.NoContent(w, r) } diff --git a/storage/user.go b/storage/user.go index 6ec116f7..9d3a8c6a 100644 --- a/storage/user.go +++ b/storage/user.go @@ -9,6 +9,7 @@ import ( "fmt" "strings" + "miniflux.app/logger" "miniflux.app/model" "github.com/lib/pq/hstore" @@ -357,6 +358,15 @@ func (s *Storage) RemoveUser(userID int64) error { return nil } +// RemoveUserAsync deletes user data without locking the database. +func (s *Storage) RemoveUserAsync(userID int64) { + go func() { + deleteUserFeeds(s.db, userID) + s.db.Exec(`DELETE FROM users WHERE id=$1`, userID) + s.db.Exec(`DELETE FROM integrations WHERE user_id=$1`, userID) + }() +} + // Users returns all users. func (s *Storage) Users() (model.Users, error) { query := ` @@ -459,3 +469,81 @@ func hashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(bytes), err } + +func deleteUserFeeds(db *sql.DB, userID int64) { + query := `SELECT id FROM feeds WHERE user_id=$1` + rows, err := db.Query(query, userID) + if err != nil { + logger.Error(`store: unable to get user feeds: %v`, err) + return + } + defer rows.Close() + + var feedIDs []int64 + for rows.Next() { + var feedID int64 + rows.Scan(&feedID) + feedIDs = append(feedIDs, feedID) + } + + worker := func(jobs <-chan int64, results chan<- bool) { + for feedID := range jobs { + deleteUserEntries(db, userID, feedID) + db.Exec(`DELETE FROM feeds WHERE id=$1`, feedID) + results <- true + } + } + + const numWorkers = 3 + numJobs := len(feedIDs) + jobs := make(chan int64, numJobs) + results := make(chan bool, numJobs) + + for w := 0; w < numWorkers; w++ { + go worker(jobs, results) + } + + for j := 0; j < numJobs; j++ { + jobs <- feedIDs[j] + } + close(jobs) + + for a := 1; a <= numJobs; a++ { + <-results + } +} + +func deleteUserEntries(db *sql.DB, userID int64, feedID int64) { + query := `SELECT id FROM entries WHERE user_id=$1 AND feed_id=$2` + rows, err := db.Query(query, userID, feedID) + if err != nil { + logger.Error(`store: unable to get user feed entries: %v`, err) + return + } + defer rows.Close() + + for rows.Next() { + var entryID int64 + rows.Scan(&entryID) + deleteUserEnclosures(db, userID, entryID) + db.Exec(`DELETE FROM entries WHERE id=$1`, entryID) + } +} + +func deleteUserEnclosures(db *sql.DB, userID int64, entryID int64) { + query := `SELECT id FROM enclosures WHERE user_id=$1 AND entry_id=$2` + rows, err := db.Query(query, userID, entryID) + if err != nil { + logger.Error(`store: unable to get user entry enclosures: %v`, err) + return + } + defer rows.Close() + + for rows.Next() { + var enclosureID int64 + rows.Scan(&enclosureID) + go func() { + db.Exec(`DELETE FROM enclosures WHERE id=$1`, enclosureID) + }() + } +} diff --git a/ui/user_remove.go b/ui/user_remove.go index 6564ca32..70536fe9 100644 --- a/ui/user_remove.go +++ b/ui/user_remove.go @@ -5,6 +5,7 @@ package ui // import "miniflux.app/ui" import ( + "errors" "net/http" "miniflux.app/http/request" @@ -13,19 +14,19 @@ import ( ) func (h *handler) removeUser(w http.ResponseWriter, r *http.Request) { - user, err := h.store.UserByID(request.UserID(r)) + loggedUser, err := h.store.UserByID(request.UserID(r)) if err != nil { html.ServerError(w, r, err) return } - if !user.IsAdmin { + if !loggedUser.IsAdmin { html.Forbidden(w, r) return } - userID := request.RouteInt64Param(r, "userID") - selectedUser, err := h.store.UserByID(userID) + selectedUserID := request.RouteInt64Param(r, "userID") + selectedUser, err := h.store.UserByID(selectedUserID) if err != nil { html.ServerError(w, r, err) return @@ -36,6 +37,11 @@ func (h *handler) removeUser(w http.ResponseWriter, r *http.Request) { return } + if selectedUser.ID == loggedUser.ID { + html.BadRequest(w, r, errors.New("You cannot remove yourself")) + return + } + if err := h.store.RemoveUser(selectedUser.ID); err != nil { html.ServerError(w, r, err) return