feat(content-drive): bulk folder duplicate backend (#37062) - #37760
Conversation
The field-level contract both halves of folder duplication build against: POST /api/v1/assets/folders/_bulkduplicate with site-qualified assetPaths, the 202 job handle, the refusals before a job exists (no overlap refusal, unlike delete), the per-folder outcome and its reasons, the BULK_FOLDER_DUPLICATE_COMPLETED event, and the advertised folderBulkDuplicate.maxPaths ceiling. The frontend is already built against it with mocks; the backend implementation follows on this branch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Claude finished @zJaaal's task in 6m 4s —— View job Backend code review — bulk folder duplicate (#37062)Reviewed the full diff ( New issues introduced by this PRNone found. I traced the paths that usually break in a batch/async job and each holds up:
Existing issues (pre-existing, not introduced here)
Resolved since the prior review round (commit 4d52a52)
Minor observation (non-blocking, likely intentional)On cancellation, Verdict: no blocking issues. The prior-round findings are all resolved and I found no new bugs in this diff. 🤖 · reviewed |
…#37062) - BatchFailureReason.PARENT_PERMISSION_DENIED: no rights to add to the folder's parent, where its duplicate lands. Distinct from PERMISSION_DENIED, which is about the folder itself. - COVERED_BY_PARENT's Javadoc now covers both operations: for delete an ancestor removed the folder first, for duplication an ancestor's duplicate already carries it. - SystemEventType.BULK_FOLDER_DUPLICATE_COMPLETED, beside delete's. - FolderBulkDuplicateForm: the selected folders and nothing else. Additive only, so rollback-safe. Unit tests cover the new value, its JSON round trip, and the form's shape. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts, red (#37062) The US1 tests, pushed ahead of the implementation so CI proves they run and fail on their own assertions. Integration tests do not run locally here. - FolderBulkDuplicateResourceIT: the 202 handle, de-duplication of one folder spelled three ways, EMPTY_SELECTION, OVER_MAX_PATHS, an overridden ceiling, and the same folder accepted twice (no overlap guard). - FolderBulkDuplicateProcessorIT: duplicates land beside their sources, hold everything (generic content and archived items included), keep their state, copy nothing twice, keep relationships pointing at the originals, and announce COPY_FOLDER once each. - FolderBulkDuplicateResource Postman collection, in default-split: submit, follow the job, find the duplicate, the advertised ceiling, the two refusals, and cleanup. The helper and processor are RED STUBs that accept everything and do nothing; the implementation replaces them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ound job (#37062) POST /api/v1/assets/folders/_bulkduplicate answers 202 with a job handle and duplicates each folder beside its original, on the folderBulkDuplicate queue. - FolderBulkDuplicateHelper: refuses an empty selection (EMPTY_SELECTION) and one over FOLDER_BULK_DUPLICATE_MAX_PATHS (OVER_MAX_PATHS), collapses repeated folders ignoring case and the trailing slash, and enqueues. No overlap guard: the same folders submitted twice are two runs. - FolderBulkDuplicateResource and its refusal mapper, in bulk delete's shape; the ceiling is advertised as folderBulkDuplicate.maxPaths. - FolderDuplicator: runs the shipped FolderAPI.copy walk unchanged, then a complement pass that copies, folder by folder, every contentlet the walk leaves behind (generic content, archived items), read from the identifier table. One transaction per folder, so COPY_FOLDER fires once the content is in place and a failure leaves nothing half made. - FolderBulkDuplicateProcessor: one outcome per submitted folder, progress per folder, and a failed folder never stops the rest. Finer failure reasons, the nested-folder skip, cancellation, notification, retry and heartbeat follow in their own stories. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… test fixes (#37062) - US1 fixes, developer approved: - "holds everything" compares chosen names by name and id-derived names (generic content, links) by type and count, since a copy always gets a new id-based name. The last CI run showed all seven items present. - The relationship test pins what ships: the copy always links the original; when both sides are copied in one run it may also link the other side's copy. - The COPY_FOLDER test is dropped: EXCLUDE_OWNER events cannot be read back through getEventsSince, and FolderAPIImpl's events API cannot be injected. Covered end to end instead. - Naming (US2), regression guards: identifyDuplicate finds the one new _copy / _copy_copy, fails safe on none or two, ignores prefix-only names; repeated duplication yields _copy, _copy_copy, _copy_copy_copy; the endpoint's description states the naming rule. - Per-folder failures (US3), red until implemented: PERMISSION_DENIED, PARENT_PERMISSION_DENIED, PATH_NOT_FOUND and PROTECTED_FOLDER, each beside a folder that still succeeds; a run where every folder fails; a folder and its own child, in both orders. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cellation and notification (#37062) Per-folder failures (US3). Every refusal that can be known up front is decided before the first copy, so the rest of the selection still runs: - a folder inside another selected folder is SKIPPED / COVERED_BY_PARENT, decided from the paths alone, in either submission order; - a site root or the system folder is FAILED / PROTECTED_FOLDER; - a path that no longer resolves is FAILED / PATH_NOT_FOUND; - read on each folder and add-children where each duplicate lands are checked for the whole selection with filterCollection, one round-trip each, giving PERMISSION_DENIED and PARENT_PERMISSION_DENIED. Records stay in submission order. FolderDuplicator's Javadoc now says why the second pass asks the walk's own question rather than a database query: the walk lists pages through the search index. Also the red tests for the next two stories, with RED STUBs: - FolderBulkDuplicateCancellationIT: cancelling mid-run leaves every folder fully duplicated or not at all, and stops the run. - Rollback: a failure in the second pass, at the top or in a child folder, leaves no duplicate at all. - FolderBulkDuplicateNotificationIT: the completion push and the durable notification reach the submitter only, worded per outcome, and a delivery failure never changes the recorded outcome. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…7062) The test that a folder the author cannot read is refused with PERMISSION_DENIED, beside one that still duplicates, was lost when the COPY_FOLDER test above it was removed. Restored as approved. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and tell the author how it ended (#37062) Cancellation (US4): checked before each folder, never inside one, so a folder is always fully duplicated or not at all. Every folder the run never reached is recorded SKIPPED with no reason. FolderDuplicator now uses the ContentletAPI it is given, the seam the rollback tests use. Notification (US5): FolderBulkDuplicateCompletionListener, registered beside bulk delete's, mirrors it. It pushes BULK_FOLDER_DUPLICATE_COMPLETED to the submitter alone with the counts and per-folder results, and writes a durable notification worded for a clean, partial, failed or cancelled run. Both are best-effort and never change the recorded outcome. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts, red (#37062) FolderBulkDuplicateHeartbeatIT, mirroring bulk delete's: one folder of 500 contentlets duplicated through the real queue with a 2-second test heartbeat must advance updated_at more than once while running, never be mistaken for progress, and finish SUCCESS rather than ABANDONED. A second check pins that the processor carries @NoRetryPolicy, since a re-run would duplicate every folder again. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
dotCMS-Machine-User
left a comment
There was a problem hiding this comment.
✅ dotbot review: all reviewer models (meta/muse-spark-1.3, ~z-ai/glm-latest) agree — patch is correct.
approved automatically by dotbot
dario-daza
left a comment
There was a problem hiding this comment.
A few comments on the bulk duplicate backend: two correctness points (a descendant skipped as covered when its ancestor is never duplicated, and concurrent duplicates of the same folder), the notification after an abandoned run, a test suggestion for archived links and non-default-language items, and two non-blocking follow-ups (shared code with bulk delete, repeated queries in the second pass).
- A descendant is COVERED_BY_PARENT only when its ancestor was actually duplicated. Coverage is decided at run time and descendants run after their ancestors, so one under a refused or rolled-back ancestor is duplicated on its own, and one a cancellation left unreached is SKIPPED with no reason. - Overlapping duplicates into the same parent wait on a transaction-scoped advisory lock. Without it both derived the same name and the second failed on identifier_pkey, since identifier ids derive from the path. - A run that ends with no recorded outcome (abandoned, or failed before it could report) is notified as interrupted instead of "0 duplicated". - An archived menu link's copy is archived too; the walk copied it active. - The second pass compares against what actually landed in the duplicate, read from the identifier table, instead of re-running the walk's index-backed queries for every folder. Spec FR-016, FR-033, FR-037 and the contract are amended to match. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
4d52a52
dotCMS-Machine-User
left a comment
There was a problem hiding this comment.
✅ dotbot review: all reviewer models (meta/muse-spark-1.3, ~z-ai/glm-latest) agree — patch is correct.
approved automatically by dotbot
🟡 [P2] setenv.sh:326 guard OTel/Pyroscope agent attachPosted as a general PR comment because the referenced file is not part of this PR's diff. Current code: [ "$OTEL_JAVAAGENT_ENABLED" = "true" ] || return 0
echo "Adding OpenTelemetry agent to CATALINA_OPTS"
export CATALINA_OPTS="$CATALINA_OPTS -javaagent:$CATALINA_HOME/otel/opentelemetry-javaagent.jar"Problem: Missing jar aborts boot; re-sourcing duplicates agent flag. Fix: [ "$OTEL_JAVAAGENT_ENABLED" = "true" ] || return 0
[ -r "$CATALINA_HOME/otel/opentelemetry-javaagent.jar" ] || { echo "WARNING: OTel jar missing"; return 0; }
case "$CATALINA_OPTS" in *opentelemetry-javaagent.jar*) return 0;; esac
export CATALINA_OPTS="$CATALINA_OPTS -javaagent:$CATALINA_HOME/otel/opentelemetry-javaagent.jar" |
|
dotbot code review:
Opt-in agent wiring works when jars exist; remaining gap is non-blocking robustness for missing jars and duplicate attach. Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads. reviewed by dotbot · meta/muse-spark-1.3 · medium |
⚪ [P3] setenv.sh: OTEL/Pyroscope toggles lack the missing-jar guard just added for GlowrootPosted as a general PR comment because the referenced file is not part of this PR's diff. Current code: add_otel_agent() {
[ "$OTEL_JAVAAGENT_ENABLED" = "true" ] || return 0
echo "Adding OpenTelemetry agent to CATALINA_OPTS"
export CATALINA_OPTS="$CATALINA_OPTS -javaagent:$CATALINA_HOME/otel/opentelemetry-javaagent.jar"Problem: A Fix: add_otel_agent() {
[ "$OTEL_JAVAAGENT_ENABLED" = "true" ] || return 0
if [ ! -r "$CATALINA_HOME/otel/opentelemetry-javaagent.jar" ]; then
echo "WARNING: OTEL_JAVAAGENT_ENABLED=true but $CATALINA_HOME/otel/opentelemetry-javaagent.jar is not in this image; starting without OpenTelemetry"
return 0
fi
echo "Adding OpenTelemetry agent to CATALINA_OPTS"
export CATALINA_OPTS="$CATALINA_OPTS -javaagent:$CATALINA_HOME/otel/opentelemetry-javaagent.jar"Apply the same check to Assumption: existing images/dist builds predate this patch, so a stale toggle can meet a missing jar. |
|
dotbot code review:
The agent wiring is opt-in, operator variables correctly override defaults, the removed init-container approach is replaced by an in-image copy guaranteed by the pom, and the compose file's OTEL_/PYROSCOPE_ settings remain valid for the bundled agents. Only a low-severity robustness gap (no missing-jar guard for the new agents, unlike the one added for Glowroot) was identified. Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads. reviewed by dotbot · ~z-ai/glm-latest · medium |
Backend half of folder duplication in Content Drive (#37062). Bottom of the stack: this PR, then the frontend in #37761 on top.
Status: draft, in progress. Built test first, one story at a time.
BatchFailureReason.PARENT_PERMISSION_DENIED, a broaderCOVERED_BY_PARENTdescription that covers both delete and duplicate, theBULK_FOLDER_DUPLICATE_COMPLETEDevent type, and the request form, with unit tests._copynaming; a named reason for every refused folder, and a folder inside another selected folder skipped; cancellation between folders, recording where the run stopped; the completion push and durable notification to the submitter; a heartbeat for long folders, and no retry of an abandoned run.UNCLASSIFIEDwith the error, not as permission denied, since a failed check proves nothing about the author's rights. 4501422 fixes two limited-user tests that granted READ only on a parent folder, which does not reach the folder under it./blogs/, came backPROTECTED_FOLDER; only//hostname/is a site root now, and anything else is reportedPATH_NOT_FOUND.401the endpoint really returns, the same generic role check bulk delete uses, instead of a403 NOT_ENTITLEDnothing emits.openapi.yamlregenerated; Postman checks it.400 EMPTY_SELECTIONinstead of a500(4a49608), and each submitted folder is resolved without listing its contents (4da0ebe). Bulk delete shares both and is left for a follow-up.COVERED_BY_PARENTonly when that folder was actually duplicated. If the ancestor is refused or its duplicate rolls back, the child is duplicated on its own. If a cancellation stops the run before the ancestor, both are skipped with no reason.identifier_pkey. A duplicate now waits for any other duplicate landing in the same place, and nothing is ever refused.What the contract pins (
specs/37062-folder-copy-backend/contracts/folder-bulk-duplicate-api.md)POST /api/v1/assets/folders/_bulkduplicatewith site-qualifiedassetPathssuch as//demo.dotcms.com/blogs/alpha/. Each folder is duplicated in place, beside its original, under a name the server derives.202with{jobId, statusUrl, submitted}. The run is a background job.400 EMPTY_SELECTION,400 OVER_MAX_PATHS, and401for a caller who is not a back-end user. Unlike bulk delete there is no overlap refusal: a duplicate landing where another is being made waits for it rather than being refused.PERMISSION_DENIED,PARENT_PERMISSION_DENIED,PATH_NOT_FOUND,PROTECTED_FOLDER,UNCLASSIFIED, and the skipCOVERED_BY_PARENT. A skip with no reason means a cancelled run never reached the folder.BULK_FOLDER_DUPLICATE_COMPLETED.FOLDER_BULK_DUPLICATE_MAX_PATHS(default 50), advertised asfolderBulkDuplicate.maxPathson/api/v1/appconfiguration.The spec itself is in #37648, merged into
main; the amendments from review are in this PR.🤖 Generated with Claude Code
This PR fixes: #37062