Skip to content

Support bulk Context Center document operations#29380

Merged
harshach merged 16 commits into
mainfrom
harshach/drive-document-apis
Jun 30, 2026
Merged

Support bulk Context Center document operations#29380
harshach merged 16 commits into
mainfrom
harshach/drive-document-apis

Conversation

@harshach

@harshach harshach commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Describe your changes:

Adds bulk delete, move, and zip download APIs for Context Center drive files, fixes current-content resolution for downloads, supports orderBy on document listing, and validates duplicate uploaded filenames case-insensitively per folder or root without using displayName.
The UI now calls the bulk APIs for selected document actions and defaults document listing to recent-first ordering.

Fixes 29489

Screen.Recording.2026-06-25.at.11.06.53.PM.mov

Type of change:

  • Bug fix
  • Improvement
  • New feature

High-level design:

The backend keeps per-file authorization and per-row success/failure reporting for bulk delete and move, streams bulk downloads as a zip, and performs duplicate checks by joining context_file to the current asset filename.
Existing single-file endpoints remain compatible, while list ordering only changes when orderBy is supplied.

Tests:

Use cases covered

  • Bulk delete and move of multiple Context Center documents.
  • Bulk download of multiple uploaded documents as a zip.
  • Download fallback for current content and asset resolution.
  • Duplicate filename rejection within the same folder or root, case-insensitive.
  • Same filename allowed in different folders.
  • Document list ordering by updatedAt.

Unit tests

  • I added unit tests for the new/changed logic.
  • Files added/updated: N/A.
  • Coverage %: Not measured.

Backend integration tests

  • I added integration tests in openmetadata-integration-tests/ for new/changed API endpoints.
  • Files added/updated:
    • openmetadata-integration-tests/src/test/java/org/openmetadata/it/drive/ContextFileIT.java
    • openmetadata-integration-tests/src/test/java/org/openmetadata/it/drive/DriveFileUploadIT.java

Ingestion integration tests

  • Not applicable. No ingestion changes.

Playwright (UI) tests

  • Not applicable. UI changes wire existing controls to the new APIs.

Manual testing performed

  • mvn spotless:apply
  • UI checkstyle sequence: organize-imports-cli, eslint --fix, prettier --write
  • mvn -pl openmetadata-integration-tests -am -DskipTests -DskipITs package
  • git diff --check
  • Live MinIO tests were not run because localhost:9000 was not reachable in this workspace.

UI screen recording / screenshots:

Not attached. UI changes wire existing controls to bulk APIs; no recording was captured in this workspace.

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR title is Fixes <issue-number>: <short explanation>
  • My PR is linked to a GitHub issue via Fixes #<issue-number> above.
  • I have commented on my code, particularly in hard-to-understand areas.
  • For JSON Schema changes: I updated the migration scripts or explained why it is not needed.
  • For UI changes: I attached a screen recording and/or screenshots above.
  • I have added tests (unit / integration / Playwright as applicable) and listed them above.

Greptile Summary

This PR adds bulk delete, bulk move, and bulk ZIP-download endpoints for Context Center drive files, fixes asset resolution for downloads by adding a full content-version fallback, introduces orderBy support on document listing with proper cursor validation, and enforces case-insensitive duplicate filename checks per folder. The UI is updated to call the new bulk APIs instead of issuing one request per file.

  • Backend bulk APIs (/bulk/delete, /bulk/move, /bulk/download) are added with per-item authorization, structured BulkOperationResult responses for delete/move, and streamed ZIP output for download; temp files used during ZIP creation are cleaned up in all paths.
  • Asset resolution now falls back from headContentIdassetId → full content-list scan sorted by isCurrent then ingestedAt, preventing silent 404s when head-content is stale.
  • sanitizeEntityName no longer appends a UUID suffix, relying instead on the new validateNoDuplicateFileName check to enforce uniqueness by querying the context_file table case-insensitively.

Confidence Score: 3/5

Two unresolved defects from prior reviews remain in the changed code: the duplicate-name check and the DB insert are not atomic (a concurrent upload pair can both pass the check and collide at the unique index), and folder delete now always cascades recursively without any signal to callers that previously relied on the non-recursive safety guard.

