Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3402e55
Started with implementation of Azure blob storage
KoblerS Sep 23, 2025
5f9f2dc
Merge branch 'main' into multi-cloud-provider-support
KoblerS Oct 10, 2025
30b9efc
Refactor Azure Blob Storage service to improve logging and error hand…
KoblerS Oct 10, 2025
0c9b1e7
Add Azure Blob Storage settings and refactor tenant ID retrieval in A…
KoblerS Oct 10, 2025
5e6939a
Refactor AzureAttachmentsService to improve content handling in the p…
KoblerS Oct 10, 2025
d902358
Update error message for missing Azure Blob Storage credentials in Az…
KoblerS Oct 10, 2025
5b810b3
Add retry logic for Cloud Foundry login in deployment action (#262)
KoblerS Oct 10, 2025
6dfa7c4
258 chore remove cross dependency from tests (#259)
Schmarvinius Oct 10, 2025
7668ca5
Enhance AWS Attachments Service with detailed JSDoc comments and impr…
KoblerS Oct 16, 2025
822f2f6
Update translation_v2.json (#265)
schiwekM Oct 13, 2025
668c4e5
Added missing descr column (#267)
schiwekM Oct 13, 2025
fbcd591
Prepare release notes (#266)
KoblerS Oct 14, 2025
c0a71f6
Enhance memory management and error handling in attachment processing…
KoblerS Oct 16, 2025
daf9fc5
Refactor Azure Blob Storage and malware scanner integration for impro…
KoblerS Oct 16, 2025
80ea57e
Merge branch 'main' into 69-support-for-azure-blob-storage
KoblerS Oct 16, 2025
c06c518
Improve error handling for credential validation in AWS S3 and Azure …
KoblerS Oct 17, 2025
261e78c
Add placeholder for deleteInfectedAttachment method in AWS and Azure …
KoblerS Oct 17, 2025
5c23870
Add CODEOWNERS file to define repository ownership (#274)
KoblerS Oct 17, 2025
eb280a1
Merge branch 'main' into 69-support-for-azure-blob-storage
KoblerS Oct 20, 2025
ad4466e
Refactor AzureAttachmentsService: remove unused attachment deletion m…
KoblerS Oct 20, 2025
fbdd785
[268]- Fix/Delete cascading attachments when deleting parent (#269)
vlovini Oct 20, 2025
fbd3527
Only have one sample in the plugin repo (#277)
schiwekM Oct 20, 2025
db284be
Update version to 3.1.0 and document changes in CHANGELOG (#278)
KoblerS Oct 20, 2025
0d362c1
Overhaul and Cleanup of the README File (#250)
eric-pSAP Oct 24, 2025
a0f2589
Enhance Azure Blob Storage support and update README documentation
KoblerS Oct 27, 2025
1b356b3
Merge branch 'main' into 69-support-for-azure-blob-storage
schiwekM Oct 27, 2025
4409e58
Fix hybrid configuration to use S3 for attachments instead of Azure
KoblerS Oct 28, 2025
ff2fa09
Refactor Azure Blob Storage client initialization and add cleanup fun…
KoblerS Oct 28, 2025
a706ac7
Merge branch 'main' into 69-support-for-azure-blob-storage
KoblerS Oct 28, 2025
a4cd73f
Update AWS SDK dependencies to latest versions
KoblerS Oct 28, 2025
8c1f81c
Update lib/azure-blob-storage.js
KoblerS Oct 28, 2025
47700a6
Refactor malware scan initiation for AWS and Azure attachment services
KoblerS Oct 28, 2025
a172f93
Refactor AWS and Azure object store credential retrieval by centraliz…
KoblerS Oct 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"editor.tabSize": 2,
"editor.indentSize": 2
}
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,15 +329,12 @@ resources:
The unit tests in this module do not need a binding to the respective object stores, run them with `npm install`. To achieve a clean install, the command `rm -rf node_modules` should be used before installation.

The integration tests need a binding to a real object store. Run them with `npm run test`.
To set the binding, provide the following environment variables:
- AWS_S3_BUCKET
- AWS_S3_REGION
- AWS_S3_ACCESS_KEY_ID
- AWS_S3_SECRET_ACCESS_KEY
To set the binding, please see the section [Storage Targets](#storage-targets).

##### Supported Storage Backends
##### Supported Storage Provider

- **AWS S3**
- **Azure Blob Storage**

### Model Texts

Expand Down
81 changes: 29 additions & 52 deletions lib/aws-s3.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ module.exports = class AWSAttachmentsService extends require("./basic") {
const creds = cds.env.requires?.objectStore?.credentials

if (!creds) {
logConfig.configValidation('objectStore.credentials', creds, false,
'Bind an SAP Object Store instance to your application or configure separateObjectStore for multitenancy')
if (Object.keys(creds).includes('container_name')) {
throw new Error('Azure Blob Storage credentials found where AWS S3 credentials expected, please check your service bindings.')
}
throw new Error("SAP Object Store instance is not bound.")
}

Expand All @@ -46,8 +47,9 @@ module.exports = class AWSAttachmentsService extends require("./basic") {
const missingFields = requiredFields.filter(field => !creds[field])

if (missingFields.length > 0) {
logConfig.configValidation('objectStore.credentials', creds, false,
`Object Store credentials missing: ${missingFields.join(', ')}`)
if (Object.keys(creds).includes('container_name')) {
throw new Error('Azure Blob Storage credentials found where AWS S3 credentials expected, please check your service bindings.')
}
throw new Error(`Missing Object Store credentials: ${missingFields.join(', ')}`)
}

Expand Down Expand Up @@ -101,31 +103,8 @@ module.exports = class AWSAttachmentsService extends require("./basic") {
return
}

// Validate Service Manager configuration
const serviceManagerCreds = cds.env.requires?.serviceManager?.credentials
if (!serviceManagerCreds) {
logConfig.configValidation('serviceManager.credentials', serviceManagerCreds, false,
'Bind a Service Manager instance for separate object store mode')
throw new Error("Service Manager Instance is not bound")
}

const { sm_url, url, clientid, clientsecret, certificate, key, certurl } = serviceManagerCreds

// Validate required Service Manager fields
const requiredSmFields = ['sm_url', 'url', 'clientid']
const missingSmFields = requiredSmFields.filter(field => !serviceManagerCreds[field])

if (missingSmFields.length > 0) {
logConfig.configValidation('serviceManager.credentials', serviceManagerCreds, false,
`Service Manager credentials missing: ${missingSmFields.join(', ')}`)
throw new Error(`Missing Service Manager credentials: ${missingSmFields.join(', ')}`)
}

logConfig.debug('Fetching access token for tenant', { tenantID, sm_url })
const token = await utils.fetchToken(url, clientid, clientsecret, certificate, key, certurl)

logConfig.debug('Fetching object store credentials for tenant', { tenantID })
const objectStoreCreds = await utils.getObjectStoreCredentials(tenantID, sm_url, token)
const objectStoreCreds = await utils.getObjectStoreCredentials(tenantID)

if (!objectStoreCreds) {
logConfig.withSuggestion('error',
Expand Down Expand Up @@ -272,24 +251,21 @@ module.exports = class AWSAttachmentsService extends require("./basic") {
})

// Initiate malware scan if configured
if (this.kind === 's3') {
logConfig.debug('Initiating malware scan for uploaded file', {
fileId: metadata.ID,
filename: metadata.filename
})

const scanRequestJob = cds.spawn(async () => {
await scanRequest(attachments, { ID: metadata.ID })
})
logConfig.debug('Initiating malware scan for uploaded file', {
fileId: metadata.ID,
filename: metadata.filename
})

scanRequestJob.on('error', (err) => {
logConfig.withSuggestion('error',
'Failed to initiate malware scan for attachment', err,
'Check malware scanner configuration and connectivity',
{ fileId: metadata.ID, filename: metadata.filename, errorMessage: err.message })
})
}
const scanRequestJob = cds.spawn(async () => {
await scanRequest(attachments, { ID: metadata.ID })
})

scanRequestJob.on('error', (err) => {
logConfig.withSuggestion('error',
'Failed to initiate malware scan for attachment', err,
'Check malware scanner configuration and connectivity',
{ fileId: metadata.ID, filename: metadata.filename, errorMessage: err.message })
})
} catch (err) {
const duration = Date.now() - startTime
logConfig.withSuggestion('error',
Expand Down Expand Up @@ -385,21 +361,21 @@ module.exports = class AWSAttachmentsService extends require("./basic") {
* @param {import('express').NextFunction} next - The next middleware function
*/
async updateContentHandler(req, next) {
logConfig.debug(`[S3 Upload] Uploading file using updateContentHandler for ${req.target.name}`)
logConfig.debug(`[AWS S3] Uploading file using updateContentHandler for ${req.target.name}`)

// Check separate object store instances
if (separateObjectStore) {
const tenantID = cds.context.tenant
await this.createClientS3(tenantID)
}

const targetID = req.data.ID || req.params[1]?.ID || req.params[1];
const targetID = req.data.ID || req.params[1]?.ID || req.params[1]
if (!targetID) {
req.reject(400, "Missing ID in request");
req.reject(400, "Missing ID in request")
}

if (req?.data?.content) {
const response = await SELECT.from(req.target, { ID: targetID }).columns("url");
const response = await SELECT.from(req.target, { ID: targetID }).columns("url")
if (response?.url) {
const Key = response.url
const input = {
Expand All @@ -416,7 +392,7 @@ module.exports = class AWSAttachmentsService extends require("./basic") {
const keys = { ID: targetID }

const scanRequestJob = cds.spawn(async () => {
await scanRequest(req.target, keys, req)
await scanRequest(req.target, keys)
})

scanRequestJob.on('error', async (err) => {
Expand All @@ -426,12 +402,12 @@ module.exports = class AWSAttachmentsService extends require("./basic") {
{ keys, errorMessage: err.message })
})

logConfig.debug(`[S3 Upload] Uploaded file using updateContentHandler for ${req.target.name}`)
logConfig.debug(`[AWS S3] Uploaded file using updateContentHandler for ${req.target.name}`)
}
} else if (req?.data?.note) {
const key = { ID: targetID }
await super.update(req.target, key, { note: req.data.note })
logConfig.debug(`[S3 Upload] Updated file upload with note for ${req.target.name}`)
logConfig.debug(`[AWS S3] Updated file upload with note for ${req.target.name}`)
} else {
next()
}
Expand Down Expand Up @@ -488,7 +464,7 @@ module.exports = class AWSAttachmentsService extends require("./basic") {
*/
async delete(Key) {
const tenantID = cds.context.tenant
logConfig.debug(`[S3 Upload] Executing delete for file ${Key} in bucket ${this.bucket}`)
logConfig.debug(`[AWS S3] Executing delete for file ${Key} in bucket ${this.bucket}`)

// Check separate object store instances
if (separateObjectStore) {
Expand All @@ -505,6 +481,7 @@ module.exports = class AWSAttachmentsService extends require("./basic") {
}

/**
* Requires implementation as delete of infected attachment is specific to storage service
* @inheritdoc
*/
async deleteInfectedAttachment(Attachments, key) {
Expand Down
Loading