Extending
The storage system provides several extension points for customizing file handling behavior. Implementations are discovered automatically via classpath scanning—simply implement the interface and it will be invoked.
Lifecycle hooks
Lifecycle hooks run during file upload (via StorageItemFilter) and delete operations. They are discovered automatically using ClassFinder.
Before save
Implement StorageItemBeforeSave to run logic before a file is persisted to storage. This is useful for validation, metadata enrichment, or transforming the file.
The StorageItemUploadPart parameter provides access to the original upload file and metadata.
1public class FileSizeBeforeSave implements StorageItemBeforeSave {23private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB45@Override6public void beforeSave(StorageItem storageItem, StorageItemUploadPart part) throws IOException {7if (part.getSize() > MAX_FILE_SIZE) {8throw new IOException("File size exceeds maximum allowed size of 10 MB");9}10}11}
After save
Implement StorageItemAfterSave to run logic after a file has been saved to storage. This is useful for logging, notifications, or triggering downstream processing.
1public class LoggingAfterSave implements StorageItemAfterSave {23private static final Logger LOGGER = LoggerFactory.getLogger(LoggingAfterSave.class);45@Override6public void afterSave(StorageItem storageItem) throws IOException {7LOGGER.info("Saved file [{}] to storage [{}]",8storageItem.getPath(),9storageItem.getStorage());10}11}
Before delete
Implement StorageItemBeforeDelete to run logic before a file is removed from storage:
1public class ValidationBeforeDelete implements StorageItemBeforeDelete {23@Override4public void beforeDelete(StorageItem storageItem) throws IOException {5String path = storageItem.getPath();6if (path != null && path.startsWith("protected/")) {7throw new IOException("Files in the protected/ directory cannot be deleted.");8}9}10}
After delete
Implement StorageItemAfterDelete to run logic after a file has been removed from storage:
1public class CleanupAfterDelete implements StorageItemAfterDelete {23private static final Logger LOGGER = LoggerFactory.getLogger(CleanupAfterDelete.class);45@Override6public void afterDelete(StorageItem storageItem) throws IOException {7LOGGER.info("Deleted file [{}] from storage [{}]",8storageItem.getPath(),9storageItem.getStorage());10}11}
Custom path generators
By default, uploaded files are stored with a path generated by RandomUuidStorageItemPathGenerator, which creates paths like ab/cd/ef1234.../filename.jpg.
To customize the path structure, implement StorageItemPathGenerator and register it in settings:
1public class DatePrefixPathGenerator implements StorageItemPathGenerator {23private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd");45@Override6public String createPath(String fullFileName) {7String datePath = LocalDate.now().format(FORMATTER);8String uniqueId = UUID.randomUUID().toString().replace("-", "");9String extension = "";1011int lastDotAt = fullFileName.lastIndexOf('.');12if (lastDotAt > -1) {13extension = fullFileName.substring(lastDotAt);14}1516return datePath + "/" + uniqueId + extension;17}18}
Register the path generator for a specific storage:
1dari/upload/{storageName}/pathGenerator=extending.DatePrefixPathGenerator
Replace {storageName} with the name of the storage back end (e.g., s3, azure). If no path generator is configured for a storage, RandomUuidStorageItemPathGenerator is used.
CDN integration
The Cdn class manages automatic synchronization of static files between the local environment and a content delivery network.
Getting CDN URLs
Use Cdn#getUrl() to get the CDN URL for a static resource:
1return Cdn.getUrl(servletPath);
In production (or when cms/isResourceInStorage is true), this uploads the file to the default storage and returns the CDN URL. In development, it returns the local path with a cache-busting timestamp.
CDN filters
Implement CdnFilter to modify a storage item before it's uploaded to the CDN:
1public class CacheHeaderCdnFilter implements CdnFilter {23@Override4public void update(StorageItem item) {5Map<String, Object> metadata = item.getMetadata();6if (metadata == null) {7metadata = new LinkedHashMap<>();8item.setMetadata(metadata);9}10metadata.put("http.header.Cache-Control", "public, max-age=31536000, immutable");11}12}
CDN context
Implement CdnContext to customize how the CDN resolves and reads files:
1public class CustomCdnContext implements CdnContext {23private final String pathPrefix;4private final ClassLoader classLoader;56public CustomCdnContext(String pathPrefix, ClassLoader classLoader) {7this.pathPrefix = pathPrefix;8this.classLoader = classLoader;9}1011@Override12public long getLastModified(String servletPath) throws IOException {13URL resource = classLoader.getResource(servletPath.substring(1));14if (resource == null) {15return -1;16}17URLConnection connection = resource.openConnection();18try {19return connection.getLastModified();20} finally {21connection.getInputStream().close();22}23}2425@Override26public InputStream open(String servletPath) throws IOException {27URL resource = classLoader.getResource(servletPath.substring(1));28if (resource == null) {29throw new IOException("Resource not found: " + servletPath);30}31return resource.openStream();32}3334@Override35public String getPathPrefix() {36return pathPrefix;37}3839@Override40public boolean equals(Object other) {41if (this == other) {42return true;43}44if (!(other instanceof CustomCdnContext)) {45return false;46}47CustomCdnContext that = (CustomCdnContext) other;48return Objects.equals(pathPrefix, that.pathPrefix)49&& Objects.equals(classLoader, that.classLoader);50}5152@Override53public int hashCode() {54return Objects.hash(pathPrefix, classLoader);55}56}
Custom hashing
The StorageItemHash interface allows you to control how files are distributed across multiple CDN base URLs.
The built-in _pathHashCode algorithm uses the hash code of the file path. To implement a custom algorithm:
- Implement
StorageItemHash:
1public class CustomStorageItemHash extends AbstractStorageItemHash {23@Override4public int hashStorageItem(StorageItem storageItem) {5String contentType = storageItem.getContentType();6return contentType != null ? contentType.hashCode() : 0;7}89@Override10public void initialize(String settingsKey, Map<String, Object> settings) {11// no additional settings required12}13}
- Register it in settings:
1dari/storageHash/myHash/class=com.example.CustomStorageItemHash
- Reference it from a storage back end:
1dari/storage/s3/hashAlgorithm=myHash
Origin URLs
Storage back ends that implement StorageItemOriginUrl can provide an origin URL that bypasses the CDN. This is used by OriginStorageItem to access files directly from the storage back end.
Configure the origin base URL on any supported back end:
1dari/storage/s3/originBaseUrl=https://my-bucket.s3.amazonaws.com
Access the origin URL programmatically:
1public String getOriginUrl(StorageItem item) {2return new OriginStorageItem(item).getPublicUrl();3}
The local filesystem, S3, and Azure back ends all support origin URLs.