Pārlūkot izejas kodu

API: Delete users asynchronously

Deleting large users might lock the tables in the hosted offering
Frédéric Guillot 6 gadi atpakaļ
vecāks
revīzija
8fb71366f8
3 mainītis faili ar 101 papildinājumiem un 6 dzēšanām
  1. 3 2
      api/user.go
  2. 88 0
      storage/user.go
  3. 10 4
      ui/user_remove.go

+ 3 - 2
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)
 }

+ 88 - 0
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)
+		}()
+	}
+}

+ 10 - 4
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