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.
Files changed (98) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +451 -0
  3. package/_i18n/i18n.properties +6 -0
  4. package/_i18n/i18n_ar.properties +6 -0
  5. package/_i18n/i18n_bg.properties +6 -0
  6. package/_i18n/i18n_cs.properties +6 -0
  7. package/_i18n/i18n_da.properties +6 -0
  8. package/_i18n/i18n_de.properties +6 -0
  9. package/_i18n/i18n_el.properties +6 -0
  10. package/_i18n/i18n_en.properties +6 -0
  11. package/_i18n/i18n_en_US_saptrc.properties +6 -0
  12. package/_i18n/i18n_es.properties +6 -0
  13. package/_i18n/i18n_es_MX.properties +6 -0
  14. package/_i18n/i18n_fi.properties +6 -0
  15. package/_i18n/i18n_fr.properties +6 -0
  16. package/_i18n/i18n_he.properties +6 -0
  17. package/_i18n/i18n_hr.properties +6 -0
  18. package/_i18n/i18n_hu.properties +6 -0
  19. package/_i18n/i18n_it.properties +6 -0
  20. package/_i18n/i18n_ja.properties +6 -0
  21. package/_i18n/i18n_kk.properties +6 -0
  22. package/_i18n/i18n_ko.properties +6 -0
  23. package/_i18n/i18n_ms.properties +6 -0
  24. package/_i18n/i18n_nl.properties +6 -0
  25. package/_i18n/i18n_no.properties +6 -0
  26. package/_i18n/i18n_pl.properties +6 -0
  27. package/_i18n/i18n_pt.properties +6 -0
  28. package/_i18n/i18n_ro.properties +6 -0
  29. package/_i18n/i18n_ru.properties +6 -0
  30. package/_i18n/i18n_sh.properties +6 -0
  31. package/_i18n/i18n_sk.properties +6 -0
  32. package/_i18n/i18n_sl.properties +6 -0
  33. package/_i18n/i18n_sv.properties +6 -0
  34. package/_i18n/i18n_th.properties +6 -0
  35. package/_i18n/i18n_tr.properties +6 -0
  36. package/_i18n/i18n_uk.properties +6 -0
  37. package/_i18n/i18n_vi.properties +6 -0
  38. package/_i18n/i18n_zh_CN.properties +6 -0
  39. package/_i18n/i18n_zh_TW.properties +6 -0
  40. package/_i18n/messages.properties +6 -0
  41. package/_i18n/messages_en_US_saptrc.properties +4 -0
  42. package/cds-plugin.js +4 -0
  43. package/db/data/sap.attachments-ScanStates.csv +6 -0
  44. package/db/data/sap.attachments-ScanStates_texts.csv +6 -0
  45. package/db/data/sap.attachments-ScanStates_texts_ar.csv +6 -0
  46. package/db/data/sap.attachments-ScanStates_texts_bg.csv +6 -0
  47. package/db/data/sap.attachments-ScanStates_texts_cs.csv +6 -0
  48. package/db/data/sap.attachments-ScanStates_texts_da.csv +6 -0
  49. package/db/data/sap.attachments-ScanStates_texts_de.csv +6 -0
  50. package/db/data/sap.attachments-ScanStates_texts_el.csv +6 -0
  51. package/db/data/sap.attachments-ScanStates_texts_en.csv +6 -0
  52. package/db/data/sap.attachments-ScanStates_texts_en_US_saptrc.csv +6 -0
  53. package/db/data/sap.attachments-ScanStates_texts_es.csv +6 -0
  54. package/db/data/sap.attachments-ScanStates_texts_es_MX.csv +6 -0
  55. package/db/data/sap.attachments-ScanStates_texts_fi.csv +6 -0
  56. package/db/data/sap.attachments-ScanStates_texts_fr.csv +6 -0
  57. package/db/data/sap.attachments-ScanStates_texts_he.csv +6 -0
  58. package/db/data/sap.attachments-ScanStates_texts_hr.csv +6 -0
  59. package/db/data/sap.attachments-ScanStates_texts_hu.csv +6 -0
  60. package/db/data/sap.attachments-ScanStates_texts_it.csv +6 -0
  61. package/db/data/sap.attachments-ScanStates_texts_ja.csv +6 -0
  62. package/db/data/sap.attachments-ScanStates_texts_kk.csv +6 -0
  63. package/db/data/sap.attachments-ScanStates_texts_ko.csv +6 -0
  64. package/db/data/sap.attachments-ScanStates_texts_ms.csv +6 -0
  65. package/db/data/sap.attachments-ScanStates_texts_nl.csv +6 -0
  66. package/db/data/sap.attachments-ScanStates_texts_no.csv +6 -0
  67. package/db/data/sap.attachments-ScanStates_texts_pl.csv +6 -0
  68. package/db/data/sap.attachments-ScanStates_texts_pt.csv +6 -0
  69. package/db/data/sap.attachments-ScanStates_texts_ro.csv +6 -0
  70. package/db/data/sap.attachments-ScanStates_texts_ru.csv +6 -0
  71. package/db/data/sap.attachments-ScanStates_texts_sh.csv +6 -0
  72. package/db/data/sap.attachments-ScanStates_texts_sk.csv +6 -0
  73. package/db/data/sap.attachments-ScanStates_texts_sl.csv +6 -0
  74. package/db/data/sap.attachments-ScanStates_texts_sv.csv +6 -0
  75. package/db/data/sap.attachments-ScanStates_texts_th.csv +6 -0
  76. package/db/data/sap.attachments-ScanStates_texts_tr.csv +6 -0
  77. package/db/data/sap.attachments-ScanStates_texts_uk.csv +6 -0
  78. package/db/data/sap.attachments-ScanStates_texts_vi.csv +6 -0
  79. package/db/data/sap.attachments-ScanStates_texts_zh_CN.csv +6 -0
  80. package/db/data/sap.attachments-ScanStates_texts_zh_TW.csv +6 -0
  81. package/db/index.cds +85 -0
  82. package/index.cds +1 -0
  83. package/lib/csn-runtime-extension.js +31 -0
  84. package/lib/generic-handlers.js +199 -0
  85. package/lib/helper.js +407 -0
  86. package/lib/mtx/server.js +583 -0
  87. package/lib/plugin.js +112 -0
  88. package/package.json +67 -0
  89. package/srv/aws-s3.js +249 -0
  90. package/srv/azure-blob-storage.js +202 -0
  91. package/srv/basic.js +331 -0
  92. package/srv/gcp.js +226 -0
  93. package/srv/malwareScanner-mocked.cds +3 -0
  94. package/srv/malwareScanner-mocked.js +127 -0
  95. package/srv/malwareScanner.cds +23 -0
  96. package/srv/malwareScanner.js +151 -0
  97. package/srv/object-store.js +44 -0
  98. 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
+ }