feat(rest): add /api/v2/assets endpoint for reading and writing file assets#36112
feat(rest): add /api/v2/assets endpoint for reading and writing file assets#36112fmontes wants to merge 6 commits into
Conversation
…assets
Adds a purpose-built v2 REST surface for file assets, addressable by
host-qualified path (//host/...) or identifier, with publish intent
expressed in the URL instead of request-body flags:
- GET /api/v2/assets?path=//host/... and GET /api/v2/assets/{identifier}
stream raw bytes (explicit content type + Content-Disposition, no
content negotiation), defaulting to working version / default language
- PUT /api/v2/assets/save writes the working version only; the live
version keeps serving its bytes until PUT /api/v2/assets/publish
- Flat multipart (file, path, language) — no nested JSON detail blob
- Write responses include the persisted fileSize so callers can assert
what actually landed
- Zero-byte uploads rejected with 400: distinct messages for a missing
file part vs a present-but-empty one
- Clean error statuses throughout: unknown host / path / language
version 404, invalid request 400, oversize 400, no permission 403
- Identifier reads enforce READ permission before serving bytes
Business logic stays in v1's WebAssetHelper (only parseLang visibility
widened); /api/v1/assets contract is unchanged.
Includes integration tests (WebAssetResourceV2IntegrationTest), a v2
folder in the WebAssets Postman collection, and regenerated OpenAPI spec.
Closes #35928
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @fmontes's task in 2m 19s —— View job Code Review
Issues Found1.
final String resolvedLang = resolveLanguage(language).toString();The resolved final String resolvedLang = lang.getLanguageCode() + "-" + lang.getCountryCode();or use the language ID directly. The integration tests currently pass null language (defaults), so this path may not be exercised under test. Fix this → 2.
if (!(view instanceof AssetView)) {
throw new IllegalStateException(
"Unexpected view type returned by saveUpdateAsset: " + view.getClass().getName());
}
3. Permission test mutates shared folder state without cleanup (test reliability, low-medium)
final PermissionAPI permissionAPI = APILocator.getPermissionAPI();
permissionAPI.resetPermissionsUnder(folder);
4. Dead variable
final File f2 = tmpFile(f1.getName().replace(".txt", ""), ".txt", content2);
5. Path validation before authentication in
6. REST convention is 201 on create, 200 on update. The helper doesn't currently distinguish, so this would need changes downstream. Noting for awareness — not a blocker. Clean
|
There was a problem hiding this comment.
Pull request overview
Adds a new /api/v2/assets REST surface for streaming raw file-asset bytes (by host-qualified path or identifier) and for writing assets via a simplified multipart contract, while reusing the existing v1 WebAssetHelper business logic to keep /api/v1/assets contract-stable (CLI-safe).
Changes:
- Introduces
WebAssetResourceV2with GET-by-path/identifier and PUT save/publish endpoints, plus dedicated response/view models that include persistedfileSize. - Updates OpenAPI spec to document the new v2 endpoints and schemas.
- Adds Postman coverage and a new Java integration test suite validating key contract guarantees and error cases.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml | Adds “File Assets” tag and documents /v2/assets endpoints + response schemas. |
| dotCMS/src/main/java/com/dotcms/rest/api/v2/asset/WebAssetResourceV2.java | New v2 resource implementing streaming reads and multipart writes reusing v1 helper logic. |
| dotCMS/src/main/java/com/dotcms/rest/api/v2/asset/ResponseEntityFileAssetView.java | Concrete ResponseEntityView wrapper for OpenAPI/schema accuracy. |
| dotCMS/src/main/java/com/dotcms/rest/api/v2/asset/FileAssetView.java | New lightweight write-response view including fileSize. |
| dotCMS/src/main/java/com/dotcms/rest/api/v1/asset/WebAssetHelper.java | Makes parseLang public to reuse language parsing from v2. |
| dotcms-postman/src/main/resources/postman/WebAssets.postman_collection.json | Adds a v2 Postman folder covering success and error-status cases. |
| dotcms-integration/src/test/java/com/dotcms/rest/api/v2/asset/WebAssetResourceV2IntegrationTest.java | New integration tests validating read/write semantics, permissions, and validation behavior. |
RestEndpointAnnotationComplianceTest flagged the new resource: - @tag description removed from the class (descriptions belong in DotRestApplication, where the File Assets tag is already defined) - save/publish @operation now declare an operation-level @RequestBody with a multipart/form-data schema, per the multipart documentation standard Regenerated openapi.yaml accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Reject malformed multipart where the file stream is present but the Content-Disposition is missing (400 instead of NPE -> 500) - Encode the filename in the Content-Disposition response header (UtilMethods.encodeURL, matching BinaryExporterServlet) to prevent invalid headers / header injection from stored filenames - Validate the version query param: only 'working' and 'live' accepted, anything else is 400 instead of silently selecting working - Stop swallowing lookup exceptions in findContentlet: DotDataException and DotSecurityException now propagate to their mappers instead of being masked as 404 Adds integration tests for the invalid-version and missing-disposition 400s; regenerated openapi.yaml. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collapse resolveLanguageParam (returned tag String) and resolveLanguageId (returned long id) into a single resolveLanguage() that returns the resolved Language. Call sites derive the tag (path reads/writes) or id (identifier reads) as needed. Removes the duplicated parseLang/blank-default logic and the two slightly-divergent "Unknown language tag" messages. No behavior change; openapi.yaml unchanged (internal refactor only). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Quality cleanup, no behavior change: - Use the shared MimeTypeUtils.getMimeType(File) instead of a hand-rolled URLConnection.guessContentTypeFromName fallback (better detection chain, fewer lines) - Collapse the duplicated WebResource.InitBuilder boilerplate (repeated in all four endpoints) into a single authenticate() helper so the auth policy stays consistent - Avoid the double file stat in buildFileResponse (exists() + length() -> one length() read) Verified: core build, RestEndpointAnnotationComplianceTest, and dotcms-integration test-compile all pass; openapi.yaml unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- getById: check isFileAsset() before the READ-permission check so a non-file identifier always returns the contract-required 404 regardless of the caller's permission, instead of a 403 - Remove the unused java.io.Serializable import - Remove the dead assetPath2() test helper Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Proposed Changes
Closes #35928
Adds a clean v2 REST surface for reading and writing dotCMS file assets (
.vtl,.dotsass,.css,.js, …) in place, addressable by host-qualified path or identifier. All business logic stays in v1'sWebAssetHelper;/api/v1/assets(which backs the CLI) is contract-unchanged.New endpoints (
com.dotcms.rest.api.v2.asset.WebAssetResourceV2):GET /api/v2/assets?path=//host/...language,version=working|livequery params)GET /api/v2/assets/{identifier}PUT /api/v2/assets/savePUT /api/v2/assets/publishWrites use a flat multipart (
file,path, optionallanguage) — no nested JSONdetailblob, publish intent lives in the URL. Reads stream raw bytes with explicit content type andContent-Disposition(no content-negotiation 406s, no large-file truncation).Contract guarantees
fileSizeso callers can assert what actually landed (closes the silent-success failure mode that once published a 0-bytetemplate.vtl)filepart vs a present-but-empty oneChecklist
WebAssetResourceV2IntegrationTest(round-trip reads, save-preserves-live/publish-promotes, both zero-byte 400s, unknown language, permission 403, folder-path 400, unknown host 404,fileSizeassertion,.dotsass)v2folder inWebAssets.postman_collection.json(8 requests incl. error-status cases)openapi.yamlNotes for the reviewer
WebAssetHelper.parseLangwidened from package-private topublic(additive, no behavior change)DotContentletStateException(core's 0-length checkin guard) already maps to 400 via the existingDotStateExceptionMapper; the resource-level zero-byte check makes the message explicit and distinctIllegalArgumentExceptionbody includes a stacktrace (same generic mapper used elsewhere) — can be tightened in a follow-up🤖 Generated with Claude Code