kalai-attach 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +451 -0
- package/_i18n/i18n.properties +6 -0
- package/_i18n/i18n_ar.properties +6 -0
- package/_i18n/i18n_bg.properties +6 -0
- package/_i18n/i18n_cs.properties +6 -0
- package/_i18n/i18n_da.properties +6 -0
- package/_i18n/i18n_de.properties +6 -0
- package/_i18n/i18n_el.properties +6 -0
- package/_i18n/i18n_en.properties +6 -0
- package/_i18n/i18n_en_US_saptrc.properties +6 -0
- package/_i18n/i18n_es.properties +6 -0
- package/_i18n/i18n_es_MX.properties +6 -0
- package/_i18n/i18n_fi.properties +6 -0
- package/_i18n/i18n_fr.properties +6 -0
- package/_i18n/i18n_he.properties +6 -0
- package/_i18n/i18n_hr.properties +6 -0
- package/_i18n/i18n_hu.properties +6 -0
- package/_i18n/i18n_it.properties +6 -0
- package/_i18n/i18n_ja.properties +6 -0
- package/_i18n/i18n_kk.properties +6 -0
- package/_i18n/i18n_ko.properties +6 -0
- package/_i18n/i18n_ms.properties +6 -0
- package/_i18n/i18n_nl.properties +6 -0
- package/_i18n/i18n_no.properties +6 -0
- package/_i18n/i18n_pl.properties +6 -0
- package/_i18n/i18n_pt.properties +6 -0
- package/_i18n/i18n_ro.properties +6 -0
- package/_i18n/i18n_ru.properties +6 -0
- package/_i18n/i18n_sh.properties +6 -0
- package/_i18n/i18n_sk.properties +6 -0
- package/_i18n/i18n_sl.properties +6 -0
- package/_i18n/i18n_sv.properties +6 -0
- package/_i18n/i18n_th.properties +6 -0
- package/_i18n/i18n_tr.properties +6 -0
- package/_i18n/i18n_uk.properties +6 -0
- package/_i18n/i18n_vi.properties +6 -0
- package/_i18n/i18n_zh_CN.properties +6 -0
- package/_i18n/i18n_zh_TW.properties +6 -0
- package/_i18n/messages.properties +6 -0
- package/_i18n/messages_en_US_saptrc.properties +4 -0
- package/cds-plugin.js +4 -0
- package/db/data/sap.attachments-ScanStates.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_ar.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_bg.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_cs.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_da.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_de.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_el.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_en.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_en_US_saptrc.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_es.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_es_MX.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_fi.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_fr.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_he.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_hr.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_hu.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_it.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_ja.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_kk.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_ko.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_ms.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_nl.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_no.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_pl.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_pt.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_ro.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_ru.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_sh.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_sk.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_sl.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_sv.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_th.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_tr.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_uk.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_vi.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_zh_CN.csv +6 -0
- package/db/data/sap.attachments-ScanStates_texts_zh_TW.csv +6 -0
- package/db/index.cds +85 -0
- package/index.cds +1 -0
- package/lib/csn-runtime-extension.js +31 -0
- package/lib/generic-handlers.js +199 -0
- package/lib/helper.js +407 -0
- package/lib/mtx/server.js +583 -0
- package/lib/plugin.js +112 -0
- package/package.json +67 -0
- package/srv/aws-s3.js +249 -0
- package/srv/azure-blob-storage.js +202 -0
- package/srv/basic.js +331 -0
- package/srv/gcp.js +226 -0
- package/srv/malwareScanner-mocked.cds +3 -0
- package/srv/malwareScanner-mocked.js +127 -0
- package/srv/malwareScanner.cds +23 -0
- package/srv/malwareScanner.js +151 -0
- package/srv/object-store.js +44 -0
- package/srv/standard.js +3 -0
package/db/index.cds
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The common root-level aspect used in applications like that:
|
|
2
|
+
// using { Attachments } from 'kalai-attach'
|
|
3
|
+
aspect Attachments : sap.attachments.Attachments {}
|
|
4
|
+
|
|
5
|
+
using {
|
|
6
|
+
managed,
|
|
7
|
+
cuid,
|
|
8
|
+
sap.common.CodeList
|
|
9
|
+
} from '@sap/cds/common';
|
|
10
|
+
|
|
11
|
+
context sap.attachments {
|
|
12
|
+
|
|
13
|
+
aspect MediaData @(_is_media_data) {
|
|
14
|
+
url : String @UI.Hidden;
|
|
15
|
+
content : LargeBinary @title: '{i18n>Attachment}'; // only for db-based services
|
|
16
|
+
mimeType : String default 'application/octet-stream' @title: '{i18n>MediaType}';
|
|
17
|
+
filename : String @title: '{i18n>FileName}';
|
|
18
|
+
hash : String @UI.Hidden @Core.Computed;
|
|
19
|
+
status : String @title: '{i18n>ScanStatus}' default 'Unscanned' @Common.Text: statusNav.name @Common.TextArrangement: #TextOnly;
|
|
20
|
+
statusNav : Association to one ScanStates
|
|
21
|
+
on statusNav.code = status;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
entity ScanStates : CodeList {
|
|
25
|
+
key code : String(32) @Common.Text: name @Common.TextArrangement: #TextOnly enum {
|
|
26
|
+
Unscanned;
|
|
27
|
+
Scanning;
|
|
28
|
+
Infected;
|
|
29
|
+
Clean;
|
|
30
|
+
Failed;
|
|
31
|
+
};
|
|
32
|
+
name : localized String(64) @title: '{i18n>ScanStatus}';
|
|
33
|
+
criticality : Integer @UI.Hidden;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
aspect Attachments : cuid, managed, MediaData {
|
|
37
|
+
note : String @title: '{i18n>Note}';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
// -- Fiori Annotations ----------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
annotate MediaData with @UI.MediaResource: {Stream: content} {
|
|
44
|
+
content @Core.MediaType: mimeType @odata.draft.skip;
|
|
45
|
+
mimeType @Core.IsMediaType;
|
|
46
|
+
status @readonly;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
annotate Attachments with @UI: {
|
|
50
|
+
HeaderInfo: {
|
|
51
|
+
TypeName : '{i18n>Attachment}',
|
|
52
|
+
TypeNamePlural: '{i18n>Attachments}',
|
|
53
|
+
},
|
|
54
|
+
LineItem : [
|
|
55
|
+
{
|
|
56
|
+
Value : content,
|
|
57
|
+
@HTML5.CssDefaults: {width: '30%'}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
Value : status,
|
|
61
|
+
Criticality : statusNav.criticality,
|
|
62
|
+
@HTML5.CssDefaults: {width: '10%'}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
Value : createdAt,
|
|
66
|
+
@HTML5.CssDefaults: {width: '20%'}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
Value : createdBy,
|
|
70
|
+
@HTML5.CssDefaults: {width: '15%'}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
Value : note,
|
|
74
|
+
@HTML5.CssDefaults: {width: '25%'}
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
} @Capabilities: {SortRestrictions: {NonSortableProperties: [content]}} {
|
|
78
|
+
content
|
|
79
|
+
@Core.ContentDisposition: {
|
|
80
|
+
Filename: filename,
|
|
81
|
+
Type : 'inline'
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
}
|
package/index.cds
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
using from './db/index.cds';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
|
|
3
|
+
function collectAttachments(ent, resultSet = [], path = []) {
|
|
4
|
+
if (!ent.compositions) return resultSet
|
|
5
|
+
for (const ele of Object.keys(ent.compositions)) {
|
|
6
|
+
const target = ent.compositions[ele]._target
|
|
7
|
+
const newPath = [...path, ele]
|
|
8
|
+
if (target?.["@_is_media_data"]) {
|
|
9
|
+
resultSet.push(newPath)
|
|
10
|
+
}
|
|
11
|
+
if (target && target !== ent) collectAttachments(target, resultSet, newPath)
|
|
12
|
+
}
|
|
13
|
+
return resultSet
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
Object.defineProperty(cds.builtin.classes.entity.prototype, '_attachments', {
|
|
17
|
+
get() {
|
|
18
|
+
const entity = this;
|
|
19
|
+
return {
|
|
20
|
+
get hasAttachmentsComposition() {
|
|
21
|
+
return entity.compositions && Object.keys(entity.compositions).some(ele => entity.compositions[ele]._target?.["@_is_media_data"] || entity.compositions[ele]._target?._attachments?.hasAttachmentsComposition)
|
|
22
|
+
},
|
|
23
|
+
get attachmentCompositions() {
|
|
24
|
+
return collectAttachments(entity)
|
|
25
|
+
},
|
|
26
|
+
get isAttachmentsEntity() {
|
|
27
|
+
return !!entity?.["@_is_media_data"]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
})
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
const LOG = cds.log('attachments')
|
|
3
|
+
const { extname } = require("path")
|
|
4
|
+
const { MAX_FILE_SIZE, sizeInBytes, checkMimeTypeMatch } = require('./helper')
|
|
5
|
+
|
|
6
|
+
const isMultitenacyEnabled = !!cds.env.requires.multitenancy
|
|
7
|
+
const objectStoreKind = cds.env.requires?.attachments?.objectStore?.kind
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Prepares the attachment data before creation
|
|
11
|
+
* @param {import('@sap/cds').Request} req - The request object
|
|
12
|
+
*/
|
|
13
|
+
async function onPrepareAttachment(req) {
|
|
14
|
+
if (!req.target?._attachments.isAttachmentsEntity) return;
|
|
15
|
+
|
|
16
|
+
const hasUpKey = Object.keys(req.data).some(key => key.startsWith("up__"))
|
|
17
|
+
|
|
18
|
+
if (!hasUpKey) {
|
|
19
|
+
const mySubject = { ...req.subject, ref: req.subject.ref.slice(0, -1) }
|
|
20
|
+
const parentKeys = Object.keys(cds.infer.target({SELECT: {from: mySubject}}).keys)
|
|
21
|
+
const parentRecord = await SELECT.one.from(mySubject).columns(parentKeys)
|
|
22
|
+
|
|
23
|
+
for (const key of parentKeys) {
|
|
24
|
+
req.data[`up__${key}`] = parentRecord[key]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
req.data.url = isMultitenacyEnabled && objectStoreKind === "shared"
|
|
29
|
+
? `${req.tenant}_${req.data.url}`
|
|
30
|
+
: cds.utils.uuid()
|
|
31
|
+
req.data.ID ??= cds.utils.uuid()
|
|
32
|
+
|
|
33
|
+
let ext = req.data.filename ? extname(req.data.filename).toLowerCase().slice(1) : null
|
|
34
|
+
req.data.mimeType = Ext2MimeTypes[ext]
|
|
35
|
+
|
|
36
|
+
if (!req.data.mimeType) {
|
|
37
|
+
LOG.warn(`An attachment ${req.data.ID} is uploaded whose extension "${ext}" is not known! Falling back to "application/octet-stream"`)
|
|
38
|
+
req.data.mimeType = "application/octet-stream"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validates if the attachment can be accessed based on its malware scan status
|
|
44
|
+
* @param {import('@sap/cds').Request} req - The request object
|
|
45
|
+
*/
|
|
46
|
+
async function validateAttachment(req) {
|
|
47
|
+
if (!req.target?._attachments.isAttachmentsEntity) return;
|
|
48
|
+
|
|
49
|
+
/* removing case condition for mediaType annotation as in our case binary value and metadata is stored in different database */
|
|
50
|
+
req?.query?.SELECT?.columns?.forEach((element) => {
|
|
51
|
+
if (element.as === 'content@odata.mediaContentType' && element.xpr) {
|
|
52
|
+
delete element.xpr
|
|
53
|
+
element.ref = ['mimeType']
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
if (req?.req?.url?.endsWith("/content")) {
|
|
58
|
+
const AttachmentsSrv = await cds.connect.to("attachments")
|
|
59
|
+
const status = await AttachmentsSrv.getStatus(req.target, { ID: req.data.ID || req.params?.at(-1).ID })
|
|
60
|
+
if (status === null || status === undefined) {
|
|
61
|
+
return req.reject(404)
|
|
62
|
+
}
|
|
63
|
+
const scanEnabled = cds.env.requires?.attachments?.scan ?? true
|
|
64
|
+
if (scanEnabled && status !== 'Clean') {
|
|
65
|
+
req.reject(403, 'UnableToDownloadAttachmentScanStatusNotClean')
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Reads the attachment content if requested
|
|
72
|
+
* @param {[cds.Entity]} param0
|
|
73
|
+
* @param {import('@sap/cds').Request} req - The request object
|
|
74
|
+
*/
|
|
75
|
+
async function readAttachment([attachment], req) {
|
|
76
|
+
if (!req.target?._attachments.isAttachmentsEntity) return;
|
|
77
|
+
|
|
78
|
+
const AttachmentsSrv = await cds.connect.to("attachments")
|
|
79
|
+
if (req._.readAfterWrite || !req?.req?.url?.endsWith("/content") || !attachment || attachment?.content) return
|
|
80
|
+
let keys = { ID: req.data.ID ?? req.params.at(-1).ID }
|
|
81
|
+
let { target } = req
|
|
82
|
+
attachment.content = await AttachmentsSrv.get(target, keys)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Checks the attachments size against the maximum defined by the annotation `@Validation.Maximum`. Default 400mb.
|
|
87
|
+
* If the limit is reached by the reported size of the content-length header or if the stream length exceeds
|
|
88
|
+
* the limits the error is thrown.
|
|
89
|
+
* @param {import('@sap/cds').Request} req - The request object
|
|
90
|
+
* @throws AttachmentSizeExceeded
|
|
91
|
+
*/
|
|
92
|
+
function validateAttachmentSize(req) {
|
|
93
|
+
if (!req.target?._attachments.isAttachmentsEntity || !req.data.content) return;
|
|
94
|
+
|
|
95
|
+
const maxFileSize = req.target.elements['content']['@Validation.Maximum'] ?
|
|
96
|
+
sizeInBytes(req.target.elements['content']['@Validation.Maximum'], req.target.name) ?? MAX_FILE_SIZE :
|
|
97
|
+
MAX_FILE_SIZE
|
|
98
|
+
|
|
99
|
+
if (req.headers["content-length"] == null || req.headers["content-length"] === "") {
|
|
100
|
+
return req.reject(400, 'ContentLengthHeaderMissing')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (isNaN(Number(req.headers["content-length"]))) {
|
|
104
|
+
return req.reject(400, 'InvalidContentLengthHeader', { contentLength: req.headers["content-length"] })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (Number(req.headers["content-length"]) > maxFileSize) {
|
|
108
|
+
if (req.data.content.pause) { req.data.content.pause() }
|
|
109
|
+
return req.reject({ status: 413, message: "AttachmentSizeExceeded", args: [req.target.elements['content']['@Validation.Maximum'] ?? '400MB'] })
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validates the attachment mime type against acceptable media types
|
|
115
|
+
* @param {import('@sap/cds').Request} req - The request object
|
|
116
|
+
*/
|
|
117
|
+
function validateAttachmentMimeType(req) {
|
|
118
|
+
if (!req.target?._attachments.isAttachmentsEntity || !req.data.content) return;
|
|
119
|
+
|
|
120
|
+
const mimeType = req.data.mimeType
|
|
121
|
+
|
|
122
|
+
const acceptableMediaTypes = req.target.elements.content['@Core.AcceptableMediaTypes'] || '*/*'
|
|
123
|
+
if (!checkMimeTypeMatch(acceptableMediaTypes, mimeType)) {
|
|
124
|
+
return req.reject(400, "AttachmentMimeTypeDisallowed", { mimeType: mimeType })
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
validateAttachmentSize,
|
|
130
|
+
onPrepareAttachment,
|
|
131
|
+
readAttachment,
|
|
132
|
+
validateAttachment,
|
|
133
|
+
validateAttachmentMimeType
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Mapping table from file extensions to mime types
|
|
137
|
+
const Ext2MimeTypes = {
|
|
138
|
+
aac: "audio/aac",
|
|
139
|
+
abw: "application/x-abiword",
|
|
140
|
+
arc: "application/octet-stream",
|
|
141
|
+
avi: "video/x-msvideo",
|
|
142
|
+
azw: "application/vnd.amazon.ebook",
|
|
143
|
+
bin: "application/octet-stream",
|
|
144
|
+
png: "image/png",
|
|
145
|
+
gif: "image/gif",
|
|
146
|
+
bmp: "image/bmp",
|
|
147
|
+
bz: "application/x-bzip",
|
|
148
|
+
bz2: "application/x-bzip2",
|
|
149
|
+
csh: "application/x-csh",
|
|
150
|
+
css: "text/css",
|
|
151
|
+
csv: "text/csv",
|
|
152
|
+
doc: "application/msword",
|
|
153
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
154
|
+
odp: "application/vnd.oasis.opendocument.presentation",
|
|
155
|
+
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
156
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
157
|
+
epub: "application/epub+zip",
|
|
158
|
+
gz: "application/gzip",
|
|
159
|
+
htm: "text/html",
|
|
160
|
+
html: "text/html",
|
|
161
|
+
ico: "image/x-icon",
|
|
162
|
+
ics: "text/calendar",
|
|
163
|
+
jar: "application/java-archive",
|
|
164
|
+
jpg: "image/jpeg",
|
|
165
|
+
jpeg: "image/jpeg",
|
|
166
|
+
js: "text/javascript",
|
|
167
|
+
json: "application/json",
|
|
168
|
+
mid: "audio/midi",
|
|
169
|
+
midi: "audio/midi",
|
|
170
|
+
mjs: "text/javascript",
|
|
171
|
+
mov: "video/quicktime",
|
|
172
|
+
mp3: "audio/mpeg",
|
|
173
|
+
mp4: "video/mp4",
|
|
174
|
+
mpeg: "video/mpeg",
|
|
175
|
+
mpkg: "application/vnd.apple.installer+xml",
|
|
176
|
+
otf: "font/otf",
|
|
177
|
+
pdf: "application/pdf",
|
|
178
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
179
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
180
|
+
rar: "application/x-rar-compressed",
|
|
181
|
+
rtf: "application/rtf",
|
|
182
|
+
svg: "image/svg+xml",
|
|
183
|
+
tar: "application/x-tar",
|
|
184
|
+
tif: "image/tiff",
|
|
185
|
+
tiff: "image/tiff",
|
|
186
|
+
ttf: "font/ttf",
|
|
187
|
+
vsd: "application/vnd.visio",
|
|
188
|
+
wav: "audio/wav",
|
|
189
|
+
woff: "font/woff",
|
|
190
|
+
woff2: "font/woff2",
|
|
191
|
+
xhtml: "application/xhtml+xml",
|
|
192
|
+
xls: "application/vnd.ms-excel",
|
|
193
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
194
|
+
xml: "application/xml",
|
|
195
|
+
zip: "application/zip",
|
|
196
|
+
txt: "application/txt",
|
|
197
|
+
lst: "application/txt",
|
|
198
|
+
webp: "image/webp",
|
|
199
|
+
}
|