139 lines
5.2 KiB
Go
139 lines
5.2 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/usememos/memos/store"
|
|
)
|
|
|
|
// DeleteSpace hard-deletes only memos directly placed in the Space. Relations
|
|
// are removed when either endpoint is deleted, but are never traversed.
|
|
func (d *DB) DeleteSpace(ctx context.Context, delete *store.DeleteSpace) (*store.DeleteSpaceResult, error) {
|
|
tx, err := d.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to begin space delete transaction")
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
if err := requirePostgresSpaceDeleteAdmin(ctx, tx, delete); err != nil {
|
|
return nil, err
|
|
}
|
|
memoIDs, err := listPostgresSpaceDeleteMemoIDs(ctx, tx, delete.ID)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to collect assigned memos")
|
|
}
|
|
attachments, err := deletePostgresMemoSetTx(ctx, tx, memoIDs)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to delete assigned memo set")
|
|
}
|
|
if _, err := tx.ExecContext(ctx, "DELETE FROM space_member WHERE space_id = $1", delete.ID); err != nil {
|
|
return nil, errors.Wrap(err, "failed to delete space memberships")
|
|
}
|
|
result, err := tx.ExecContext(ctx, "DELETE FROM space WHERE id = $1", delete.ID)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to delete space")
|
|
}
|
|
if count, err := result.RowsAffected(); err != nil {
|
|
return nil, errors.Wrap(err, "failed to count deleted spaces")
|
|
} else if count != 1 {
|
|
return nil, store.ErrSpaceNotFound
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return nil, errors.Wrap(err, "failed to commit space deletion")
|
|
}
|
|
return &store.DeleteSpaceResult{Attachments: attachments}, nil
|
|
}
|
|
|
|
func requirePostgresSpaceDeleteAdmin(ctx context.Context, tx *sql.Tx, delete *store.DeleteSpace) error {
|
|
var actorStatus store.RowStatus
|
|
if err := tx.QueryRowContext(ctx, `SELECT row_status FROM "user" WHERE id = $1`, delete.ActorUserID).Scan(&actorStatus); errors.Is(err, sql.ErrNoRows) {
|
|
return store.ErrSpacePermissionDenied
|
|
} else if err != nil {
|
|
return errors.Wrap(err, "failed to read space deletion actor")
|
|
} else if actorStatus != store.Normal {
|
|
return store.ErrSpacePermissionDenied
|
|
}
|
|
var spaceID int32
|
|
// Invitation creation takes the same lock before inserting its relationship.
|
|
if err := tx.QueryRowContext(ctx, "SELECT id FROM space WHERE id = $1 FOR UPDATE", delete.ID).Scan(&spaceID); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return store.ErrSpaceNotFound
|
|
}
|
|
return err
|
|
}
|
|
var role store.SpaceMemberRole
|
|
if err := tx.QueryRowContext(ctx, "SELECT role FROM space_member WHERE space_id = $1 AND user_id = $2 AND status = 'ACTIVE'", delete.ID, delete.ActorUserID).Scan(&role); errors.Is(err, sql.ErrNoRows) {
|
|
return store.ErrSpacePermissionDenied
|
|
} else if err != nil {
|
|
return errors.Wrap(err, "failed to read space administrator membership")
|
|
} else if role != store.SpaceMemberRoleAdmin {
|
|
return store.ErrSpacePermissionDenied
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func listPostgresSpaceDeleteMemoIDs(ctx context.Context, tx *sql.Tx, spaceID int32) ([]int32, error) {
|
|
rows, err := tx.QueryContext(ctx, "SELECT id FROM memo WHERE space_id = $1 ORDER BY id", spaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
ids := make([]int32, 0)
|
|
for rows.Next() {
|
|
var id int32
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
ids = append(ids, id)
|
|
}
|
|
return ids, rows.Err()
|
|
}
|
|
|
|
func deletePostgresMemoSetTx(ctx context.Context, tx *sql.Tx, memoIDs []int32) ([]*store.Attachment, error) {
|
|
attachments, err := listPostgresMemoSetAttachments(ctx, tx, memoIDs)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to collect memo attachments")
|
|
}
|
|
if err := deletePostgresMemoSetShares(ctx, tx, memoIDs); err != nil {
|
|
return nil, errors.Wrap(err, "failed to delete memo shares")
|
|
}
|
|
if err := deleteMemosTx(ctx, tx, memoIDs); err != nil {
|
|
return nil, errors.Wrap(err, "failed to delete memos")
|
|
}
|
|
if err := deleteReactionsByMemoIDsTx(ctx, tx, memoIDs); err != nil {
|
|
return nil, errors.Wrap(err, "failed to delete memo reactions")
|
|
}
|
|
if err := deleteAttachmentsByIDsTx(ctx, tx, attachmentIDsFromList(attachments)); err != nil {
|
|
return nil, errors.Wrap(err, "failed to delete memo attachments")
|
|
}
|
|
if err := deleteMemoRelationsTx(ctx, tx, memoIDs); err != nil {
|
|
return nil, errors.Wrap(err, "failed to delete incident memo relations")
|
|
}
|
|
return attachments, nil
|
|
}
|
|
|
|
func listPostgresMemoSetAttachments(ctx context.Context, tx *sql.Tx, memoIDs []int32) ([]*store.Attachment, error) {
|
|
attachments := make([]*store.Attachment, 0)
|
|
seen := make(map[int32]struct{})
|
|
for _, batch := range deleteUserBatches(memoIDs, deleteUserBatchSize) {
|
|
clause, args := deleteUserInClause(1, batch)
|
|
if err := appendDeleteUserAttachments(ctx, tx, `SELECT id, uid, creator_id, memo_id, storage_type, reference, payload
|
|
FROM attachment WHERE memo_id IN `+clause+` ORDER BY id`, args, seen, &attachments); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return attachments, nil
|
|
}
|
|
|
|
func deletePostgresMemoSetShares(ctx context.Context, tx *sql.Tx, memoIDs []int32) error {
|
|
for _, batch := range deleteUserBatches(memoIDs, deleteUserBatchSize) {
|
|
clause, args := deleteUserInClause(1, batch)
|
|
if _, err := tx.ExecContext(ctx, "DELETE FROM memo_share WHERE memo_id IN "+clause, args...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|