The bulk APIs themselves are well-structured, the cursor-mismatch crash is fixed, and the revokeObjectURL timing issue is addressed. However, removing the UUID suffix from sanitizeEntityName while leaving a non-atomic duplicate check creates a real data-integrity gap on concurrent uploads. Separately, FolderResource silently changed the semantics of DELETE /folders/{id} from fail-if-non-empty to always-recursive, which breaks any caller that passed ?recursive=false or omitted the parameter to protect non-empty folders.

ContextFileResource.java (upload path, non-atomic duplicate check) and FolderResource.java (recursive-delete behavioral change) warrant the most scrutiny before merging.

Important Files Changed

Filename Overview
openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileResource.java Core file with new bulk delete/move/download endpoints; the duplicate-key race window and async ZIP temp-file lifecycle are correct, but bulkDownloadFiles throws EntityNotFoundException (unstructured 404) when any file lacks an asset — inconsistent with the structured BulkOperationResult used by delete and move.
openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ContextFileRepository.java Adds validateNoDuplicateFileName, listByUpdatedAt with proper updatedAt-keyed cursor parsing (null-safe, throws 400 on mismatch), and moveContextFile with duplicate validation; pagination logic is correct.
openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java Adds countByFileNameInFolder and six listByUpdatedAt* JDBI queries; MySQL/Postgres variants are identical (standard SQL), cursor-keyset logic matches the repository pagination branches.
openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/FolderResource.java Removes the ?recursive query param and hardcodes true, making folder delete always recursive — a silent behavioral change for callers that relied on the non-recursive safety guard (flagged in previous review).
openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx Replaces per-file Promise.allSettled loops with single bulk API calls; error handling via try/catch with showErrorToast; partial delete/move results are correctly reflected in local state.
openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts Adds bulkDeleteDriveFiles, bulkMoveFilesToFolder, downloadDriveFiles with blob-error normalization; normalizeBlobError correctly re-throws as a typed AxiosError with a text message.
openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterPureUtils.ts Extracts downloadBlob utility that delays URL.revokeObjectURL by 1s via setTimeout, fixing the previous synchronous-revoke concern.
openmetadata-ui/src/main/resources/ui/playwright/utils/ContextCenterUtil.ts Adds shared Playwright helpers for upload, row selection, bulk assertion, and download capture; prototype monkey-patch in installDownloadCapture is guarded against double-application but not restored between test invocations.
openmetadata-integration-tests/src/test/java/org/openmetadata/it/drive/ContextFileIT.java New integration tests cover bulk move/delete, cascade-delete, orderBy listing, and cursor-mismatch rejection; tests are self-contained with their own data setup/teardown.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as Browser / UI
    participant API as ContextFileResource
    participant Repo as ContextFileRepository
    participant DB as Database
    participant S3 as AssetService (S3/MinIO)

    note over UI,S3: Bulk Delete
    UI->>API: "POST /bulk/delete {ids, hardDelete}"
    loop per id
        API->>API: delete(id) auth check inside
        API-->>DB: soft/hard delete
    end
    API-->>UI: BulkOperationResult (SUCCESS/PARTIAL_SUCCESS/FAILURE)

    note over UI,S3: Bulk Move
    UI->>API: "PUT /bulk/move {ids, folder}"
    loop per id
        API->>API: authorizer.authorize(EDIT_ALL)
        API->>Repo: moveContextFile(id, newFolder)
        Repo->>DB: validateNoDuplicateFileName
        Repo->>DB: UPDATE context_file / entity_relationship
    end
    API-->>UI: BulkOperationResult

    note over UI,S3: Bulk Download
    UI->>API: "POST /bulk/download {ids}"
    loop per id (resolve assets)
        API->>Repo: getInternal(id) + resolveAsset(file)
        Repo->>DB: getContentById / getAssetById
    end
    loop per id (download to temp)
        API->>S3: assetService.read(asset).join()
        S3-->>API: InputStream
        API->>API: write to temp file
    end
    API->>UI: stream ZipOutputStream (temp files to zip to cleanup)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as Browser / UI
    participant API as ContextFileResource
    participant Repo as ContextFileRepository
    participant DB as Database
    participant S3 as AssetService (S3/MinIO)

    note over UI,S3: Bulk Delete
    UI->>API: "POST /bulk/delete {ids, hardDelete}"
    loop per id
        API->>API: delete(id) auth check inside
        API-->>DB: soft/hard delete
    end
    API-->>UI: BulkOperationResult (SUCCESS/PARTIAL_SUCCESS/FAILURE)

    note over UI,S3: Bulk Move
    UI->>API: "PUT /bulk/move {ids, folder}"
    loop per id
        API->>API: authorizer.authorize(EDIT_ALL)
        API->>Repo: moveContextFile(id, newFolder)
        Repo->>DB: validateNoDuplicateFileName
        Repo->>DB: UPDATE context_file / entity_relationship
    end
    API-->>UI: BulkOperationResult

    note over UI,S3: Bulk Download
    UI->>API: "POST /bulk/download {ids}"
    loop per id (resolve assets)
        API->>Repo: getInternal(id) + resolveAsset(file)
        Repo->>DB: getContentById / getAssetById
    end
    loop per id (download to temp)
        API->>S3: assetService.read(asset).join()
        S3-->>API: InputStream
        API->>API: write to temp file
    end
    API->>UI: stream ZipOutputStream (temp files to zip to cleanup)
