Skip to main content

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.

1
public class FileSizeBeforeSave implements StorageItemBeforeSave {
2
3
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
4
5
@Override
6
public void beforeSave(StorageItem storageItem, StorageItemUploadPart part) throws IOException {
7
if (part.getSize() > MAX_FILE_SIZE) {
8
throw 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.

1
public class LoggingAfterSave implements StorageItemAfterSave {
2
3
private static final Logger LOGGER = LoggerFactory.getLogger(LoggingAfterSave.class);
4
5
@Override
6
public void afterSave(StorageItem storageItem) throws IOException {
7
LOGGER.info("Saved file [{}] to storage [{}]",
8
storageItem.getPath(),
9
storageItem.getStorage());
10
}
11
}

Before delete

Implement StorageItemBeforeDelete to run logic before a file is removed from storage:

1
public class ValidationBeforeDelete implements StorageItemBeforeDelete {
2
3
@Override
4
public void beforeDelete(StorageItem storageItem) throws IOException {
5
String path = storageItem.getPath();
6
if (path != null && path.startsWith("protected/")) {
7
throw 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:

1
public class CleanupAfterDelete implements StorageItemAfterDelete {
2
3
private static final Logger LOGGER = LoggerFactory.getLogger(CleanupAfterDelete.class);
4
5
@Override
6
public void afterDelete(StorageItem storageItem) throws IOException {
7
LOGGER.info("Deleted file [{}] from storage [{}]",
8
storageItem.getPath(),
9
storageItem.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:

1
public class DatePrefixPathGenerator implements StorageItemPathGenerator {
2
3
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd");
4
5
@Override
6
public String createPath(String fullFileName) {
7
String datePath = LocalDate.now().format(FORMATTER);
8
String uniqueId = UUID.randomUUID().toString().replace("-", "");
9
String extension = "";
10
11
int lastDotAt = fullFileName.lastIndexOf('.');
12
if (lastDotAt > -1) {
13
extension = fullFileName.substring(lastDotAt);
14
}
15
16
return datePath + "/" + uniqueId + extension;
17
}
18
}

Register the path generator for a specific storage:

1
dari/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:

1
return 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:

1
public class CacheHeaderCdnFilter implements CdnFilter {
2
3
@Override
4
public void update(StorageItem item) {
5
Map<String, Object> metadata = item.getMetadata();
6
if (metadata == null) {
7
metadata = new LinkedHashMap<>();
8
item.setMetadata(metadata);
9
}
10
metadata.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:

1
public class CustomCdnContext implements CdnContext {
2
3
private final String pathPrefix;
4
private final ClassLoader classLoader;
5
6
public CustomCdnContext(String pathPrefix, ClassLoader classLoader) {
7
this.pathPrefix = pathPrefix;
8
this.classLoader = classLoader;
9
}
10
11
@Override
12
public long getLastModified(String servletPath) throws IOException {
13
URL resource = classLoader.getResource(servletPath.substring(1));
14
if (resource == null) {
15
return -1;
16
}
17
URLConnection connection = resource.openConnection();
18
try {
19
return connection.getLastModified();
20
} finally {
21
connection.getInputStream().close();
22
}
23
}
24
25
@Override
26
public InputStream open(String servletPath) throws IOException {
27
URL resource = classLoader.getResource(servletPath.substring(1));
28
if (resource == null) {
29
throw new IOException("Resource not found: " + servletPath);
30
}
31
return resource.openStream();
32
}
33
34
@Override
35
public String getPathPrefix() {
36
return pathPrefix;
37
}
38
39
@Override
40
public boolean equals(Object other) {
41
if (this == other) {
42
return true;
43
}
44
if (!(other instanceof CustomCdnContext)) {
45
return false;
46
}
47
CustomCdnContext that = (CustomCdnContext) other;
48
return Objects.equals(pathPrefix, that.pathPrefix)
49
&& Objects.equals(classLoader, that.classLoader);
50
}
51
52
@Override
53
public int hashCode() {
54
return 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:

  1. Implement StorageItemHash:
1
public class CustomStorageItemHash extends AbstractStorageItemHash {
2
3
@Override
4
public int hashStorageItem(StorageItem storageItem) {
5
String contentType = storageItem.getContentType();
6
return contentType != null ? contentType.hashCode() : 0;
7
}
8
9
@Override
10
public void initialize(String settingsKey, Map<String, Object> settings) {
11
// no additional settings required
12
}
13
}
  1. Register it in settings:
1
dari/storageHash/myHash/class=com.example.CustomStorageItemHash
  1. Reference it from a storage back end:
1
dari/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:

1
dari/storage/s3/originBaseUrl=https://my-bucket.s3.amazonaws.com

Access the origin URL programmatically:

1
public String getOriginUrl(StorageItem item) {
2
return new OriginStorageItem(item).getPublicUrl();
3
}

The local filesystem, S3, and Azure back ends all support origin URLs.