@xeplr/actions 1.4.6 → 1.4.7
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/README.md +18 -1
- package/lib/builtins/file/list.js +60 -0
- package/lib/builtins/file/move.js +62 -7
- package/lib/builtins/file/read.js +77 -0
- package/lib/builtins/index.js +2 -0
- package/lib/fileRoots.js +71 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,7 +125,10 @@ Register the ones you want (`register(actions.builtins.dbFetch)`) or pass a modu
|
|
|
125
125
|
| `emailDelete` | `email-delete` | Flag `\Deleted` **and expunge** — permanent, not a move to Trash. |
|
|
126
126
|
| `emailDownloadEmail` | `email-download-email` | Stream the raw message to an `.eml` file, named by `fileName`. `{ count, saved[], path, existing[] }`. |
|
|
127
127
|
| `emailDownloadAttachments` | `email-download-attachments` | Save attachments, optionally filtered by `extension` / `filenameContains`, named by `fileName` (see [Saving files](#saving-files-names-and-a-name-already-taken)). `{ count, saved[], path, skipped, existing[] }`. |
|
|
128
|
-
| `
|
|
128
|
+
| `fileList` | `file-list` | The files in a folder, optionally matching `pattern` (`*.csv`, `Bank-2026-??.pdf`), sorted by name or date. `{ files[{ name, path, size, modifiedAt }], count }`. "Is it there?" is the exact name and `count`. See [File steps](#file-steps-and-the-folders-they-may-use). |
|
|
129
|
+
| `fileRead` | `file-read` | A CSV, text, Excel, JSON or NDJSON file as rows (format from the extension). `{ rows, count, columns, truncated }`, at most `maxRows` (50,000). |
|
|
130
|
+
| `fileMove` | `file-move` | Move or copy a file into a folder (made if missing) or to a new name; `ifExists` as every saving step. `{ path, name, moved, skipped }`. |
|
|
131
|
+
| `httpRequest`, `spawnProgram`, `dbQuery` | — | **Placeholders.** Empty modules with no `name` or `execute`; `register()` rejects them. |
|
|
129
132
|
|
|
130
133
|
### Connections
|
|
131
134
|
|
|
@@ -245,6 +248,19 @@ A job or a workflow step stores `connectionInfoId`, never a password. `resolveCo
|
|
|
245
248
|
- **`resolver(id, { field, inputs })`** is the host's: a login, or `{ dbType, connection }`. It sees all the inputs, for a companion id such as BI's `dbInfoId`.
|
|
246
249
|
- **An id that resolves to nothing is refused** (`CONNECTION_NOT_FOUND`), never replaced by a literal `connection`. An id with no resolver configured: `NO_CONNECTION_RESOLVER`.
|
|
247
250
|
|
|
251
|
+
## File steps, and the folders they may use
|
|
252
|
+
|
|
253
|
+
`file-list`, `file-read` and `file-move` take a path from whoever wrote the workflow, so they work **only inside the folders the install names** (`lib/fileRoots.js`):
|
|
254
|
+
|
|
255
|
+
| | |
|
|
256
|
+
|---|---|
|
|
257
|
+
| `XEPLR_FILE_ROOTS` | Absolute folders, comma-separated: `/data/bank,/data/erp`. |
|
|
258
|
+
| always allowed | `XEPLR_ACTIONS_TMP_DIR` (where downloads and exports land by default), so the next step can read what the last one saved. |
|
|
259
|
+
|
|
260
|
+
- **Paths are absolute.** A relative one is refused: it would mean different files depending on where the server was started.
|
|
261
|
+
- **No way out:** `../` and symbolic links are followed to the real path before it is checked; a target that does not exist yet is checked by its nearest existing folder.
|
|
262
|
+
- **Not defaulted:** with no `XEPLR_FILE_ROOTS`, a path outside the temp folder is refused (`PATH_NOT_ALLOWED`), naming the setting. Why: without it, a workflow could read any file the server can, its own settings included.
|
|
263
|
+
|
|
248
264
|
## Saving files: names, and a name already taken
|
|
249
265
|
|
|
250
266
|
Every action that saves a file follows one rule (`lib/fileNames.js`): `email-download-attachments`, `email-download-email` and `export-table`.
|
|
@@ -461,6 +477,7 @@ await actions.runAction(actions.builtins.dbPush, input, { metaStore: xcfg.metaSt
|
|
|
461
477
|
| name | required? | meaning |
|
|
462
478
|
|---|---|---|
|
|
463
479
|
| `XEPLR_ACTIONS_TMP_DIR` | no | Where `db-fetch` and `email-read` spool NDJSON, email downloads go (unless `outDir`), and the uploader spools batches. Default `<os.tmpdir()>/xeplr-actions`. Files are not cleaned up by `db-fetch` or `email-read`. |
|
|
480
|
+
| `XEPLR_FILE_ROOTS` | for `file-list`, `file-read`, `file-move` outside the temp folder | Absolute folders, comma-separated, that file steps may use. See [File steps](#file-steps-and-the-folders-they-may-use). |
|
|
464
481
|
| `XCFG_DB_NAME` | for `attachConfig` | The `xeplr_configs` database name. In `configRequiredEnv`. |
|
|
465
482
|
| `XCFG_DB_CONNECTION_INFO_ENCRYPTED` | no | Encrypted server login for `attachConfig`; overrides `XEPLR_DB_CONNECTION`. |
|
|
466
483
|
| `XEPLR_DB_CONNECTION` | for `attachConfig`, unless the above is set | Shared encrypted server login. |
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// file-list action — the files in a folder, optionally matching a pattern.
|
|
2
|
+
//
|
|
3
|
+
// Also the way to ask "is it there yet?": list with the exact name and branch
|
|
4
|
+
// on output.count. Only inside the folders file steps may use (lib/fileRoots.js).
|
|
5
|
+
|
|
6
|
+
var fs = require('fs');
|
|
7
|
+
var path = require('path');
|
|
8
|
+
var fileRoots = require('../../fileRoots');
|
|
9
|
+
|
|
10
|
+
/** `*.csv`, `Bank-2026-??.pdf` → a test on a name. Case-insensitive. */
|
|
11
|
+
function matcher(pattern) {
|
|
12
|
+
if (!pattern) return function() { return true; };
|
|
13
|
+
var re = new RegExp('^' + String(pattern).split('').map(function(ch) {
|
|
14
|
+
if (ch === '*') return '.*';
|
|
15
|
+
if (ch === '?') return '.';
|
|
16
|
+
return ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
17
|
+
}).join('') + '$', 'i');
|
|
18
|
+
return function(name) { return re.test(name); };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
name: 'file-list',
|
|
23
|
+
description: 'The files in a folder, optionally matching a pattern such as *.csv. Returns { files[], count }. ' +
|
|
24
|
+
'Ask "is it there?" with the exact name and branch on count.',
|
|
25
|
+
requires: [],
|
|
26
|
+
inputSchema: [
|
|
27
|
+
{ name: 'folder', type: 'string', required: true, order: 1,
|
|
28
|
+
description: 'An absolute folder inside XEPLR_FILE_ROOTS, e.g. /data/bank.' },
|
|
29
|
+
{ name: 'pattern', type: 'string', order: 2,
|
|
30
|
+
description: 'Only names matching this: * any characters, ? one. E.g. Bank-{trigger.window.from|date:YYYY-MM}*.csv' },
|
|
31
|
+
{ name: 'sort', type: 'string', default: 'name', options: ['name', 'newest', 'oldest'], order: 3,
|
|
32
|
+
description: 'name: A to Z. newest / oldest: by when the file last changed.' }
|
|
33
|
+
],
|
|
34
|
+
outputSchema: [
|
|
35
|
+
{ name: 'files', type: 'array', description: '[{ name, path, size, modifiedAt }]' },
|
|
36
|
+
{ name: 'count', type: 'number', description: 'How many, so the next step can branch on none.' }
|
|
37
|
+
],
|
|
38
|
+
|
|
39
|
+
execute: async function(ctx) {
|
|
40
|
+
var input = ctx.input || {};
|
|
41
|
+
var dir = fileRoots.allowed(input.folder, 'The folder');
|
|
42
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
43
|
+
var missing = new Error('"' + input.folder + '" is not a folder that exists.');
|
|
44
|
+
missing.code = 'NOT_A_FOLDER';
|
|
45
|
+
throw missing;
|
|
46
|
+
}
|
|
47
|
+
var test = matcher(input.pattern);
|
|
48
|
+
var files = fs.readdirSync(dir, { withFileTypes: true })
|
|
49
|
+
.filter(function(e) { return e.isFile() && test(e.name); })
|
|
50
|
+
.map(function(e) {
|
|
51
|
+
var full = path.join(dir, e.name);
|
|
52
|
+
var st = fs.statSync(full);
|
|
53
|
+
return { name: e.name, path: full, size: st.size, modifiedAt: st.mtime.toISOString() };
|
|
54
|
+
});
|
|
55
|
+
if (input.sort === 'newest') files.sort(function(a, b) { return b.modifiedAt.localeCompare(a.modifiedAt); });
|
|
56
|
+
else if (input.sort === 'oldest') files.sort(function(a, b) { return a.modifiedAt.localeCompare(b.modifiedAt); });
|
|
57
|
+
else files.sort(function(a, b) { return a.name.localeCompare(b.name); });
|
|
58
|
+
return { files: files, count: files.length };
|
|
59
|
+
}
|
|
60
|
+
};
|
|
@@ -1,8 +1,63 @@
|
|
|
1
|
-
// file-move action —
|
|
2
|
-
// location to another. Source + target can each be local/sharepoint/google
|
|
3
|
-
// (i.e. it uses getSource(sourceType) on both ends).
|
|
1
|
+
// file-move action — move or copy a file, into a folder or to a new name.
|
|
4
2
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// "Processed" folders, an archive by month, a copy for another team. Only
|
|
4
|
+
// inside the folders file steps may use (lib/fileRoots.js), at both ends. A
|
|
5
|
+
// name already taken at the target follows the rule every file-writing step
|
|
6
|
+
// follows (lib/fileNames.js): fail by default.
|
|
7
|
+
|
|
8
|
+
var fs = require('fs');
|
|
9
|
+
var path = require('path');
|
|
10
|
+
var fileRoots = require('../../fileRoots');
|
|
11
|
+
var fileNames = require('../../fileNames');
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
name: 'file-move',
|
|
15
|
+
description: 'Move or copy a file into a folder, or to a new name. Returns { path, name, moved, skipped }.',
|
|
16
|
+
requires: [],
|
|
17
|
+
inputSchema: [
|
|
18
|
+
{ name: 'from', type: 'string', required: true, order: 1,
|
|
19
|
+
description: 'The file, by absolute path, e.g. {steps.save.output.path}.' },
|
|
20
|
+
{ name: 'to', type: 'string', required: true, order: 2,
|
|
21
|
+
description: 'A folder (it keeps its name), or a full path to rename it, e.g. /data/archive/{trigger.window.from|date:YYYY-MM}/' },
|
|
22
|
+
{ name: 'operation', type: 'string', default: 'move', options: ['move', 'copy'], order: 3,
|
|
23
|
+
description: 'move: it is no longer where it was. copy: it is in both places.' },
|
|
24
|
+
{ name: 'ifExists', type: 'string', default: 'fail', options: fileNames.IF_EXISTS, order: 4,
|
|
25
|
+
description: 'When the target name is taken. fail: the step fails and nothing moves. skip: left where it is. ' +
|
|
26
|
+
'overwrite: replaced. addSuffix: "name (2).ext".' }
|
|
27
|
+
],
|
|
28
|
+
outputSchema: [
|
|
29
|
+
{ name: 'path', type: 'string' }, { name: 'name', type: 'string' },
|
|
30
|
+
{ name: 'moved', type: 'boolean' }, { name: 'skipped', type: 'boolean' }
|
|
31
|
+
],
|
|
32
|
+
|
|
33
|
+
execute: async function(ctx) {
|
|
34
|
+
var input = ctx.input || {};
|
|
35
|
+
var from = fileRoots.allowed(input.from, 'The file to move');
|
|
36
|
+
if (!fs.existsSync(from) || !fs.statSync(from).isFile()) {
|
|
37
|
+
var missing = new Error('"' + input.from + '" is not a file that exists.');
|
|
38
|
+
missing.code = 'FILE_NOT_FOUND';
|
|
39
|
+
throw missing;
|
|
40
|
+
}
|
|
41
|
+
// A folder: ends with a separator, or is one already.
|
|
42
|
+
var toIsFolder = /[\/\\]$/.test(input.to) || (fs.existsSync(input.to) && fs.statSync(input.to).isDirectory());
|
|
43
|
+
var dir = fileRoots.allowed(toIsFolder ? input.to : path.dirname(input.to), 'The target folder');
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
45
|
+
var wanted = toIsFolder ? path.basename(from) : fileNames.safeName(path.basename(input.to));
|
|
46
|
+
var place = fileNames.plan(dir, [wanted], input.ifExists)[0];
|
|
47
|
+
if (!place.write) return { path: place.path, name: place.name, moved: false, skipped: true };
|
|
48
|
+
|
|
49
|
+
if (input.operation === 'copy') {
|
|
50
|
+
fs.copyFileSync(from, place.path);
|
|
51
|
+
} else {
|
|
52
|
+
try {
|
|
53
|
+
fs.renameSync(from, place.path);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// Another disk: copy, then remove.
|
|
56
|
+
if (err.code !== 'EXDEV') throw err;
|
|
57
|
+
fs.copyFileSync(from, place.path);
|
|
58
|
+
fs.unlinkSync(from);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { path: place.path, name: place.name, moved: input.operation !== 'copy', skipped: false };
|
|
62
|
+
}
|
|
63
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// file-read action — a CSV, Excel, JSON or NDJSON file as rows.
|
|
2
|
+
//
|
|
3
|
+
// What the next step works with: the data step filters them, export-table
|
|
4
|
+
// writes them, a matcher compares them. Only inside the folders file steps may
|
|
5
|
+
// use (lib/fileRoots.js). Bounded by maxRows, because a step's output is
|
|
6
|
+
// stored with the run.
|
|
7
|
+
|
|
8
|
+
var path = require('path');
|
|
9
|
+
var fs = require('fs');
|
|
10
|
+
var readline = require('readline');
|
|
11
|
+
var fileRoots = require('../../fileRoots');
|
|
12
|
+
var formats = require('../../formats');
|
|
13
|
+
|
|
14
|
+
var BY_EXTENSION = { csv: 'csv', txt: 'txt', tsv: 'txt', xlsx: 'excel', xls: 'excel', json: 'json', ndjson: 'ndjson', jsonl: 'ndjson' };
|
|
15
|
+
|
|
16
|
+
async function* ndjsonRows(file) {
|
|
17
|
+
var lines = readline.createInterface({ input: fs.createReadStream(file), crlfDelay: Infinity });
|
|
18
|
+
for await (var line of lines) if (line.trim()) yield JSON.parse(line);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
name: 'file-read',
|
|
23
|
+
description: 'Read a CSV, Excel (.xlsx), JSON or NDJSON file into rows. Returns { rows, count, columns, truncated }.',
|
|
24
|
+
requires: [],
|
|
25
|
+
inputSchema: [
|
|
26
|
+
{ name: 'path', type: 'string', required: true, order: 1,
|
|
27
|
+
description: 'An absolute file path inside XEPLR_FILE_ROOTS, e.g. {steps.save.output.path}.' },
|
|
28
|
+
{ name: 'format', type: 'string', order: 2, options: ['csv', 'txt', 'excel', 'json', 'ndjson'],
|
|
29
|
+
description: 'Empty: from the file\'s extension.' },
|
|
30
|
+
{ name: 'sheet', type: 'string', order: 3, group: 'Excel',
|
|
31
|
+
description: 'Which sheet. Empty: the first.' },
|
|
32
|
+
{ name: 'maxRows', type: 'number', default: 50000, order: 4,
|
|
33
|
+
description: 'Rows to read before stopping (truncated: true). A step\'s rows are stored with the run.' }
|
|
34
|
+
],
|
|
35
|
+
outputSchema: [
|
|
36
|
+
{ name: 'rows', type: 'array' }, { name: 'count', type: 'number' },
|
|
37
|
+
{ name: 'columns', type: 'array', description: 'The file\'s columns, in order: [{ key, header }].' },
|
|
38
|
+
{ name: 'truncated', type: 'boolean' }
|
|
39
|
+
],
|
|
40
|
+
|
|
41
|
+
execute: async function(ctx) {
|
|
42
|
+
var input = ctx.input || {};
|
|
43
|
+
var file = fileRoots.allowed(input.path, 'The file');
|
|
44
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
|
|
45
|
+
var missing = new Error('"' + input.path + '" is not a file that exists.');
|
|
46
|
+
missing.code = 'FILE_NOT_FOUND';
|
|
47
|
+
throw missing;
|
|
48
|
+
}
|
|
49
|
+
var ext = path.extname(file).slice(1).toLowerCase();
|
|
50
|
+
var kind = input.format || BY_EXTENSION[ext];
|
|
51
|
+
if (!kind) {
|
|
52
|
+
var unknown = new Error('Cannot tell how to read ".' + ext + '". Set format: csv, txt, excel, json or ndjson.');
|
|
53
|
+
unknown.code = 'FORMAT_UNKNOWN';
|
|
54
|
+
throw unknown;
|
|
55
|
+
}
|
|
56
|
+
var source;
|
|
57
|
+
if (kind === 'ndjson') {
|
|
58
|
+
source = ndjsonRows(file);
|
|
59
|
+
} else {
|
|
60
|
+
var reader = formats.getFormat(kind);
|
|
61
|
+
formats.checkFormatRequires('file-read', kind, reader);
|
|
62
|
+
var opts = input.sheet ? { sheet: input.sheet } : {};
|
|
63
|
+
source = reader.inputMode === 'path' ? reader.parsePath(file, opts) : reader.parseStream(fs.createReadStream(file), opts);
|
|
64
|
+
}
|
|
65
|
+
var max = Math.max(1, Number(input.maxRows) || 50000);
|
|
66
|
+
var rows = [];
|
|
67
|
+
var truncated = false;
|
|
68
|
+
for await (var row of source) {
|
|
69
|
+
if (rows.length >= max) { truncated = true; break; }
|
|
70
|
+
rows.push(row);
|
|
71
|
+
}
|
|
72
|
+
// The order of the file's own columns, said out loud: a step's output is
|
|
73
|
+
// stored as JSONB, which reorders a row's keys.
|
|
74
|
+
var columns = rows.length ? Object.keys(rows[0]).map(function(k) { return { key: k, header: k }; }) : [];
|
|
75
|
+
return { rows: rows, count: rows.length, columns: columns, truncated: truncated };
|
|
76
|
+
}
|
|
77
|
+
};
|
package/lib/builtins/index.js
CHANGED
|
@@ -48,6 +48,8 @@ module.exports = {
|
|
|
48
48
|
// File actions — factory-shaped for the same reason (meta store binding).
|
|
49
49
|
fileUpload: require('./file/upload'),
|
|
50
50
|
fileMove: require('./file/move'),
|
|
51
|
+
fileList: require('./file/list'),
|
|
52
|
+
fileRead: require('./file/read'),
|
|
51
53
|
|
|
52
54
|
// Email actions. Inbound routes to getInboundProvider(input.emailType)
|
|
53
55
|
// (imap today), outbound to getOutboundProvider (smtp) — see
|
package/lib/fileRoots.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// WHERE A FILE STEP MAY LOOK.
|
|
2
|
+
//
|
|
3
|
+
// file-list, file-read and file-move take a path from whoever wrote the
|
|
4
|
+
// workflow. Unbounded, that is every file the server can read: its own
|
|
5
|
+
// settings, other installs' data. So they work only inside the folders the
|
|
6
|
+
// install names:
|
|
7
|
+
//
|
|
8
|
+
// XEPLR_FILE_ROOTS absolute folders, comma-separated, e.g. /data/bank,/data/erp
|
|
9
|
+
//
|
|
10
|
+
// plus XEPLR_ACTIONS_TMP_DIR (or the system temp folder's xeplr-actions),
|
|
11
|
+
// where downloads and exports land by default, so the file a step just saved
|
|
12
|
+
// can be read by the next one.
|
|
13
|
+
//
|
|
14
|
+
// NOT DEFAULTED: no roots configured and a path outside the temp folder is
|
|
15
|
+
// refused, naming the variable. "Where data lives is never defaulted."
|
|
16
|
+
|
|
17
|
+
var fs = require('fs');
|
|
18
|
+
var os = require('os');
|
|
19
|
+
var path = require('path');
|
|
20
|
+
|
|
21
|
+
function rootsError(message) {
|
|
22
|
+
var err = new Error(message);
|
|
23
|
+
err.code = 'PATH_NOT_ALLOWED';
|
|
24
|
+
err.status = 400;
|
|
25
|
+
return err;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function tmpRoot() {
|
|
29
|
+
return path.resolve(process.env.XEPLR_ACTIONS_TMP_DIR || path.join(os.tmpdir(), 'xeplr-actions'));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The folders a file step may use, resolved and real (symlinks followed). */
|
|
33
|
+
function roots() {
|
|
34
|
+
var listed = String(process.env.XEPLR_FILE_ROOTS || '').split(',')
|
|
35
|
+
.map(function(r) { return r.trim(); })
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
listed.forEach(function(r) {
|
|
38
|
+
if (!path.isAbsolute(r)) throw rootsError('XEPLR_FILE_ROOTS must list absolute folders, and "' + r + '" is not one.');
|
|
39
|
+
});
|
|
40
|
+
return listed.concat([tmpRoot()]).map(function(r) {
|
|
41
|
+
try { return fs.realpathSync(r); } catch (e) { return path.resolve(r); }
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The path, absolute and real, when it is inside an allowed folder; refused
|
|
47
|
+
* otherwise. A path that does not exist yet (a move's target) is checked by
|
|
48
|
+
* its nearest existing parent, so "../" and symlinks cannot step outside.
|
|
49
|
+
*/
|
|
50
|
+
function allowed(p, what) {
|
|
51
|
+
if (!p || typeof p !== 'string') throw rootsError((what || 'A path') + ' is required.');
|
|
52
|
+
if (!path.isAbsolute(p)) throw rootsError((what || 'The path') + ' "' + p + '" must be absolute, e.g. /data/bank/statement.csv.');
|
|
53
|
+
var resolved = path.resolve(p);
|
|
54
|
+
var probe = resolved;
|
|
55
|
+
var rest = '';
|
|
56
|
+
while (!fs.existsSync(probe) && path.dirname(probe) !== probe) {
|
|
57
|
+
rest = path.join(path.basename(probe), rest);
|
|
58
|
+
probe = path.dirname(probe);
|
|
59
|
+
}
|
|
60
|
+
var real = path.join(fs.realpathSync(probe), rest);
|
|
61
|
+
var ok = roots().some(function(r) { return real === r || real.indexOf(r + path.sep) === 0; });
|
|
62
|
+
if (!ok) {
|
|
63
|
+
throw rootsError((what || 'The path') + ' "' + p + '" is outside the folders file steps may use. ' +
|
|
64
|
+
(process.env.XEPLR_FILE_ROOTS
|
|
65
|
+
? 'Allowed: ' + process.env.XEPLR_FILE_ROOTS + ' and the temp folder.'
|
|
66
|
+
: 'Set XEPLR_FILE_ROOTS to the folders they may use (absolute, comma-separated).'));
|
|
67
|
+
}
|
|
68
|
+
return real;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { allowed: allowed, roots: roots, tmpRoot: tmpRoot };
|
package/package.json
CHANGED