Repository navigation
feat(storage): share temporary uploads and WebDAV temporary files through S3 #37775
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: s3-stack/4-publishing
Are you sure you want to change the base?
Changes from all commits
dac557e
066326b
668fa15
e7d3e70
2b02e9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,8 @@ | |
| import com.dotcms.http.CircuitBreakerUrl; | ||
| import com.dotcms.http.CircuitBreakerUrl.Method; | ||
| import com.dotcms.rest.exception.BadRequestException; | ||
| import com.dotcms.storage.AssetStorageFeature; | ||
| import com.dotcms.storage.TemporaryAssetStorage; | ||
| import com.dotcms.util.CloseUtils; | ||
| import com.dotcms.util.ConversionUtils; | ||
| import com.dotcms.util.SecurityUtils; | ||
|
|
@@ -112,7 +114,14 @@ public DotTempFile createEmptyTempFile(final String incomingFileName,final HttpS | |
| final String tempFileId = TEMP_RESOURCE_PREFIX + UUIDGenerator.shorty(); | ||
|
|
||
| final String tempFileUri = File.separator + tempFileId + File.separator + incomingFileName; | ||
| final File tempFile = new File(APILocator.getFileAssetAPI().getRealAssetPathTmpBinary() + tempFileUri); | ||
| final File tempFile; | ||
| try { | ||
| tempFile = AssetStorageFeature.isEnabled() | ||
| ? TemporaryAssetStorage.getInstance().file(tempFileId, incomingFileName) | ||
| : new File(APILocator.getFileAssetAPI().getRealAssetPathTmpBinary() + tempFileUri); | ||
| } catch (IOException e) { | ||
| throw new DotRuntimeException("Invalid temporary upload path", e); | ||
| } | ||
| final File tempFolder = tempFile.getParentFile(); | ||
|
|
||
| if (!tempFolder.mkdirs()) { | ||
|
|
@@ -126,6 +135,14 @@ public DotTempFile createEmptyTempFile(final String incomingFileName,final HttpS | |
| throw new DotRuntimeException("Invalid file upload"); | ||
| } | ||
| createTempPermissionFile(tempFolder, allowList); | ||
| if (AssetStorageFeature.isEnabled()) { | ||
| try { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 [P2] TempFileAPI.java:139 managed marker written before upload completes, fencing servable local bytes Current code: if (AssetStorageFeature.isEnabled()) {
try {
Files.writeString(java.nio.file.Path.of(com.dotmarketing.util.ConfigUtils.getAssetTempPath(),
tempFileId, TemporaryAssetStorage.MANAGED_MARKER), "");Problem: Marker is written at create time, before any receipt exists. If the upload aborts or Fix: if (AssetStorageFeature.isEnabled()) {
// marker is written by TemporaryAssetStorage.store() once the S3 copy is verified
}(store() already writes |
||
| Files.writeString(java.nio.file.Path.of(com.dotmarketing.util.ConfigUtils.getAssetTempPath(), | ||
| tempFileId, TemporaryAssetStorage.MANAGED_MARKER), ""); | ||
| } catch (IOException e) { | ||
| throw new DotRuntimeException("Unable to mark temporary upload", e); | ||
| } | ||
| } | ||
| SecurityLogger.logInfo(this.getClass(),"Temp File Created with id: " + tempFileId + ", uploaded by userId: " + user.getUserId()); | ||
| return new DotTempFile(tempFileId, tempFile); | ||
| } | ||
|
|
@@ -174,20 +191,21 @@ public DotTempFile createTempFile(final String incomingFileName,final HttpServle | |
| final File tempFile = dotTempFile.file; | ||
| final long maxLength = maxFileSize(request); | ||
|
|
||
| try (final OutputStream out = new BoundedOutputStream(maxLength,Files.newOutputStream(tempFile.toPath()))) { | ||
| try { | ||
| try (final OutputStream out = new BoundedOutputStream(maxLength,Files.newOutputStream(tempFile.toPath()))) { | ||
|
|
||
|
|
||
| int read = 0; | ||
| final byte[] bytes = new byte[4096]; | ||
| while ((read = inputStream.read(bytes)) != -1) { | ||
| out.write(bytes, 0, read); | ||
| } | ||
|
|
||
| if (dotTempFile.metadata == null && dotTempFile.file.exists()) { | ||
|
|
||
| return new DotTempFile(dotTempFile.id, dotTempFile.file); | ||
| if (!AssetStorageFeature.isEnabled()) { | ||
| return dotTempFile.metadata == null && dotTempFile.file.exists() | ||
| ? new DotTempFile(dotTempFile.id, dotTempFile.file) : dotTempFile; | ||
| } | ||
| } | ||
| return dotTempFile; | ||
| return completeTempFile(dotTempFile); | ||
| } catch (IOException e) { | ||
| final String message = APILocator.getLanguageAPI().getStringKey(WebAPILocator.getLanguageWebAPI().getLanguage(request), "temp.file.max.file.size.error").replace("{0}", UtilMethods.prettyByteify(maxLength)); | ||
| throw new DotStateException(message, e); | ||
|
|
@@ -198,6 +216,24 @@ public DotTempFile createTempFile(final String incomingFileName,final HttpServle | |
| } | ||
| } | ||
|
|
||
| /** Finish a closed upload, including files written by the image editor. */ | ||
| public DotTempFile completeTempFile(final DotTempFile temporary) { | ||
| DotTempFile completed = temporary.metadata == null && temporary.file.exists() | ||
| ? new DotTempFile(temporary.id, temporary.file) : temporary; | ||
| if (AssetStorageFeature.isEnabled()) { | ||
| // DotTempFile can add a detected extension while building its metadata. | ||
| if (!completed.file.exists() && !completed.file.getName().equals(completed.fileName)) { | ||
| completed = new DotTempFile(completed.id, new File(completed.file.getParentFile(), completed.fileName)); | ||
| } | ||
| try { | ||
| TemporaryAssetStorage.getInstance().store(completed.id, completed.file); | ||
| } catch (com.dotmarketing.exception.DotDataException e) { | ||
| throw new DotRuntimeException("Unable to complete temporary upload", e); | ||
| } | ||
| } | ||
| return completed; | ||
| } | ||
|
|
||
| /** | ||
| * Takes a URL, downloads it and the returns the resulting file as tempFile with a unique ID and | ||
| * file handle that can be used to access the temp file. The request will be used to create a | ||
|
|
@@ -246,11 +282,7 @@ public DotTempFile createTempFileFromUrl(final String incomingFileName, | |
| urlGetter.doOut(out); | ||
| } | ||
|
|
||
| if (dotTempFile.metadata == null && dotTempFile.file.exists()) { | ||
|
|
||
| return new DotTempFile(dotTempFile.id, dotTempFile.file); | ||
| } | ||
| return dotTempFile; | ||
| return completeTempFile(dotTempFile); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -360,6 +392,19 @@ private boolean canUseTempFile(final List<String> incomingAccessingList, final D | |
| * @return | ||
| */ | ||
| public Optional<DotTempFile> getTempFile(final List<String> accessingList, final String tempFileId) { | ||
| if (AssetStorageFeature.isEnabled()) { | ||
| if (!TemporaryAssetStorage.validId(tempFileId)) return Optional.empty(); | ||
| try { | ||
| final var store = TemporaryAssetStorage.getInstance(); | ||
| final var record = store.receipt(tempFileId); | ||
| if (record.isPresent()) { | ||
| return store.retrieve(record.get(), accessingList).map(file -> new DotTempFile(tempFileId, file)); | ||
| } | ||
| if (store.isManagedLocally(tempFileId)) return Optional.empty(); | ||
| } catch (com.dotmarketing.exception.DotDataException e) { | ||
| throw new DotRuntimeException("Unable to retrieve temporary upload", e); | ||
| } | ||
| } | ||
| Optional<DotTempFile> tempFile = getTempFile(tempFileId); | ||
| if (tempFile.isPresent() && canUseTempFile(accessingList, tempFile.get())) { | ||
| return tempFile; | ||
|
|
@@ -405,6 +450,17 @@ public Optional<DotTempFile> getTempFile(final HttpServletRequest request, final | |
| * @return | ||
| */ | ||
| public boolean isTempResource(final String tempFileId) { | ||
| if (AssetStorageFeature.isEnabled()) { | ||
| if (!TemporaryAssetStorage.validId(tempFileId)) return false; | ||
| try { | ||
| final var store = TemporaryAssetStorage.getInstance(); | ||
| final var record = store.receipt(tempFileId); | ||
| if (record.isPresent()) return store.exists(record.get()); | ||
| if (store.isManagedLocally(tempFileId)) return false; | ||
| } catch (com.dotmarketing.exception.DotDataException e) { | ||
| throw new DotRuntimeException("Unable to check temporary upload", e); | ||
| } | ||
| } | ||
| return getTempFile(tempFileId).isPresent(); | ||
| } | ||
|
|
||
|
|
@@ -456,6 +512,13 @@ public String getRequestFingerprint(final HttpServletRequest request) { | |
| */ | ||
| public Optional<String> getTempResourceId(final File file){ | ||
| try { | ||
| if (AssetStorageFeature.isEnabled()) { | ||
| final var root = new File(com.dotmarketing.util.ConfigUtils.getAssetTempPath()).getCanonicalFile().toPath(); | ||
| final var path = file.getCanonicalFile().toPath(); | ||
| if (!path.startsWith(root) || root.relativize(path).getNameCount() < 2) return Optional.empty(); | ||
| final String id = root.relativize(path).getName(0).toString(); | ||
| return isTempResource(id) ? Optional.of(id) : Optional.empty(); | ||
| } | ||
| final String tempResourceId = file.toPath().getParent().getFileName().toString(); | ||
| if (isTempResource(tempResourceId)) { | ||
| return Optional.of(tempResourceId); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mark temp file managed only after completeTempFile succeeds, not during create
Current code:
Problem: The
.s3-uploadmarker is written at creation, before any receipt exists. If the upload never completes (client abort, orstore()fails on an S3 outage), the local bytes are fenced off:getTempFilereturns empty becauseisManagedLocallyis true butreceipt()is absent, and there is no legacy fallback. Until BinaryCleanupJob ages the file out, a valid local copy is unreadable even though the flag-off path would serve it. Consider writing the marker only afterstore()verifies the S3 copy (store() already writes it), or removing the marker instore()'s failure path.