diff --git a/internal/admin/user_delete.go b/internal/admin/user_delete.go index 11b06265b3..dbe4bc510e 100644 --- a/internal/admin/user_delete.go +++ b/internal/admin/user_delete.go @@ -220,7 +220,7 @@ func (data *userDeletionData) deleteExternalData(ctx context.Context, docEngine if docEngine == nil && (data.ownedTenantID != "" || len(data.datasets) > 0 || len(data.documents) > 0) { return fmt.Errorf("document engine is unavailable for user data cleanup") } - if store == nil && (len(data.datasets) > 0 || len(data.documents) > 0 || len(data.files) > 0) { + if store == nil && (data.ownedTenantID != "" || len(data.datasets) > 0 || len(data.documents) > 0 || len(data.files) > 0) { return fmt.Errorf("storage is unavailable for user data cleanup") } datasetIDs := make(map[string]struct{}, len(data.datasetIDs)) @@ -260,6 +260,21 @@ func (data *userDeletionData) deleteExternalData(ctx context.Context, docEngine } common.Info("Removed dataset bucket", zap.String("dataset", datasetNameID), zap.String("bucket", dataset.ID)) } + if data.ownedTenantID != "" { + downloadBucket := fmt.Sprintf("%s-downloads", data.ownedTenantID) + exists, err := store.BucketExistsWithError(ctx, downloadBucket) + if err != nil { + return fmt.Errorf("check downloads bucket %s: %w", downloadBucket, err) + } + if !exists { + common.Warn("Downloads bucket already missing", zap.String("bucket", downloadBucket)) + } else { + if err := store.RemoveBucket(ctx, downloadBucket); err != nil { + return fmt.Errorf("remove downloads bucket %s: %w", downloadBucket, err) + } + common.Info("Removed downloads bucket", zap.String("bucket", downloadBucket)) + } + } for _, file := range data.files { if file.SourceType != string(entity.FileSourceKnowledgebase) && file.Location != nil && *file.Location != "" && file.Type != "folder" { exists, err := store.ObjectExists(ctx, file.ParentID, *file.Location) @@ -527,8 +542,9 @@ func (data *userDeletionData) deleteDatabaseRows(ctx context.Context, tx *gorm.D } } commitQuery := tx.Model(&entity.FileCommit{}).Where("author_id = ?", userID) - if len(data.fileIDs) > 0 { - commitQuery = commitQuery.Or("folder_id IN ?", data.fileIDs) + commitFolderIDs := append(append([]string{}, data.fileIDs...), data.datasetIDs...) + if len(commitFolderIDs) > 0 { + commitQuery = commitQuery.Or("folder_id IN ?", commitFolderIDs) } if err = commitQuery.Pluck("id", &commitIDs).Error; err != nil { return err diff --git a/internal/admin/user_delete_test.go b/internal/admin/user_delete_test.go index 11e2a9abe9..fa83f2e7ba 100644 --- a/internal/admin/user_delete_test.go +++ b/internal/admin/user_delete_test.go @@ -188,6 +188,10 @@ func TestDeleteUserRemovesOwnedDataAndJoinedDocuments(t *testing.T) { insert("INSERT INTO tenant_model (id, model_name, provider_id, instance_id, model_type) VALUES (?, ?, ?, ?, ?)", "model", "model", "provider", "instance", 1) insert("INSERT INTO tenant_model_group (id, group_type, strategy) VALUES (?, ?, ?)", "group", "chat", "weighted") insert("INSERT INTO tenant_model_group_mapping (group_id, provider_id, instance_id, model_id) VALUES (?, ?, ?, ?)", "group", "provider", "instance", "model") + insert("INSERT INTO file_commit (id, folder_id, message, author_id, file_count) VALUES (?, ?, ?, ?, ?)", "own-commit", "own-kb", "Own page edit", "user-2", 1) + insert("INSERT INTO file_commit_item (id, commit_id, file_id, operation) VALUES (?, ?, ?, ?)", "own-commit-item", "own-commit", "own-page", "add") + insert("INSERT INTO file_commit (id, folder_id, message, author_id, file_count) VALUES (?, ?, ?, ?, ?)", "other-commit", "team-kb", "Other page edit", "user-2", 1) + insert("INSERT INTO file_commit_item (id, commit_id, file_id, operation) VALUES (?, ?, ?, ?)", "other-commit-item", "other-commit", "other-page", "add") docEngine := &deletionEngine{} store := &deletionStorage{} @@ -211,7 +215,8 @@ func TestDeleteUserRemovesOwnedDataAndJoinedDocuments(t *testing.T) { {"conversation", "own-conversation"}, {"conversation", "joined-conversation"}, {"api_4_conversation", "own-api-conversation"}, {"api_4_conversation", "agent-conversation"}, {"user_canvas", "agent"}, {"user_canvas_version", "agent-version"}, {"chat_channel", "channel"}, {"memory", "memory"}, {"tenant_model_provider", "provider"}, {"tenant_model_instance", "instance"}, - {"tenant_model", "model"}, {"tenant_model_group", "group"}, + {"tenant_model", "model"}, {"tenant_model_group", "group"}, {"file_commit", "own-commit"}, + {"file_commit_item", "own-commit-item"}, } { var count int64 if err := db.Table(item[0]).Where("id = ?", item[1]).Count(&count).Error; err != nil { @@ -231,7 +236,7 @@ func TestDeleteUserRemovesOwnedDataAndJoinedDocuments(t *testing.T) { t.Fatalf("dependent rows remain in %s: count=%d, err=%v", table, count, err) } } - for _, tableID := range [][2]string{{"document", "other-doc"}, {"file", "other-file"}, {"search", "other-search"}, {"knowledgebase", "team-kb"}, {"dialog", "team-chat"}, {"conversation", "other-conversation"}} { + for _, tableID := range [][2]string{{"document", "other-doc"}, {"file", "other-file"}, {"search", "other-search"}, {"knowledgebase", "team-kb"}, {"dialog", "team-chat"}, {"conversation", "other-conversation"}, {"file_commit", "other-commit"}, {"file_commit_item", "other-commit-item"}} { var count int64 if err := db.Table(tableID[0]).Where("id = ?", tableID[1]).Count(&count).Error; err != nil || count != 1 { t.Fatalf("shared resource %s/%s missing: count=%d, err=%v", tableID[0], tableID[1], count, err) @@ -241,7 +246,7 @@ func TestDeleteUserRemovesOwnedDataAndJoinedDocuments(t *testing.T) { if err := db.First(&team, "id = ?", "team-kb").Error; err != nil || team.DocNum != 1 || team.TokenNum != 3 || team.ChunkNum != 4 { t.Fatalf("joined dataset counters = %+v, err=%v", team, err) } - if !slices.Equal(store.buckets, []string{"own-kb"}) || !slices.Equal(store.emptyBuckets, []string{"own-folder"}) || !slices.Equal(store.files, []string{"own-kb/own.pdf", "team-kb/joined.pdf", "own-folder/own.pdf", "team-folder/joined.pdf"}) { + if !slices.Equal(store.buckets, []string{"own-kb", "user-1-downloads"}) || !slices.Equal(store.emptyBuckets, []string{"own-folder"}) || !slices.Equal(store.files, []string{"own-kb/own.pdf", "team-kb/joined.pdf", "own-folder/own.pdf", "team-folder/joined.pdf"}) { t.Fatalf("storage cleanup = buckets %v, empty buckets %v, files %v", store.buckets, store.emptyBuckets, store.files) } if !slices.Contains(docEngine.dropped, "ragflow_user-1") || !slices.Contains(docEngine.dropped, "memory_user-1") || !slices.Contains(docEngine.deleted, "doc_id:[joined-doc]") || !slices.Contains(docEngine.deleted, "metadata:id:[joined-doc]") {