diff --git a/.pipeline/config.yml b/.pipeline/config.yml
index e31529d3..aef9a8da 100644
--- a/.pipeline/config.yml
+++ b/.pipeline/config.yml
@@ -36,4 +36,4 @@ steps:
- sonar.java.source=17
- sonar.exclusions=**/node_modules/**,**/target/**,**/test/**
- sonar.coverage.jacoco.xmlReportPaths=cds-feature-attachments/target/site/jacoco/jacoco.xml
- - sonar.coverage.exclusions=cds-feature-attachments/src/test/**,cds-feature-attachments/src/gen/**,integration-tests/**
+ - sonar.coverage.exclusions=cds-feature-attachments/src/test/**,cds-feature-attachments/src/gen/**,integration-tests/**,examples/**
diff --git a/cds-feature-attachments/pom.xml b/cds-feature-attachments/pom.xml
index 59d3c7c6..43cdb1a3 100644
--- a/cds-feature-attachments/pom.xml
+++ b/cds-feature-attachments/pom.xml
@@ -78,13 +78,11 @@
ch.qos.logback
logback-classic
- 1.5.18
test
org.awaitility
awaitility
- 4.3.0
test
diff --git a/examples/cds-feature-attachments-fs/README.md b/examples/cds-feature-attachments-fs/README.md
new file mode 100644
index 00000000..02580881
--- /dev/null
+++ b/examples/cds-feature-attachments-fs/README.md
@@ -0,0 +1,7 @@
+### General Info
+
+This artifact demonstrates how to implement a custom storage for the cds-feature-attachments. This is just a sample implementation using a filessystem as content storage. It's not supposed for productive usage.
+
+### Implementation details
+
+This artifact provides a custom handlers for events from the [AttachmentService](../cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/AttachmentService.java).
\ No newline at end of file
diff --git a/examples/cds-feature-attachments-fs/pom.xml b/examples/cds-feature-attachments-fs/pom.xml
new file mode 100644
index 00000000..73f678c0
--- /dev/null
+++ b/examples/cds-feature-attachments-fs/pom.xml
@@ -0,0 +1,64 @@
+
+ 4.0.0
+
+
+ com.sap.cds
+ cds-feature-attachments-root
+ ${revision}
+ ../..
+
+
+ cds-feature-attachments-fs
+ jar
+
+ CDS Feature for Attachments - Filesystem
+ https://cap.cloud.sap/docs/plugins/#attachments
+
+
+
+ com.sap.cds
+ cds-feature-attachments
+
+
+
+
+ com.sap.cds
+ cds-services-impl
+ test
+
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+
+
+
+
+
+ com.sap.cds
+ cds-maven-plugin
+
+
+ cds.resolve
+
+ resolve
+
+
+
+
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+
+ true
+
+
+
+
+
+
+
diff --git a/examples/cds-feature-attachments-fs/src/main/java/com/sap/cds/feature/attachments/fs/configuration/Registration.java b/examples/cds-feature-attachments-fs/src/main/java/com/sap/cds/feature/attachments/fs/configuration/Registration.java
new file mode 100644
index 00000000..446e4071
--- /dev/null
+++ b/examples/cds-feature-attachments-fs/src/main/java/com/sap/cds/feature/attachments/fs/configuration/Registration.java
@@ -0,0 +1,31 @@
+package com.sap.cds.feature.attachments.fs.configuration;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+import org.apache.commons.io.FileUtils;
+
+import com.sap.cds.feature.attachments.fs.handler.FSAttachmentsServiceHandler;
+import com.sap.cds.feature.attachments.fs.handler.FSDraftServiceHandler;
+import com.sap.cds.services.runtime.CdsRuntimeConfiguration;
+import com.sap.cds.services.runtime.CdsRuntimeConfigurer;
+
+/**
+ * The class registers the event handlers for the attachments feature based on filesystem.
+ */
+public class Registration implements CdsRuntimeConfiguration {
+
+ @Override
+ public void eventHandlers(CdsRuntimeConfigurer configurer) {
+ Path tmpPath = FileUtils.getTempDirectory().toPath();
+ Path rootFolder = tmpPath.resolve("com.sap.cds.cds-feature-attachments-fs");
+
+ try {
+ configurer.eventHandler(new FSAttachmentsServiceHandler(rootFolder));
+ configurer.eventHandler(new FSDraftServiceHandler());
+ } catch (IOException e) {
+ throw new IllegalStateException("Error while creating the FSAttachmentsServiceHandler", e);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/examples/cds-feature-attachments-fs/src/main/java/com/sap/cds/feature/attachments/fs/handler/FSAttachmentsServiceHandler.java b/examples/cds-feature-attachments-fs/src/main/java/com/sap/cds/feature/attachments/fs/handler/FSAttachmentsServiceHandler.java
new file mode 100644
index 00000000..797b0bf6
--- /dev/null
+++ b/examples/cds-feature-attachments-fs/src/main/java/com/sap/cds/feature/attachments/fs/handler/FSAttachmentsServiceHandler.java
@@ -0,0 +1,118 @@
+/**************************************************************************
+ * (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
+ **************************************************************************/
+package com.sap.cds.feature.attachments.fs.handler;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.commons.io.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments;
+import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData;
+import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode;
+import com.sap.cds.feature.attachments.service.AttachmentService;
+import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext;
+import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext;
+import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext;
+import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext;
+import com.sap.cds.services.EventContext;
+import com.sap.cds.services.handler.EventHandler;
+import com.sap.cds.services.handler.annotations.On;
+import com.sap.cds.services.handler.annotations.ServiceName;
+
+/**
+ * This class is an event handler that is called when an attachment is created, marked as deleted, restored or read.
+ */
+@ServiceName(value = "*", type = AttachmentService.class)
+public class FSAttachmentsServiceHandler implements EventHandler {
+
+ private static final Logger logger = LoggerFactory.getLogger(FSAttachmentsServiceHandler.class);
+
+ private final Path rootFolder;
+
+ /**
+ * Creates a new FSAttachmentsServiceHandler with the given root folder.
+ *
+ * @param rootFolder the root folder where the attachments are stored
+ * @throws IOException if the root folder cannot be created
+ */
+ public FSAttachmentsServiceHandler(Path rootFolder) throws IOException {
+ this.rootFolder = rootFolder;
+ if (!Files.exists(this.rootFolder)) {
+ Files.createDirectories(this.rootFolder);
+ }
+ }
+
+ @On
+ void createAttachment(AttachmentCreateEventContext context) throws IOException {
+ logger.info("FS Attachment Service handler called for creating attachment for entity name: {}",
+ context.getAttachmentEntity().getQualifiedName());
+
+ String contentId = (String) context.getAttachmentIds().get(Attachments.ID);
+
+ MediaData data = context.getData();
+ data.setStatus(StatusCode.CLEAN);
+
+ try (InputStream input = data.getContent()) {
+ Path contentPath = getContentPath(context, contentId);
+ Files.createDirectories(contentPath.getParent());
+ Files.copy(input, contentPath);
+
+ context.setIsInternalStored(false);
+ context.setContentId(contentId);
+ context.setCompleted();
+ }
+ }
+
+ @On
+ void markAttachmentAsDeleted(AttachmentMarkAsDeletedEventContext context) throws IOException {
+ logger.info("Marking attachment as deleted with document id: {}", context.getContentId());
+
+ Path contenPath = getContentPath(context, context.getContentId());
+ Path parent = contenPath.getParent();
+ Path destPath = getDeletedFolder(context).resolve(parent.getFileName());
+
+ FileUtils.moveDirectory(parent.toFile(), destPath.toFile());
+ context.setCompleted();
+ }
+
+ @On
+ void restoreAttachment(AttachmentRestoreEventContext context) {
+ logger.info("FS Attachment Service handler called for restoring attachment for timestamp: {}",
+ context.getRestoreTimestamp());
+
+ // nothing to do as data are stored in the database and handled by the database
+ context.setCompleted();
+ }
+
+ @On
+ void readAttachment(AttachmentReadEventContext context) throws IOException {
+ logger.info("FS Attachment Service handler called for reading attachment with document id: {}",
+ context.getContentId());
+ InputStream fileInputStream = Files.newInputStream(getContentPath(context, context.getContentId()));
+ context.getData().setContent(fileInputStream);
+ context.setCompleted();
+ }
+
+ private Path getContentPath(EventContext context, String contentId) {
+ return this.rootFolder.resolve("%s/%s/content.bin".formatted(getTenant(context), contentId));
+ }
+
+ private Path getDeletedFolder(EventContext context) {
+ return this.rootFolder.resolve("%s/deleted".formatted(getTenant(context)));
+ }
+
+ private static String getTenant(EventContext context) {
+ String tenant = context.getUserInfo().getTenant();
+ if (tenant == null) {
+ tenant = "default";
+ }
+ return tenant;
+ }
+
+}
diff --git a/examples/cds-feature-attachments-fs/src/main/java/com/sap/cds/feature/attachments/fs/handler/FSDraftServiceHandler.java b/examples/cds-feature-attachments-fs/src/main/java/com/sap/cds/feature/attachments/fs/handler/FSDraftServiceHandler.java
new file mode 100644
index 00000000..64df231d
--- /dev/null
+++ b/examples/cds-feature-attachments-fs/src/main/java/com/sap/cds/feature/attachments/fs/handler/FSDraftServiceHandler.java
@@ -0,0 +1,88 @@
+/**************************************************************************
+ * (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
+ **************************************************************************/
+package com.sap.cds.feature.attachments.fs.handler;
+
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Optional;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.sap.cds.CdsData;
+import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments;
+import com.sap.cds.feature.attachments.handler.common.ApplicationHandlerHelper;
+import com.sap.cds.reflect.CdsAssociationType;
+import com.sap.cds.reflect.CdsElement;
+import com.sap.cds.reflect.CdsEntity;
+import com.sap.cds.services.draft.DraftCreateEventContext;
+import com.sap.cds.services.draft.DraftPatchEventContext;
+import com.sap.cds.services.draft.DraftService;
+import com.sap.cds.services.handler.EventHandler;
+import com.sap.cds.services.handler.annotations.Before;
+import com.sap.cds.services.handler.annotations.HandlerOrder;
+import com.sap.cds.services.handler.annotations.ServiceName;
+
+/**
+ * Event handler for events on the DraftService.
+ */
+@ServiceName(value = "*", type = DraftService.class)
+public class FSDraftServiceHandler implements EventHandler {
+
+ private static final Logger logger = LoggerFactory.getLogger(FSDraftServiceHandler.class);
+
+ @Before
+ @HandlerOrder(HandlerOrder.LATE)
+ void createDraftAttachment(DraftCreateEventContext context, CdsData data) {
+ CdsEntity target = context.getTarget();
+
+ // check if target entity contains aspect Attachments
+ if (ApplicationHandlerHelper.isMediaEntity(target)) {
+ String fileName = (String) data.get(Attachments.FILE_NAME);
+
+ // guessing the MIME type of the attachment based on the file name
+ String mimeType = URLConnection.guessContentTypeFromName(fileName);
+ data.put(Attachments.MIME_TYPE, mimeType);
+
+ // get unique identifier of attachment's parent entity, e.g. the Books entity
+ Parent parent = getParentId(target, data);
+
+ logger.info("Creating draft attachment '{}' for parent entity '{}' with ids {}", fileName,
+ parent != null ? parent.entity : "unknown", parent != null ? parent.ids : "unknown");
+
+ // do something with the data of the draft attachments entity
+ }
+ }
+
+ @Before
+ @HandlerOrder(HandlerOrder.LATE)
+ void patchDraftAttachment(DraftPatchEventContext context, CdsData data) {
+ CdsEntity target = context.getTarget();
+
+ // check if target entity contains aspect Attachments
+ if (ApplicationHandlerHelper.isMediaEntity(target)) {
+ // remove wrong mime type from data
+ // TODO: remove this once the SAPUI5 sets the correct MIME type
+ data.remove(Attachments.MIME_TYPE);
+ }
+ }
+
+ private static Parent getParentId(CdsEntity target, CdsData data) {
+ // find association to parent entity
+ Optional upAssociation = target.findAssociation("up_");
+
+ // if association is found, try to get foreign key to parent entity
+ if (upAssociation.isPresent()) {
+ // get association type
+ CdsAssociationType assocType = upAssociation.get().getType();
+ // get the refs of the association and map them to the corresponding data of the entity
+ List