fix(contentlet): stop timestamp preservation from failing binary copies on shared storage (#37068) - #37069
fix(contentlet): stop timestamp preservation from failing binary copies on shared storage (#37068)#37069fabrizzio-dotCMS wants to merge 3 commits into
Conversation
…es on shared storage (#37068) Copying a contentlet with a binary field aborted on NFS/EFS-backed asset volumes with "Cannot set the file time.". ESContentletAPIImpl.copyContentlet staged each binary through the two-arg commons-io FileUtils.copyFile, which defaults to preserveFileDate=true. The bytes were already copied at that point; only the trailing setTimes call failed, and commons-io turns that into an IOException. It surfaced as a WorkflowActionFailureException, killing the Copy action and any bulk copy that included the content. The destination is a brand new file under a throwaway UUID temp folder, so the source's modification time carries no meaning. Pass preserveFileDate = false, and REPLACE_EXISTING explicitly: that overload takes CopyOption as a varargs which would otherwise default to none, and Files.copy fails when the destination already exists - which happens when two binary fields on the same content type hold files with the same name. Real I/O failures still propagate as DotDataException. FileSystemStoragePersistenceAPIImpl.pushFile had the same defect writing into the asset bucket on the same shared volume, and is fixed alongside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @fabrizzio-dotCMS's task in 1m 14s —— View job PR Review
I reviewed the diff against
New IssuesNo issues found. The root-cause analysis is accurate: the byte copy at A couple of non-blocking observations (not defects, no action required):
Nice work — the fix is correct, minimal, and the reasoning in the comments will help the next reader understand why |
…37068) Deploy verification aid. The reported "Cannot set the file time." stack was traced to a build that predates the fix, so this logs a greppable marker immediately before FileUtils.copyFile in copyContentlet. If the copy fails and the marker is absent from dotcms.log, the running image does not contain the fix. If the marker is present, the failure is something else and the log carries the exact source and destination paths. To be reverted before merge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes #37068
Problem
Copying a contentlet with a binary field aborts on NFS/EFS-backed asset volumes (clustered Cloud environments) with:
It surfaces as a
WorkflowActionFailureException, so the Copy workflow action fails — and in a bulk copy run (fireBulkActionTasks) one bad file kills the task.Root cause
ESContentletAPIImpl.copyContentlet(...)staged each binary through the two-argument commons-io helper, which delegates withpreserveFileDate = true:setTimes(...)triesBasicFileAttributeView.setTimes(...)then falls back toFile.setLastModified(long). On NFS/EFS-style mounts both can fail.The byte copy has already succeeded at that point. Only the timestamp-preservation step fails, and the destination is a brand-new file under a throwaway UUID temp folder (
getRealAssetPathTmpBinary() + /<uuid>), so the source's modification time carries no business meaning. A cosmetic, best-effort operation was aborting a user-facing workflow action.The line dates to 2013 and is unchanged on
main— it only surfaces where the timestamp call is not permitted.Fix
Two details worth reviewing:
REPLACE_EXISTINGmust be passed explicitly. The three-arg form iscopyFile(src, dest, boolean, CopyOption...)— writing justcopyFile(src, dest, false)passes zero copy options, andFiles.copythen fails if the destination exists. This is not merely defensive:destFileistemporalFolder + "/" + srcFile.getName()andtemporalFolderis shared across all fields of the contentlet, so a content type with two binary fields holding files of the same name writes twice to the same path. That case was previously covered bycreateNewFile()plus the implicitREPLACE_EXISTINGof the two-arg overload.Files.copy(source unreadable, disk full, destination not writable) still throws and is wrapped asDotDataExceptionby the existing catch block.The now-redundant
createNewFile()is removed: commons-io creates parent directories and the destination file itself.Second call site
FileSystemStoragePersistenceAPIImpl.pushFile(...)had the same defect writing into the asset bucket on the same shared volume, and is fixed identically.REPLACE_EXISTINGthere preserves the previous overwrite semantics exactly.Audited but not changed
These share the
preserveFileDate = truedefault but are not on the shared asset path and have no reports — flagged for a follow-up rather than widened here:PublisherAPIImpl:379copyFile(bundle build)FsFileResource:117copyFile(WebDAV)FsDirectoryResource:122copyDirectory(WebDAV)OSGIUtil:1190copyDirectory(bundle dir bootstrap)Tests
Two integration tests added to
ESContentletAPIImplTest(already registered inMainSuite3a):copyContentletWithBinaryFieldKeepsFileContent— the copy carries its own binary, byte-identical to the source, not a pointer at the source file.copyContentletWithTwoBinaryFieldsSharingFileName— two binary fields whose files share a name; guards theREPLACE_EXISTINGoption described above. Without it this fails withFileAlreadyExistsException.Known coverage gap — the
setTimesfailure itself is not simulated. There is no portable way to force it in a test: as root in CI the modification time can always be set, and a fake wedge would not reproduce the real condition. The tests cover the copy contract and theREPLACE_EXISTINGregression; validating the original NFS/EFS scenario requires a manual check on a clustered environment (see QA note below).Verification
dotcms-corecompiles and installs clean.dotcms-integrationtest-compiles clean (BUILD SUCCESS).openapi.yamlunchanged — no annotation changes in this PR.QA
On a clustered Cloud environment with shared asset storage:
dotcms.logshows noCannot set the file time.and noWorkflowActionFailureException.🤖 Generated with Claude Code