138 lines
5.1 KiB
Go
138 lines
5.1 KiB
Go
package sqlite
|
|
|
|
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 := requireSQLiteSpaceDeleteAdmin(ctx, tx, delete); err != nil {
|
|
return nil, err
|
|
}
|
|
memoIDs, err := listSQLiteSpaceDeleteMemoIDs(ctx, tx, delete.ID)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to collect assigned memos")
|
|
}
|
|
attachments, err := deleteSQLiteMemoSetTx(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 = ?", delete.ID); err != nil {
|
|
return nil, errors.Wrap(err, "failed to delete space memberships")
|
|
}
|
|
result, err := tx.ExecContext(ctx, "DELETE FROM space WHERE id = ?", 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 requireSQLiteSpaceDeleteAdmin(ctx context.Context, tx dbExecutor, delete *store.DeleteSpace) error {
|
|
var actorStatus store.RowStatus
|
|
if err := tx.QueryRowContext(ctx, "SELECT row_status FROM user WHERE id = ?", 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
|
|
if err := tx.QueryRowContext(ctx, "SELECT id FROM space WHERE id = ?", 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 = ? AND user_id = ? 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 listSQLiteSpaceDeleteMemoIDs(ctx context.Context, tx dbExecutor, spaceID int32) ([]int32, error) {
|
|
rows, err := tx.QueryContext(ctx, "SELECT id FROM memo WHERE space_id = ? 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 deleteSQLiteMemoSetTx(ctx context.Context, tx dbExecutor, memoIDs []int32) ([]*store.Attachment, error) {
|
|
attachments, err := listSQLiteMemoSetAttachments(ctx, tx, memoIDs)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to collect memo attachments")
|
|
}
|
|
if err := deleteSQLiteMemoSetShares(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 listSQLiteMemoSetAttachments(ctx context.Context, tx dbExecutor, 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 deleteSQLiteMemoSetShares(ctx context.Context, tx dbExecutor, 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
|
|
}
|