Loading

Comments Outside Diff (1)

  1. openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileResource.java, line 1030-1047 (link)

    P1 Zip deduplication collision with pre-existing suffixed names

    The usedNames map tracks the safeName → count but never checks whether a generated suffix name (e.g. "foo (2).txt") already exists as its own entry. If the download batch contains both a file named "foo (2).txt" and two files named "foo.txt", the second "foo.txt" is also renamed to "foo (2).txt", silently creating a duplicate zip entry. Most extraction tools will only unpack one of them, causing the other file's content to be lost without any error to the user.

Reviews (20): Last reviewed commit: "lint fix" | Re-trigger Greptile

@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels Jun 24, 2026
@harshach harshach added the skip-pr-checks Bypass PR metadata validation check label Jun 24, 2026
@harshach harshach marked this pull request as ready for review June 24, 2026 00:29
@harshach harshach requested a review from a team as a code owner June 24, 2026 00:29
Comment thread openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 62%
63.06% (70469/111744) 45.91% (40638/88510) 47.35% (12577/26560)

Comment thread conf/openmetadata.yaml Outdated
Comment on lines +818 to +832
private String zipEntryName(DownloadEntry entry, Set<String> usedNames) {
String sourceName =
entry.asset().getFileName() != null && !entry.asset().getFileName().isBlank()
? entry.asset().getFileName()
: entry.file().getDisplayName() != null && !entry.file().getDisplayName().isBlank()
? entry.file().getDisplayName()
: entry.file().getName();
String safeName = sanitizeFileName(sourceName).replace('/', '_');
String candidate = safeName;
int count = 2;
while (!usedNames.add(candidate)) {
candidate = zipEntryNameWithSuffix(safeName, count);
count++;
}
return candidate;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: Zip dedup is case-sensitive; collisions on case-insensitive FS

zipEntryName/usedNames (a HashSet<String>) deduplicates generated zip entry names case-sensitively. Two selected files whose sanitized names differ only in case (e.g. Report.txt and report.txt) are both accepted as distinct zip entries. On case-insensitive filesystems (Windows, default macOS) the extractor will treat them as the same path, and one file silently overwrites the other on extraction — the user loses content with no error.

Suggested fix: track a case-insensitive key in the dedup set (e.g. usedNames.add(candidate.toLowerCase(Locale.ROOT))) while still emitting the original-cased name as the entry, so disambiguation suffixes are applied for case-only collisions too.

Was this helpful? React with 👍 / 👎

Comment on lines +540 to +554
List<UUID> ids = validateBulkIds(request == null ? null : request.ids);
limits.enforceBulkSizeLimit(entityType, ids.size());

AssetService assetService = AssetServiceFactory.getService();
if (assetService == null) {
return Response.status(Response.Status.SERVICE_UNAVAILABLE)
.entity("{\"message\":\"Object storage is not configured\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}

List<ResolvedDownloadEntry> resolvedEntries = new ArrayList<>();
for (UUID id : ids) {
ContextFile file = getInternal(uriInfo, securityContext, id, "", include);
Asset asset = resolveAsset(file);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: Bulk download stages every file to local temp disk with no size cap

bulkDownloadFiles reads each requested file fully into a temp file on the server before streaming the zip. The bulk-size limit (limits.enforceBulkSizeLimit) caps the number of items, but not the total bytes. A request for many large files can exhaust the server's temp filesystem before any response is streamed. The temp files are cleaned up in a finally, but the peak on-disk footprint is unbounded relative to total content size.

Consider streaming entries directly into the ZipOutputStream (accepting that a mid-stream read failure can't change the already-sent 200), or enforcing a cumulative byte budget while staging and aborting with 413/400 once exceeded.

Was this helpful? React with 👍 / 👎

Comment on lines +756 to +761
private Response buildBulkOperationResponse(BulkOperationResult result) {
if (result.getStatus() == ApiStatus.FAILURE) {
return Response.status(Response.Status.BAD_REQUEST).entity(result).build();
}
return Response.ok(result).build();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Quality: All-failure bulk response returns HTTP 400 regardless of cause

buildBulkOperationResponse returns HTTP 400 whenever every item fails (ApiStatus.FAILURE), even when the per-item failures are 403 (authorization) or 404 (not found). The accurate per-item statuses are preserved in the response body, but clients/proxies keying off the HTTP status will misclassify authorization or not-found failures as bad-request. Consider deriving the envelope status from the dominant per-item status, or documenting that callers must inspect failedRequest[].status.

Was this helpful? React with 👍 / 👎

@sonarqubecloud

Copy link
Copy Markdown

@sonarqubecloud

Copy link
Copy Markdown

@gitar-bot

gitar-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 5 resolved / 8 findings

Adds bulk delete, move, and zip-download endpoints with improved document listing and case-insensitive filename validation. Please address the potential zip deduplication collisions and the lack of size limits for server-side temp file staging during bulk downloads.

💡 Edge Case: Zip dedup is case-sensitive; collisions on case-insensitive FS

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileResource.java:818-832 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileResource.java:603-607

zipEntryName/usedNames (a HashSet<String>) deduplicates generated zip entry names case-sensitively. Two selected files whose sanitized names differ only in case (e.g. Report.txt and report.txt) are both accepted as distinct zip entries. On case-insensitive filesystems (Windows, default macOS) the extractor will treat them as the same path, and one file silently overwrites the other on extraction — the user loses content with no error.

Suggested fix: track a case-insensitive key in the dedup set (e.g. usedNames.add(candidate.toLowerCase(Locale.ROOT))) while still emitting the original-cased name as the entry, so disambiguation suffixes are applied for case-only collisions too.

💡 Edge Case: Bulk download stages every file to local temp disk with no size cap

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileResource.java:540-554

bulkDownloadFiles reads each requested file fully into a temp file on the server before streaming the zip. The bulk-size limit (limits.enforceBulkSizeLimit) caps the number of items, but not the total bytes. A request for many large files can exhaust the server's temp filesystem before any response is streamed. The temp files are cleaned up in a finally, but the peak on-disk footprint is unbounded relative to total content size.

Consider streaming entries directly into the ZipOutputStream (accepting that a mid-stream read failure can't change the already-sent 200), or enforcing a cumulative byte budget while staging and aborting with 413/400 once exceeded.

💡 Quality: All-failure bulk response returns HTTP 400 regardless of cause

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileResource.java:756-761 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileResource.java:791-805

buildBulkOperationResponse returns HTTP 400 whenever every item fails (ApiStatus.FAILURE), even when the per-item failures are 403 (authorization) or 404 (not found). The accurate per-item statuses are preserved in the response body, but clients/proxies keying off the HTTP status will misclassify authorization or not-found failures as bad-request. Consider deriving the envelope status from the dominant per-item status, or documenting that callers must inspect failedRequest[].status.

✅ 5 resolved
Edge Case: Bulk zip download streams HTTP 200 even when a file read fails

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileResource.java:553-567
In bulkDownloadFiles the Response.ok(...) (HTTP 200 + headers) is returned before the StreamingOutput runs, so the status and Content-Disposition are committed before any content is read. Inside the stream, if assetService.read(...).join() returns a null InputStream, or an IOException/RuntimeException occurs while transferring bytes for the 2nd..Nth file, the code throws WebApplicationException — but the client has already received a 200 and the partial bytes already written. The result is a silently truncated/corrupt zip that the client cannot distinguish from success (the IT only covers the happy path).

The pre-loop check (resolveAsset + EntityNotFoundException) only verifies the asset record exists; it does not guarantee the object-store content is readable, so the mid-stream failure path is reachable. Consider validating/opening all streams (or at least HEAD-checking content) before committing the 200, or document/accept truncation and ensure the zip entry is aborted in a way the client detects (e.g. omit the broken entry and add a manifest). At minimum, log the failure server-side so truncated downloads are diagnosable.

Quality: Bulk download error toast may render an unreadable Blob body

📄 openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts:193-201 📄 openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx:322-336
downloadDriveFiles issues the request with responseType: 'blob'. When the backend returns a JSON error (e.g. 503 'Object storage is not configured' from bulkDownloadFiles, or 404), axios will surface err.response.data as a Blob, not a parsed JSON object. handleBulkDownload passes err as AxiosError directly to showErrorToast, which typically reads the message from the response body — with a Blob body the user is likely to see an empty or '[object Blob]' toast instead of the server message. Consider reading the blob as text on error (e.g. await err.response.data.text()), or only set responseType: 'blob' for successful responses, so the real error message is shown.

Bug: Duplicate-name check compares raw filename to sanitized stored name

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileResource.java:300 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileResource.java:317 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileUploadSupport.java:85-99 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java:15097 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java:15113 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ContextFileRepository.java:319-331
On upload, validateNoDuplicateFileName(originalFileName, ...) is called with the raw, un-sanitized filename, while the stored context_file.name (cf.name) is sanitizeEntityName(originalFileName), which lower-cases and replaces every char outside [a-zA-Z0-9._-] with _ and collapses repeats. The duplicate query now matches LOWER(COALESCE(ae.name, cf.name)) = LOWER(:fileName). When ae.name is NULL (buildAsset only sets fileName, never name) the COALESCE falls back to cf.name, so the comparison becomes sanitized-name vs raw-name.

For any filename containing characters that sanitization transforms (e.g. spaces or parentheses), the two strings differ: uploading "Quarterly Report.pdf" stores cf.name = "quarterly_report.pdf", and a second upload of "Quarterly Report.pdf" compares "quarterly_report.pdf" against LOWER("Quarterly Report.pdf") = "quarterly report.pdf" — no match, so the duplicate is silently accepted. This regressed with this commit's removal of the UUID suffix (making names deterministic) combined with the COALESCE-to-cf.name fallback. The new integration tests only exercise filenames without spaces/special chars (Duplicate.PDF, NestedDuplicate.txt, shared-name.txt), so they don't catch this gap.

Fix: compare against a consistent value — either pass the sanitized name into validateNoDuplicateFileName, or ensure ae.name/cf.name stores the original filename used for the comparison. If matching on sanitized names is intended, sanitize :fileName in the same way before the query.

Bug: Duplicate-filename check is TOCTOU; no DB uniqueness guard

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ContextFileRepository.java:319-329 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ContextFileRepository.java:207 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java:15084-15098
validateNoDuplicateFileName runs a SELECT count(*) before the insert (upload at ContextFileResource ~line 311, move at ContextFileRepository:207). There is no unique constraint backing it, so two concurrent uploads of the same filename to the same folder can both pass the count check and both be persisted, defeating the constraint the feature intends to enforce. Additionally, because the count query inner-joins asset_entity and matches on LOWER(ae.name), files that have no resolvable asset are never counted as duplicates — acceptable for upload (always has an asset) but means the rule is silently skipped for entity-only files created via the create API. If strict uniqueness is required, add a DB-level unique index (e.g. on folder + lowercased filename) or accept the race as best-effort and note it.

Edge Case: Folder DELETE silently ignores recursive flag, always cascades

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/FolderResource.java:204-218 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/FolderRepository.java:120-123
The folder delete path now forces cascade behaviour in two places, making the public recursive query parameter dead and removing the non-empty-folder safety guard.

In FolderResource.delete (line 209), boolean cascade = true; replaces the recursive argument in all three super.delete(...) / deleteByIdAsync(...) calls, while the @QueryParam("recursive") @DefaultValue("false") parameter is still declared but never read. Additionally, FolderRepository.deleteChildren (line 121-123) overrides the base method to hard-code super.deleteChildren(id, true, hardDelete, updatedBy), forcing recursive deletion regardless of the flag passed by any caller.

The base EntityRepository.deleteChildren throws IllegalArgumentException(entityIsNotEmpty(...)) when recursive=false and children exist. That protection is now bypassed for folders: a client calling DELETE /folders/{id} (default recursive=false) expecting a rejection of a non-empty folder will instead silently cascade-delete every child folder and file. While cascading is the stated intent of this commit, the misleading parameter is an API-contract / data-loss surprise.

Suggested fix: drop the unused recursive parameter from the endpoint signature (or honor it and document that folder deletes always cascade), and rely solely on the repository-level override so the behaviour is explicit. At minimum update the @Operation/@Parameter docs to state that folder deletion always cascades.

🤖 Prompt for agents
Code Review: Adds bulk delete, move, and zip-download endpoints with improved document listing and case-insensitive filename validation. Please address the potential zip deduplication collisions and the lack of size limits for server-side temp file staging during bulk downloads.

1. 💡 Edge Case: Zip dedup is case-sensitive; collisions on case-insensitive FS
   Files: openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileResource.java:818-832, openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileResource.java:603-607

   `zipEntryName`/`usedNames` (a `HashSet<String>`) deduplicates generated zip entry names case-sensitively. Two selected files whose sanitized names differ only in case (e.g. `Report.txt` and `report.txt`) are both accepted as distinct zip entries. On case-insensitive filesystems (Windows, default macOS) the extractor will treat them as the same path, and one file silently overwrites the other on extraction — the user loses content with no error.
   
   Suggested fix: track a case-insensitive key in the dedup set (e.g. `usedNames.add(candidate.toLowerCase(Locale.ROOT))`) while still emitting the original-cased name as the entry, so disambiguation suffixes are applied for case-only collisions too.

2. 💡 Edge Case: Bulk download stages every file to local temp disk with no size cap
   Files: openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileResource.java:540-554

   `bulkDownloadFiles` reads each requested file fully into a temp file on the server before streaming the zip. The bulk-size limit (`limits.enforceBulkSizeLimit`) caps the number of items, but not the total bytes. A request for many large files can exhaust the server's temp filesystem before any response is streamed. The temp files are cleaned up in a `finally`, but the peak on-disk footprint is unbounded relative to total content size.
   
   Consider streaming entries directly into the `ZipOutputStream` (accepting that a mid-stream read failure can't change the already-sent 200), or enforcing a cumulative byte budget while staging and aborting with 413/400 once exceeded.

3. 💡 Quality: All-failure bulk response returns HTTP 400 regardless of cause
   Files: openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileResource.java:756-761, openmetadata-service/src/main/java/org/openmetadata/service/resources/drive/ContextFileResource.java:791-805

   `buildBulkOperationResponse` returns HTTP 400 whenever every item fails (`ApiStatus.FAILURE`), even when the per-item failures are 403 (authorization) or 404 (not found). The accurate per-item statuses are preserved in the response body, but clients/proxies keying off the HTTP status will misclassify authorization or not-found failures as bad-request. Consider deriving the envelope status from the dominant per-item status, or documenting that callers must inspect `failedRequest[].status`.

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Bulk Document Operations, ZIP Downloads, Ordering Support, and Filename Validation

4 participants