MediaControllerService Usage Guide
Updated at 1782225554000MediaControllerService is the central service that orchestrates media operations: upload, replace, download, details retrieval, metadata update, file-size reduction, pagination, and deletion. It does not hard-code admin/user authorization rules. Instead, API controllers pass an access-check predicate, such as “this media belongs to the current user” or “this admin can access all media”.
General Architecture
MediaControllerService sits between API controllers and lower-level infrastructure services such as media storage, file system access, pagination, validation, event handling, and pluggable upload/download adapters.
flowchart TD
A[API Controller] --> B[MediaControllerService]
B --> C[MediaValidator]
B --> D[MediaService]
B --> E[MediaFileService]
B --> F[FileSystemManager]
B --> G[EventHandlerManager]
B --> H[MediaUpDownloaderManager]
H --> I[Custom MediaUpDownloader]
B --> J[ResourceRequestHandler]
The service prefers
MediaUpDownloader when the system is configured with an adapter that supports the requested operation. If no adapter is available, it falls back to the default behavior: storing files in the local file system, reading metadata with Tika, saving media records, and publishing events.Main Use Cases
MediaControllerService supports these operations:
- Add media from multipart upload.
- Add media from URL.
- Replace an existing media file.
- Download media by name.
- Get media details by id or name.
- Update metadata such as alternative text, title, caption, description, URL, and duration.
- Reduce file size.
- Delete media by id or name.
- List media with pagination and filters.
Upload Media
Controllers usually do not process uploaded files directly. They pass
HttpServletRequest and HttpServletResponse to the service. The service validates the file, stores it, creates a media record, and writes the JSON media model to the response.@Async @DoPost("/media/add") public void uploadMedia( HttpServletRequest request, HttpServletResponse response, @UserId long userId, @RequestParam("avatar") boolean avatar, @RequestParam("notPublic") boolean notPublic ) throws Exception { mediaControllerService.addMedia( request, response, "USER", 0L, userId, avatar, notPublic ); }
Main parameter meanings:
-
uploadFrom: the upload source, for exampleUSERorADMIN. -
ownerAdminId: the owning admin id; pass0when not applicable. -
ownerUserId: the owning user id; pass0when not applicable. -
avatar: iftrue, the file is treated as an avatar. -
notPublic: iftrue, the media is private.
Upload flow:
sequenceDiagram
participant Client
participant Controller
participant Service as MediaControllerService
participant Adapter as MediaUpDownloader
participant Validator as MediaValidator
participant FileSystem as File system
participant MediaService
participant EventBus as Event handler
Client->>Controller: POST multipart file
Controller->>Service: addMedia(...)
Service->>Adapter: check upload adapter
alt Adapter supports upload
Service->>Adapter: upload(arguments)
else Default upload
Service->>Validator: validate file part
Service->>EventBus: MediaUploadEvent
Service->>FileSystem: store file asynchronously
Service->>Service: reduce file size if enabled
Service->>MediaService: create media record
Service->>EventBus: MediaUploadedEvent
Service-->>Client: JSON media model
end
Uploaded files are validated with Tika to detect the actual MIME type. The service checks file name, extension, accepted MIME types, upload size limits, and image size limits. If the request marks the file as
avatar, the media type is treated as avatar.Replace Media
When replacing media, the controller should pass a predicate to ensure the caller can only replace media they are allowed to access.
@Async @DoPost("/media/{id}/replace") public void replaceMedia( HttpServletRequest request, HttpServletResponse response, @UserId long userId, @PathVariable long mediaId ) throws Exception { mediaControllerService.replaceMedia( request, response, mediaId, media -> media.getOwnerUserId() == userId ); }
When replacement succeeds:
- The service finds the existing media by id or name.
- It checks the access predicate.
- It validates the new file.
- It stores the new file.
- It reduces file size if enabled.
- It updates the media record with the new file information.
- It publishes
MediaReplacedEvent. - It writes the JSON media model to the response.
If the media does not exist or the predicate returns
false, the service treats it as media/resource not found.
Add Media From URL
There are two URL-based media flows.
For a normal URL metadata request, the service validates the request, generates a media name from the URL and type, then creates a media record. This is useful when the media already lives at an external URL and the system only needs to manage its metadata.
For the “save media file from URL” flow, the service downloads the file into storage. The URL must be non-blank and must be a valid external URL. If a
MediaUpDownloader supports upload-from-url, the service delegates to that adapter. Otherwise, it uses the HTTP client to download the file, detects MIME type with Tika, reduces file size if needed, stores the media record, and publishes the upload event.Note: the non-throwing variant catches exceptions, logs a warning, and returns
0. If the caller needs to handle errors directly or avoid logging sensitive URLs, use the orThrow variant.Download Media
Public media can be downloaded directly. For private media, the ownership predicate decides whether the caller may access it.
@Async @DoGet("/media/{name}") public void getMedia( @UserId Long userId, RequestArguments requestArguments, @PathVariable String name ) throws Exception { mediaControllerService.getMediaByName( requestArguments, userId, name, false, media -> userId != null && media.getOwnerUserId() == userId ); }
exposePrivateMedia = false means private media is returned only when the predicate is valid.Default download flow:
- Validate media name.
- Find media by name.
- Check whether the media is public or the caller can access private media.
- Publish owner validation and download events.
- Resolve the physical file by media type and media name.
- Stream the file to the response.
If a download adapter is configured and supports download, the service delegates to that adapter instead of streaming a local file.
Get Media Details
@DoGet("/media/{name}/details") public MediaDetailsResponse getMediaDetails( @UserId long userId, @PathVariable String name ) { return mediaControllerService.getWebMediaDetailsByName( name, media -> media.getOwnerUserId() == userId ); }
The service returns information such as name, media type, file size, width/height for images, and original-size file information when the media has been reduced before.
Details are resolved in this order:
- Let the media adapter return details.
- If the adapter returns nothing, publish a details event.
- If the event returns nothing, compute default details in the service.
Reduce File Size
@DoPost("/media/{id}/reduce-file-size") public MediaFileSizeReductionResponse reduceMediaFileSize( @UserId long userId, @PathVariable long mediaId, @RequestBody ReduceMediaFileSizeRequest request ) { MediaFileSizeReductionResult result = mediaControllerService.reduceMediaFileSizeById( mediaId, request.getExpectedFileSize(), media -> media.getOwnerUserId() == userId ); return modelToResponseConverter .toMediaFileSizeReductionResponse(result); }
File-size reduction flow:
flowchart TD
A[reduceMediaFileSize] --> B{Enabled by setting?}
B -- No --> C[Return empty result]
B -- Yes --> D{Adapter supports reduce?}
D -- Yes --> E[Call MediaUpDownloader.reduceMediaFileSize]
D -- No --> F[Publish MediaFileSizeReductionEvent]
F --> G{Event returns result?}
G -- Yes --> H[Use event result]
G -- No --> I[Call MediaFileService.reduceMediaFileSize]
After reducing file size by id or name, the service updates the media record with the final file, saves original-file information if available, saves the previous slug if the file name changed, and publishes
MediaFileSizeReducedEvent.Update Metadata
@DoPut("/media/{name}") public ResponseEntity updateMedia( @UserId long userId, @PathVariable String name, @RequestBody UpdateMediaRequest request ) { mediaControllerService.updateMedia( name, request, media -> media.getOwnerUserId() == userId ); return ResponseEntity.noContent(); }
Example request body:
{
"alternativeText": "User profile image",
"title": "Avatar",
"caption": "Profile photo",
"description": "Image uploaded from the account page"
}
Before updating, the request is validated for field length, including alternative text, title, caption, and description. For requests that include a URL, the URL is validated only when present. Duration is validated only when the caller asks to update duration.
One important detail: before converting the request into an update model, the service reads the current file size from the file system and sets it on the request. This keeps media metadata aligned with the stored file.
List Media
@DoGet("/media/list") public PaginationModel<MediaResponse> getMediaList( @UserId long userId, @RequestParam("type") String type, @RequestParam("keyword") String keyword, @RequestParam("sortOrder") String sortOrder, @RequestParam("nextPageToken") String nextPageToken, @RequestParam("prevPageToken") String prevPageToken, @RequestParam("lastPage") boolean lastPage, @RequestParam(value = "limit", defaultValue = "30") int limit ) { return mediaControllerService.getMediaList( DefaultMediaFilter.builder() .ownerUserId(userId) .type(trimOrNull(type)) .prefixKeyword(trimOrNull(keyword)) .likeKeyword(trimOrNull(keyword)) .build(), sortOrder, nextPageToken, prevPageToken, lastPage, limit ); }
Controllers usually build filters based on the current context:
- Web users filter by
ownerUserId. - Admins with global access can view all media.
- Admins without global access are restricted by
ownerAdminId. - Filters can include group, type, status, keyword, and creation time range.
- If status is not provided, controllers usually exclude statuses that should not be displayed.
- Page size is validated before querying.
Delete Media
@DoDelete("/media/{name}") public ResponseEntity deleteMedia( @UserId long userId, @PathVariable String name ) { mediaValidator.validateOwnerUserMedia(userId, name); mediaControllerService.removeMediaByName(name); return ResponseEntity.noContent(); }
Validate ownership before deletion. The service supports two deletion levels:
- Logical deletion: update media status through the media service.
- Permanent deletion: if enabled by setting, and the media is already in deleted status, permanently remove the record and delete the physical file.
After each deletion, the service publishes
MediaRemovedEvent.
Security And Authorization
MediaControllerService does not hard-code admin or user roles. Instead, API controllers pass a predicate that checks whether the current media is valid for the caller.
flowchart LR
A[Controller knows caller] --> B[Create access predicate]
B --> C[MediaControllerService]
C --> D{Media exists and predicate passes?}
D -- Yes --> E[Perform operation]
D -- No --> F[Return not found]
This keeps the service reusable across admin and web APIs, while avoiding disclosure of media that the caller is not allowed to access.
Integrate External Storage With MediaUpDownloader
To upload/download through S3, CDN, or another media service, implement
MediaUpDownloader.@EzySingleton public class CloudMediaUpDownloader implements MediaUpDownloader { @Override public String getName() { return "cloud"; } @Override public boolean isUploadSupported() { return true; } @Override public void upload(MediaUploadArguments arguments) { // Read the file from the request, upload it to cloud storage, // create/update the media record, and write the response. } @Override public boolean isDownloadSupported() { return true; } @Override public void download(MediaDownloadArguments arguments) { // Stream the file from cloud storage or redirect to a CDN URL. } @Override public boolean isReduceMediaSupported() { return true; } @Override public MediaFileSizeReductionResult reduceMediaFileSize( MediaFileSizeReductionArguments arguments ) { // Call a custom image/video optimization pipeline. return MediaFileSizeReductionResult.NO; } }
Then configure the media uploader/downloader setting to point to
"cloud". MediaControllerService will prefer this adapter instead of local file-system behavior for operations the adapter supports.Extension Through Events
Besides adapters, the service publishes events at important processing points:
- Before upload.
- After upload.
- During download.
- During file-size reduction.
- After replace.
- After metadata update.
- After deletion.
- When resolving file path or media details.
This allows the system to add behavior such as audit logs, CDN synchronization, image processing, advanced ownership checks, or statistics collection without pushing extra logic into controllers.
Important Limitations
The service does not grant permissions by itself; callers must pass the correct access predicate.
The service does not guarantee that every media record has a physical file. During download, if the record exists but the file is missing, the service still returns media not found.
File-size reduction only runs when enabled by setting. If no adapter handles it and no event handler returns a result, the default media file service is used.
Default multipart upload requires
FileUploader. If file upload is disabled, the service returns a not acceptable error.For the non-throwing save-from-url variant, download errors are swallowed and the result is
0, so callers should choose the correct variant depending on how they want to handle errors.Conclusion
MediaControllerService is the orchestration layer for the full media lifecycle. It brings validation, storage, metadata handling, events, pluggable adapters, and response writing into one consistent flow, while leaving authorization decisions to admin or web controllers.
This design makes the media API easier to extend, especially when integrating external storage, CDNs, image optimization pipelines, or custom download behavior.