@sap/cds 9.7.0 → 9.8.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/CHANGELOG.md +49 -0
- package/_i18n/i18n_en_US_saptrc.properties +1 -56
- package/_i18n/messages_en_US_saptrc.properties +1 -92
- package/eslint.config.mjs +1 -1
- package/lib/compile/for/flows.js +86 -79
- package/lib/compile/for/lean_drafts.js +12 -0
- package/lib/compile/to/json.js +4 -2
- package/lib/env/defaults.js +1 -0
- package/lib/env/serviceBindings.js +15 -5
- package/lib/index.js +1 -1
- package/lib/log/cds-error.js +33 -20
- package/lib/req/spawn.js +2 -2
- package/lib/srv/bindings.js +6 -13
- package/lib/srv/cds.Service.js +8 -36
- package/lib/srv/protocols/hcql.js +19 -2
- package/lib/srv/protocols/http.js +1 -1
- package/lib/utils/cds-utils.js +25 -16
- package/lib/utils/tar-win.js +106 -0
- package/lib/utils/tar.js +23 -158
- package/libx/_runtime/common/generic/crud.js +8 -7
- package/libx/_runtime/common/generic/sorting.js +7 -3
- package/libx/_runtime/common/utils/resolveView.js +47 -40
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -0
- package/libx/_runtime/fiori/lean-draft.js +11 -2
- package/libx/_runtime/messaging/kafka.js +6 -5
- package/libx/_runtime/remote/Service.js +14 -2
- package/libx/_runtime/remote/utils/client.js +2 -4
- package/libx/_runtime/remote/utils/query.js +4 -4
- package/libx/odata/middleware/batch.js +316 -339
- package/libx/odata/middleware/create.js +0 -5
- package/libx/odata/middleware/delete.js +2 -6
- package/libx/odata/middleware/operation.js +10 -8
- package/libx/odata/middleware/read.js +0 -10
- package/libx/odata/middleware/stream.js +1 -0
- package/libx/odata/middleware/update.js +0 -6
- package/libx/odata/parse/afterburner.js +47 -22
- package/libx/odata/parse/cqn2odata.js +6 -1
- package/libx/odata/parse/grammar.peggy +14 -2
- package/libx/odata/parse/multipartToJson.js +2 -1
- package/libx/odata/parse/parser.js +1 -1
- package/package.json +2 -2
package/lib/log/cds-error.js
CHANGED
|
@@ -5,33 +5,46 @@ const { format, inspect } = require('../utils/cds-utils')
|
|
|
5
5
|
* Constructs and optionally throws an Error object.
|
|
6
6
|
* Usage variants:
|
|
7
7
|
*
|
|
8
|
-
* cds.error (404, '
|
|
9
|
-
* cds.error (
|
|
10
|
-
* cds.error ({
|
|
8
|
+
* cds.error (404, 'code', 'message', { ... details })
|
|
9
|
+
* cds.error (404, { ... details })
|
|
10
|
+
* cds.error ({ ... any details })
|
|
11
|
+
* cds.error ('code', 'message', { ... details })
|
|
12
|
+
* cds.error ('message', { ... details })
|
|
11
13
|
* cds.error `template string usage variant`
|
|
14
|
+
* cds.error (new Error, { ... any details })
|
|
12
15
|
*
|
|
13
16
|
* When called with `new` the newly created Error is returned.
|
|
14
17
|
* When called without `new` the error is thrown immediately.
|
|
15
18
|
* The latter is useful for usages like that:
|
|
16
19
|
*
|
|
17
|
-
* let x = y || cds.error `
|
|
20
|
+
* let x = y || cds.error `expected y to be defined`
|
|
18
21
|
*
|
|
19
22
|
* @param {number} [status] - HTTP status code
|
|
20
|
-
* @param {string} [
|
|
23
|
+
* @param {string} [code] - Stable error code, which clients can rely on
|
|
24
|
+
* @param {string} [message] - Human-readable error message
|
|
21
25
|
* @param {object} [details] - Additional error details
|
|
22
|
-
* @param {Function} [caller] - The function calling
|
|
26
|
+
* @param {Function} [caller] - The function calling us => stack trace cut off here
|
|
27
|
+
* @returns {Error} The constructed Error (only when called with `new`)
|
|
23
28
|
*/
|
|
24
|
-
const error = exports = module.exports = function error ( status,
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
if (
|
|
30
|
-
if (
|
|
31
|
-
|
|
29
|
+
const error = exports = module.exports = function error ( status, code, msg, details, caller ) {
|
|
30
|
+
let e = details
|
|
31
|
+
if (status?.raw) [ msg, status, code, e, caller ] = [ error.message(...arguments) ]
|
|
32
|
+
if (typeof status !== 'number') [ status, code, msg, e, caller ] = [ undefined, status, code, msg, e, caller ]
|
|
33
|
+
if (typeof code === 'object') [ code, msg, e, caller ] = [ undefined, undefined, code, msg ]
|
|
34
|
+
if (typeof msg === 'object') [ code, msg, e, caller ] = [ undefined, code, msg, e ]
|
|
35
|
+
if (typeof e === 'string') [ status, msg, e, caller ] = [ msg, e, caller, error ]
|
|
36
|
+
if (code && !msg) [ code, msg ] = [ undefined, code ]
|
|
37
|
+
if (e instanceof Error || typeof e === 'object' && 'stack' in e) { // is error?
|
|
38
|
+
e = Object.assign (e, caller) //> yes -> just decorate it
|
|
39
|
+
} else {
|
|
40
|
+
e = Object.assign (new Error (msg, e), e)
|
|
41
|
+
Error.captureStackTrace (e, caller || error)
|
|
42
|
+
}
|
|
43
|
+
if (status) e.status = status
|
|
44
|
+
if (code) e.code = code
|
|
45
|
+
if (new.target) return e; else throw e
|
|
32
46
|
}
|
|
33
47
|
|
|
34
|
-
|
|
35
48
|
/**
|
|
36
49
|
* Constructs a message from a tagged template string. In contrast to usual
|
|
37
50
|
* template strings embedded values are formatted using `util.format`
|
|
@@ -56,14 +69,14 @@ exports.message = (strings,...values) => {
|
|
|
56
69
|
* typeof x === 'string' || cds.error.expected `${{x}} to be a string`
|
|
57
70
|
* //> Error: Expected argument 'x' to be a string, but got: { foo: 'bar' }
|
|
58
71
|
*/
|
|
59
|
-
exports.expected = ([,
|
|
60
|
-
const [
|
|
61
|
-
return error (`Expected argument
|
|
72
|
+
exports.expected = function expected ([,_to_be_expected], arg) {
|
|
73
|
+
const [name] = Object.keys(arg), value = arg[name]
|
|
74
|
+
return error (`Expected argument ${inspect(name)}${_to_be_expected}, but got: ${inspect(value)}`, undefined, expected)
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
exports.isSystemError = err => {
|
|
65
|
-
// all errors thrown by the peggy parser should not crash the app
|
|
66
|
-
if (err.name === 'SyntaxError' && err.constructor?.name === 'peg$SyntaxError') return false
|
|
78
|
+
// all errors thrown by the peggy parser or body-parser (used by express.json) should not crash the app
|
|
79
|
+
if (err.name === 'SyntaxError' && (err.constructor?.name === 'peg$SyntaxError' || err.type === 'entity.parse.failed')) return false
|
|
67
80
|
return err.name in {
|
|
68
81
|
TypeError:1,
|
|
69
82
|
ReferenceError:1,
|
package/lib/req/spawn.js
CHANGED
|
@@ -21,9 +21,9 @@ module.exports = function spawn (o, fn, /** @type {import('../index')} */ cds=th
|
|
|
21
21
|
return tx.rollback(e)
|
|
22
22
|
})
|
|
23
23
|
.then (res => Promise.all(em.listeners('succeeded').map(each => each(res))))
|
|
24
|
-
.catch (err => Promise.all(em.listeners('failed').map(each => each(err))))
|
|
25
|
-
.finally (() => Promise.all(em.listeners('done').map(each => each())))
|
|
26
24
|
})
|
|
25
|
+
.catch (err => Promise.all(em.listeners('failed').map(each => each(err))))
|
|
26
|
+
.finally (() => Promise.all(em.listeners('done').map(each => each())))
|
|
27
27
|
}
|
|
28
28
|
const em = new EventEmitter
|
|
29
29
|
em.timer = (
|
package/lib/srv/bindings.js
CHANGED
|
@@ -29,23 +29,16 @@ class Bindings {
|
|
|
29
29
|
const kind = [ required?.kind, 'hcql', 'rest', 'odata' ].find (k => k in binding.endpoints)
|
|
30
30
|
const path = binding.endpoints [kind]
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
...
|
|
35
|
-
...binding.credentials,
|
|
36
|
-
url: server.url + path
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// in case of cds.requires.Foo = true
|
|
40
|
-
else required = cds.requires[service] = cds.env.requires[service] = {
|
|
41
|
-
...cds.requires.kinds [binding.kind],
|
|
32
|
+
if (typeof required !== 'object') required = {}
|
|
33
|
+
cds.env.requires[service] = required = {
|
|
34
|
+
kind, ...cds.requires.kinds [kind], ...required,
|
|
42
35
|
credentials: {
|
|
36
|
+
...required.credentials,
|
|
43
37
|
...binding.credentials,
|
|
44
38
|
url: server.url + path
|
|
45
|
-
}
|
|
39
|
+
},
|
|
46
40
|
}
|
|
47
|
-
|
|
48
|
-
required.kind = kind
|
|
41
|
+
required.kind = kind // override kind from binding
|
|
49
42
|
|
|
50
43
|
// REVISIT: temporary fix to inherit kind as well for mocked odata services
|
|
51
44
|
// otherwise mocking with two services does not work for kind:odata-v2
|
package/lib/srv/cds.Service.js
CHANGED
|
@@ -139,9 +139,9 @@ const compat_function_factory = (api, srv, it) => cds.utils.deprecated (ns => {
|
|
|
139
139
|
*/
|
|
140
140
|
class Service extends ReflectionAPI {
|
|
141
141
|
|
|
142
|
-
/**
|
|
143
|
-
* @param {string} name
|
|
144
|
-
* @param {import('../core/linked-csn').LinkedCSN} model
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} name
|
|
144
|
+
* @param {import('../core/linked-csn').LinkedCSN} model
|
|
145
145
|
*/
|
|
146
146
|
constructor (name, model, options) { super()
|
|
147
147
|
if (typeof name === 'object') [ model, options, name = _service_in(model) ] = [ name, model ]
|
|
@@ -191,49 +191,21 @@ class Service extends ReflectionAPI {
|
|
|
191
191
|
if (this._resolve) return this._resolve
|
|
192
192
|
|
|
193
193
|
const { resolveView, getTransition } = require('../../libx/_runtime/common/utils/resolveView')
|
|
194
|
-
const PERSISTENCE_TABLE = '@cds.persistence.table'
|
|
195
|
-
|
|
196
|
-
const _isPersistenceTable = target =>
|
|
197
|
-
Object.prototype.hasOwnProperty.call(target, PERSISTENCE_TABLE) && target[PERSISTENCE_TABLE]
|
|
198
|
-
const _defaultAbort = tx => e => e._service?.name === tx.definition?.name
|
|
199
194
|
|
|
200
|
-
this._resolve =
|
|
195
|
+
this._resolve = query => {
|
|
201
196
|
const ctx = cds.context
|
|
202
197
|
const model = ctx?.model || this.model
|
|
203
|
-
return resolveView(query, model, this
|
|
198
|
+
return resolveView(query, model, this)
|
|
204
199
|
}
|
|
205
200
|
|
|
206
|
-
//
|
|
207
|
-
this._resolve.transitions =
|
|
201
|
+
// NOTE: used in lean-draft and odata stream middleware
|
|
202
|
+
this._resolve.transitions = query => {
|
|
208
203
|
const target = query && typeof query === 'object' ? cds.infer.target(query) || query?._target : undefined
|
|
209
204
|
const _tx = typeof tx === 'function' ? cds.context?.tx : this
|
|
210
205
|
const event = query?.INSERT ? 'INSERT' : query?.UPDATE ? 'UPDATE' : query?.DELETE ? 'DELETE' : undefined
|
|
211
|
-
return getTransition(target, _tx,
|
|
212
|
-
abort: abortCondition ?? (this.isDatabaseService ? this.resolve._abortDB : _defaultAbort(this))
|
|
213
|
-
})
|
|
206
|
+
return getTransition(target, _tx, null, event)
|
|
214
207
|
}
|
|
215
208
|
|
|
216
|
-
this._resolve.resolve4db = query => {
|
|
217
|
-
return this.resolve(query, this, this.resolve.abortDB)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// REVISIT: Remove once we get rid of composition tree
|
|
221
|
-
this._resolve.table = target => {
|
|
222
|
-
if (target.query?._target && !_isPersistenceTable(target)) {
|
|
223
|
-
return this.resolve.table(target.query._target)
|
|
224
|
-
}
|
|
225
|
-
return target
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// REVISIT: Remove once we get rid of old db
|
|
229
|
-
this._resolve.abortDB = target => {
|
|
230
|
-
return !!(_isPersistenceTable(target) || !target.query?._target)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
this._resolve.transitions4db = (query, skipForbiddenViewCheck) => {
|
|
234
|
-
return this.resolve.transitions(query, this.resolve.abortDB, skipForbiddenViewCheck)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
209
|
return this._resolve
|
|
238
210
|
}
|
|
239
211
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const cds = require('../../index'), {inspect} = cds.utils
|
|
2
2
|
const express = require('express')
|
|
3
|
+
const { pipeline } = require('node:stream/promises')
|
|
3
4
|
|
|
4
5
|
const LOG = cds.log('hcql')
|
|
5
6
|
const PROD = process.env.NODE_ENV === 'production'
|
|
@@ -69,7 +70,18 @@ class HCQLAdapter extends require('./http') {
|
|
|
69
70
|
* The ultimate handler for all CRUD requests.
|
|
70
71
|
*/
|
|
71
72
|
crud (req, res, next) {
|
|
72
|
-
let query = this.query4
|
|
73
|
+
let query = this.query4(req)
|
|
74
|
+
|
|
75
|
+
if (query.stream && can_stream(req))
|
|
76
|
+
return this.service
|
|
77
|
+
.tx(() =>
|
|
78
|
+
query.stream().then(results => {
|
|
79
|
+
res.set('content-type', 'application/octet-stream')
|
|
80
|
+
return pipeline(results, res)
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
.catch(next)
|
|
84
|
+
|
|
73
85
|
return this.service.run (query)
|
|
74
86
|
.then (results => this.reply (results, res))
|
|
75
87
|
.catch (next)
|
|
@@ -81,7 +93,9 @@ class HCQLAdapter extends require('./http') {
|
|
|
81
93
|
* which is expected to be a plain CQN object or a CQL string.
|
|
82
94
|
*/
|
|
83
95
|
query4 (/** @type express.Request */ req) {
|
|
84
|
-
let q = req.body = cds.ql(req.body ?? {})
|
|
96
|
+
let q = req.body = cds.ql(req.body ?? {})
|
|
97
|
+
if (!q.bind) this.error(400, 'Invalid query', { query: req.body })
|
|
98
|
+
q.bind(this.service)
|
|
85
99
|
// handle request headers
|
|
86
100
|
if (q.SELECT) {
|
|
87
101
|
if (req.get('Accept-Language')) q.SELECT.localized = true
|
|
@@ -148,4 +162,7 @@ const ql_fragment = x => {
|
|
|
148
162
|
}
|
|
149
163
|
return x
|
|
150
164
|
}
|
|
165
|
+
const can_stream = req =>
|
|
166
|
+
req.headers.accept?.split?.(',').find(h => h.split(';')[0].trim() === 'application/octet-stream')
|
|
167
|
+
|
|
151
168
|
module.exports = HCQLAdapter
|
|
@@ -51,7 +51,7 @@ class HttpAdapter {
|
|
|
51
51
|
const user = cds.context.user
|
|
52
52
|
if (required.some(role => user.has(role))) return next()
|
|
53
53
|
else if (user._is_anonymous) return next(401) // request login
|
|
54
|
-
else throw Object.assign(new Error, { code: 403, reason: `User '${user.id}' is lacking required roles: [${required}]
|
|
54
|
+
else throw Object.assign(new Error, { code: 403, reason: `User '${user.id}' is lacking required roles: [${required}]` })
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
package/lib/utils/cds-utils.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
const cwd = process.env._original_cwd || process.cwd()
|
|
2
2
|
const cds = require('../index')
|
|
3
3
|
|
|
4
|
-
/* eslint no-empty: ["error", { "allowEmptyCatch": true }] */
|
|
5
|
-
// eslint-disable-next-line no-unused-vars
|
|
6
|
-
const _tarLib = () => { try { return require('tar') } catch(_) {} }
|
|
7
|
-
|
|
8
4
|
exports = module.exports = new class {
|
|
9
5
|
get colors() { return super.colors = require('./colors') }
|
|
10
6
|
get inflect() { return super.inflect = require('./inflect') }
|
|
@@ -17,9 +13,19 @@ exports = module.exports = new class {
|
|
|
17
13
|
const {format} = require('node:util')
|
|
18
14
|
return super.format = format
|
|
19
15
|
}
|
|
16
|
+
get yaml() {
|
|
17
|
+
const yaml = require('js-yaml')
|
|
18
|
+
return super.yaml = Object.assign(yaml,{parse:yaml.load})
|
|
19
|
+
}
|
|
20
|
+
get tar() {
|
|
21
|
+
if (process.platform === 'win32') try { require.resolve('tar')
|
|
22
|
+
return super.tar = require('./tar-lib')
|
|
23
|
+
} catch {
|
|
24
|
+
return super.tar = require('./tar-win')
|
|
25
|
+
}
|
|
26
|
+
else return super.tar = require('./tar')
|
|
27
|
+
}
|
|
20
28
|
get uuid() { return super.uuid = require('crypto').randomUUID }
|
|
21
|
-
get yaml() { const yaml = require('js-yaml'); return super.yaml = Object.assign(yaml,{parse:yaml.load}) }
|
|
22
|
-
get tar() { return super.tar = process.platform === 'win32' && _tarLib() ? require('./tar-lib') : require('./tar') }
|
|
23
29
|
get semver() { return super.semver = require('./version') }
|
|
24
30
|
}
|
|
25
31
|
|
|
@@ -309,17 +315,20 @@ exports.csv = require('./csv-reader')
|
|
|
309
315
|
* Loads a file through ESM or CommonJs.
|
|
310
316
|
* @returns { Promise<any> }
|
|
311
317
|
*/
|
|
312
|
-
// TODO find a better place.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (err.code
|
|
318
|
-
|
|
319
|
-
const { pathToFileURL } = require('url')
|
|
320
|
-
return import (pathToFileURL(id).href) // must use a file: URL, esp. on Windows for C:\... paths
|
|
318
|
+
exports._import = id => { // TODO find a better place.
|
|
319
|
+
if (id.endsWith('.mjs')) return _import (id)
|
|
320
|
+
if (id.endsWith('.cjs')) return require (id)
|
|
321
|
+
else try { return require(id) } catch (err) {
|
|
322
|
+
if (err.message === 'Cannot use import statement outside a module') return _import (id) // for jest
|
|
323
|
+
if (err.code === 'ERR_REQUIRE_ESM') return _import (id)
|
|
324
|
+
else throw err
|
|
321
325
|
}
|
|
322
326
|
}
|
|
327
|
+
const _import = process.platform === 'win32' ? (()=>{
|
|
328
|
+
const url = require('url') // On Windows we must use a file: URL, esp. for C:\... paths
|
|
329
|
+
return id => import (url.pathToFileURL(id).href)
|
|
330
|
+
})() : id => import (id)
|
|
331
|
+
|
|
323
332
|
|
|
324
333
|
const SECRETS = /(passw)|(cert)|(ca)|(secret)|(key)/i
|
|
325
334
|
/**
|
|
@@ -349,7 +358,7 @@ exports.redacted = function _redacted(cred) {
|
|
|
349
358
|
|
|
350
359
|
|
|
351
360
|
/**
|
|
352
|
-
* A variant of child_process.exec that returns a promise,
|
|
361
|
+
* A variant of child_process.exec that returns a promise,
|
|
353
362
|
* which resolves with the command's stdout split into lines.
|
|
354
363
|
* @example
|
|
355
364
|
* await cds.utils.sh `npm ls -lp --depth=0`
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// This module monkey patches ./tar.js to work on Windows, where tar does not work properly w/o these changes.
|
|
2
|
+
|
|
3
|
+
exports = module.exports = require('./tar')
|
|
4
|
+
|
|
5
|
+
exports._spawn_tar_c = (dir, args) => {
|
|
6
|
+
args.push('.')
|
|
7
|
+
if (Array.isArray(args[0])) return winSpawnTempDir(dir, args)
|
|
8
|
+
else return winSpawnDir(dir, args)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
exports._path = path => {
|
|
12
|
+
if (!path) return path
|
|
13
|
+
if (typeof path === 'string') return path.replace('C:', '//localhost/c$').replace(/\\+/g, '/')
|
|
14
|
+
if (Array.isArray(path)) return path.map(el => exports._path(el))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
const child_process = require('child_process')
|
|
19
|
+
const spawn = /\btar\b/.test(process.env.DEBUG) ? (cmd, args, options) => {
|
|
20
|
+
Error.captureStackTrace(spawn,spawn)
|
|
21
|
+
process.stderr.write(cmd +' ', args.join(' ') +' '+ spawn.stack.slice(7) + '\n')
|
|
22
|
+
return child_process.spawn(cmd, args, options)
|
|
23
|
+
} : child_process.spawn
|
|
24
|
+
|
|
25
|
+
const cds = require('../index'), { fs, path, exists, rimraf } = cds.utils
|
|
26
|
+
const { PassThrough } = require('stream')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
// spawn tar on Windows, using the cli version
|
|
31
|
+
const winSpawnDir = (dir, args) => {
|
|
32
|
+
if (args.some(arg => arg === '-f')) return spawn ('tar', ['c', '-C', exports._path(dir), ...exports._path(args)])
|
|
33
|
+
else return spawn ('tar', ['cf', '-', '-C', exports._path(dir), ...exports._path(args)])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// copy a directory recursively on Windows, using fs.promises
|
|
37
|
+
async function winCopyDir(src, dest) {
|
|
38
|
+
if ((await fs.promises.stat(src)).isDirectory()) {
|
|
39
|
+
const entries = await fs.promises.readdir(src)
|
|
40
|
+
return Promise.all(entries.map(async each => winCopyDir(path.join(src, each), path.join(dest, each))))
|
|
41
|
+
} else {
|
|
42
|
+
await fs.promises.mkdir(path.dirname(dest), { recursive: true })
|
|
43
|
+
return fs.promises.copyFile(src, dest)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// copy resources containing files and folders to temp dir on Windows
|
|
48
|
+
// cli tar has a size limit on Windows
|
|
49
|
+
const winCreateTemp = async (root, resources) => {
|
|
50
|
+
// Asynchronously copies the entire content from src to dest.
|
|
51
|
+
const temp = await fs.promises.mkdtemp(`${fs.realpathSync(require('os').tmpdir())}${path.sep}tar-`)
|
|
52
|
+
for (let resource of resources) {
|
|
53
|
+
const destination = path.join(temp, path.relative(root, resource))
|
|
54
|
+
if ((await fs.promises.stat(resource)).isFile()) {
|
|
55
|
+
const dirName = path.dirname(destination)
|
|
56
|
+
if (!await exists(dirName)) {
|
|
57
|
+
await fs.promises.mkdir(dirName, { recursive: true })
|
|
58
|
+
}
|
|
59
|
+
await fs.promises.copyFile(resource, destination)
|
|
60
|
+
} else {
|
|
61
|
+
if (fs.promises.cp) {
|
|
62
|
+
await fs.promises.cp(resource, destination, { recursive: true })
|
|
63
|
+
} else {
|
|
64
|
+
// node < 16
|
|
65
|
+
await winCopyDir(resource, destination)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return temp
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// spawn tar on Windows, using a temp dir, which is copied from the original dir
|
|
74
|
+
// cli tar has a size limit on Windows
|
|
75
|
+
const winSpawnTempDir = (dir, args) => {
|
|
76
|
+
// Synchronous trick: use a PassThrough as placeholder
|
|
77
|
+
const stdout = new PassThrough()
|
|
78
|
+
const stderr = new PassThrough()
|
|
79
|
+
const c = {
|
|
80
|
+
stdout,
|
|
81
|
+
stderr,
|
|
82
|
+
on: (...a) => { stdout.on(...a); stderr.on(...a); return c },
|
|
83
|
+
once: (...a) => { stdout.once(...a); stderr.once(...a); return c },
|
|
84
|
+
kill: () => {},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// async copy, then swap streams/events
|
|
88
|
+
winCreateTemp(dir, args.shift()).then(tempPath => {
|
|
89
|
+
const real = winSpawnDir(tempPath, args)
|
|
90
|
+
real.stdout.pipe(stdout)
|
|
91
|
+
real.stderr && real.stderr.pipe(stderr)
|
|
92
|
+
const cleanup = () => exists(tempPath) && rimraf(tempPath)
|
|
93
|
+
real.on('close', (...ev) => {
|
|
94
|
+
stdout.emit('close', ...ev)
|
|
95
|
+
stderr.emit('close', ...ev)
|
|
96
|
+
cleanup()
|
|
97
|
+
})
|
|
98
|
+
real.on('error', (...ev) => {
|
|
99
|
+
stdout.emit('error', ...ev)
|
|
100
|
+
stderr.emit('error', ...ev)
|
|
101
|
+
cleanup()
|
|
102
|
+
})
|
|
103
|
+
c.kill = (...ev) => real.kill(...ev)
|
|
104
|
+
})
|
|
105
|
+
return c
|
|
106
|
+
}
|
package/lib/utils/tar.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const { PassThrough } = require('stream')
|
|
2
1
|
const child_process = require('child_process')
|
|
3
2
|
const spawn = /\btar\b/.test(process.env.DEBUG) ? (cmd, args, options) => {
|
|
4
3
|
Error.captureStackTrace(spawn,spawn)
|
|
@@ -6,137 +5,9 @@ const spawn = /\btar\b/.test(process.env.DEBUG) ? (cmd, args, options) => {
|
|
|
6
5
|
return child_process.spawn(cmd, args, options)
|
|
7
6
|
} : child_process.spawn
|
|
8
7
|
|
|
9
|
-
const cds = require('../index'), { fs, path, mkdirp
|
|
8
|
+
const cds = require('../index'), { fs, path, mkdirp } = cds.utils
|
|
10
9
|
const _resolve = (...x) => path.resolve (cds.root,...x)
|
|
11
10
|
|
|
12
|
-
// ======= ONLY_FOR_WINDOWS ======
|
|
13
|
-
// This section contains logic relevant for Windows OS.
|
|
14
|
-
|
|
15
|
-
// tar does not work properly on Windows w/o this change
|
|
16
|
-
const win = path => {
|
|
17
|
-
if (!path) return path
|
|
18
|
-
if (typeof path === 'string') return path.replace('C:', '//localhost/c$').replace(/\\+/g, '/')
|
|
19
|
-
if (Array.isArray(path)) return path.map(el => win(el))
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// spawn tar on Windows, using the cli version
|
|
23
|
-
const winSpawnDir = (dir, args) => {
|
|
24
|
-
if (args.some(arg => arg === '-f')) return spawn ('tar', ['c', '-C', win(dir), ...win(args)])
|
|
25
|
-
else return spawn ('tar', ['cf', '-', '-C', win(dir), ...win(args)])
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// copy a directory recursively on Windows, using fs.promises
|
|
29
|
-
async function winCopyDir(src, dest) {
|
|
30
|
-
if ((await fs.promises.stat(src)).isDirectory()) {
|
|
31
|
-
const entries = await fs.promises.readdir(src)
|
|
32
|
-
return Promise.all(entries.map(async each => winCopyDir(path.join(src, each), path.join(dest, each))))
|
|
33
|
-
} else {
|
|
34
|
-
await fs.promises.mkdir(path.dirname(dest), { recursive: true })
|
|
35
|
-
return fs.promises.copyFile(src, dest)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// copy resources containing files and folders to temp dir on Windows
|
|
40
|
-
// cli tar has a size limit on Windows
|
|
41
|
-
const winCreateTemp = async (root, resources) => {
|
|
42
|
-
// Asynchronously copies the entire content from src to dest.
|
|
43
|
-
const temp = await fs.promises.mkdtemp(`${fs.realpathSync(require('os').tmpdir())}${path.sep}tar-`)
|
|
44
|
-
for (let resource of resources) {
|
|
45
|
-
const destination = path.join(temp, path.relative(root, resource))
|
|
46
|
-
if ((await fs.promises.stat(resource)).isFile()) {
|
|
47
|
-
const dirName = path.dirname(destination)
|
|
48
|
-
if (!await exists(dirName)) {
|
|
49
|
-
await fs.promises.mkdir(dirName, { recursive: true })
|
|
50
|
-
}
|
|
51
|
-
await fs.promises.copyFile(resource, destination)
|
|
52
|
-
} else {
|
|
53
|
-
if (fs.promises.cp) {
|
|
54
|
-
await fs.promises.cp(resource, destination, { recursive: true })
|
|
55
|
-
} else {
|
|
56
|
-
// node < 16
|
|
57
|
-
await winCopyDir(resource, destination)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return temp
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// spawn tar on Windows, using a temp dir, which is copied from the original dir
|
|
66
|
-
// cli tar has a size limit on Windows
|
|
67
|
-
const winSpawnTempDir = (dir, args) => {
|
|
68
|
-
// Synchronous trick: use a PassThrough as placeholder
|
|
69
|
-
const stdout = new PassThrough()
|
|
70
|
-
const stderr = new PassThrough()
|
|
71
|
-
const c = {
|
|
72
|
-
stdout,
|
|
73
|
-
stderr,
|
|
74
|
-
on: (...a) => { stdout.on(...a); stderr.on(...a); return c },
|
|
75
|
-
once: (...a) => { stdout.once(...a); stderr.once(...a); return c },
|
|
76
|
-
kill: () => {},
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// async copy, then swap streams/events
|
|
80
|
-
winCreateTemp(dir, args.shift()).then(tempPath => {
|
|
81
|
-
const real = winSpawnDir(tempPath, args)
|
|
82
|
-
real.stdout.pipe(stdout)
|
|
83
|
-
real.stderr && real.stderr.pipe(stderr)
|
|
84
|
-
const cleanup = () => exists(tempPath) && rimraf(tempPath)
|
|
85
|
-
real.on('close', (...ev) => {
|
|
86
|
-
stdout.emit('close', ...ev)
|
|
87
|
-
stderr.emit('close', ...ev)
|
|
88
|
-
cleanup()
|
|
89
|
-
})
|
|
90
|
-
real.on('error', (...ev) => {
|
|
91
|
-
stdout.emit('error', ...ev)
|
|
92
|
-
stderr.emit('error', ...ev)
|
|
93
|
-
cleanup()
|
|
94
|
-
})
|
|
95
|
-
c.kill = (...ev) => real.kill(...ev)
|
|
96
|
-
})
|
|
97
|
-
return c
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ====== END ONLY_FOR_WINDOWS ======
|
|
101
|
-
|
|
102
|
-
const tarInfo = async (info) => {
|
|
103
|
-
let cmd, param
|
|
104
|
-
if (info === 'version') {
|
|
105
|
-
cmd = 'tar'
|
|
106
|
-
param = ['--version']
|
|
107
|
-
} else {
|
|
108
|
-
cmd = process.platform === 'win32' ? 'where' : 'which'
|
|
109
|
-
param = ['tar']
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const c = spawn (cmd, param)
|
|
113
|
-
|
|
114
|
-
return {__proto__:c,
|
|
115
|
-
then (resolve, reject) {
|
|
116
|
-
let data=[], stderr=''
|
|
117
|
-
c.stdout.on('data', d => {
|
|
118
|
-
data.push(d)
|
|
119
|
-
})
|
|
120
|
-
c.stderr.on('data', d => stderr += d)
|
|
121
|
-
c.on('close', code => {
|
|
122
|
-
code ? reject(new Error(stderr)) : resolve(Buffer.concat(data).toString().replace(/\n/g,'').replace(/\r/g,''))
|
|
123
|
-
})
|
|
124
|
-
c.on('error', reject)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const logDebugTar = async () => {
|
|
130
|
-
const LOG = cds.log('tar')
|
|
131
|
-
if (!LOG?._debug) return
|
|
132
|
-
try {
|
|
133
|
-
LOG (`tar path: ${await tarInfo('path')}`)
|
|
134
|
-
LOG (`tar version: ${await tarInfo('version')}`)
|
|
135
|
-
} catch (err) {
|
|
136
|
-
LOG('tar error', err)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
11
|
/**
|
|
141
12
|
* Creates a tar archive, to an in-memory Buffer, or piped to write stream or file.
|
|
142
13
|
* @example ```js
|
|
@@ -162,26 +33,12 @@ const logDebugTar = async () => {
|
|
|
162
33
|
* - `.to()` is a convenient shortcut to pipe the output into a write stream
|
|
163
34
|
*/
|
|
164
35
|
exports.create = (dir='.', ...args) => {
|
|
165
|
-
|
|
36
|
+
|
|
166
37
|
if (typeof dir === 'string') dir = _resolve(dir)
|
|
167
38
|
if (Array.isArray(dir)) [ dir, ...args ] = [ cds.root, dir, ...args ]
|
|
168
|
-
|
|
169
|
-
let c
|
|
170
39
|
args = args.filter(el => el)
|
|
171
|
-
if (process.platform === 'win32') {
|
|
172
|
-
args.push('.')
|
|
173
|
-
if (Array.isArray(args[0])) c = winSpawnTempDir(dir, args)
|
|
174
|
-
else c = winSpawnDir(dir, args)
|
|
175
|
-
} else {
|
|
176
|
-
if (Array.isArray(args[0])) {
|
|
177
|
-
args.push (...args.shift().map (f => path.isAbsolute(f) ? path.relative(dir,f) : f))
|
|
178
|
-
} else {
|
|
179
|
-
args.push('.')
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
c = spawn ('tar', ['c', '-C', dir, ...args], { env: { COPYFILE_DISABLE: 1 }})
|
|
183
|
-
}
|
|
184
40
|
|
|
41
|
+
const c = exports._spawn_tar_c (dir, args)
|
|
185
42
|
return {__proto__:c, // returning a thenable + fluent ChildProcess...
|
|
186
43
|
|
|
187
44
|
/**
|
|
@@ -219,6 +76,22 @@ exports.create = (dir='.', ...args) => {
|
|
|
219
76
|
}
|
|
220
77
|
}
|
|
221
78
|
|
|
79
|
+
|
|
80
|
+
// Extracted to allow os-specific implementations, e.g. for win32
|
|
81
|
+
exports._spawn_tar_c = (dir, args) => {
|
|
82
|
+
if (Array.isArray(args[0])) {
|
|
83
|
+
args.push (...args.shift().map (f => path.isAbsolute(f) ? path.relative(dir,f) : f))
|
|
84
|
+
} else {
|
|
85
|
+
args.push ('.')
|
|
86
|
+
}
|
|
87
|
+
return spawn ('tar', ['c', '-C', dir, ...args], { env: { COPYFILE_DISABLE: 1 }})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
// Extracted to allow os-specific implementations, e.g. for win32
|
|
92
|
+
exports._path = p => p
|
|
93
|
+
|
|
94
|
+
|
|
222
95
|
/**
|
|
223
96
|
* Extracts a tar archive, from an in-memory Buffer, or piped from a read stream or file.
|
|
224
97
|
* @example ```js
|
|
@@ -242,7 +115,7 @@ exports.extract = (archive, ...args) => ({
|
|
|
242
115
|
to (...dest) {
|
|
243
116
|
if (typeof dest === 'string') dest = _resolve(...dest)
|
|
244
117
|
const input = typeof archive !== 'string' || archive == '-' ? '-' : _resolve(archive)
|
|
245
|
-
const x = spawn('tar', ['xf',
|
|
118
|
+
const x = spawn('tar', ['xf', exports._path(input), '-C', exports._path(dest), ...args])
|
|
246
119
|
if (archive === '-') return x.stdin
|
|
247
120
|
if (Buffer.isBuffer(archive)) archive = require('stream').Readable.from (archive)
|
|
248
121
|
if (typeof archive !== 'string') (archive.stdout || archive) .pipe (x.stdin)
|
|
@@ -251,8 +124,8 @@ exports.extract = (archive, ...args) => ({
|
|
|
251
124
|
x.stderr.on ('data', d => stderr += d)
|
|
252
125
|
return {__proto__:x,
|
|
253
126
|
then (resolve, reject) {
|
|
254
|
-
x.on('close',
|
|
255
|
-
if (
|
|
127
|
+
x.on('close', err => {
|
|
128
|
+
if (err) return reject (new Error(stderr))
|
|
256
129
|
if (process.platform === 'linux') stdout = stderr
|
|
257
130
|
resolve (stdout ? stdout.split('\n').slice(0,-1).map(x => x.replace(/^x |\r/g,'')): undefined)
|
|
258
131
|
})
|
|
@@ -304,12 +177,4 @@ exports.t = tar.tf = tar.list
|
|
|
304
177
|
* @example fs.createReadStream('t.tar') .pipe (tar.x.to('dest/dir'))
|
|
305
178
|
* @returns `stdin` of the tar child process
|
|
306
179
|
*/
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
// ---------------------------------------------------------------------------------
|
|
312
|
-
// Compatibility...
|
|
313
|
-
|
|
314
|
-
exports.packTarArchive = (resources,d) => d ? tar.cz (d,resources) : tar.cz (resources)
|
|
315
|
-
exports.unpackTarArchive = (x,dir) => tar.xz(x).to(dir)
|
|
180
|
+
exports.extract.to = function (..._) { return this('-').to(..._) }
|