• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

box / boxcli / 3688883245

pending completion
3688883245

Pull #441

github

GitHub
Merge 0c6f2cfe0 into 83ac6d7c8
Pull Request #441: fix: single file upload on Node 16

1077 of 1387 branches covered (77.65%)

Branch coverage included in aggregate %.

35 of 54 new or added lines in 9 files covered. (64.81%)

1 existing line in 1 file now uncovered.

3935 of 4421 relevant lines covered (89.01%)

121.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.16
/src/util.js
1
'use strict';
2

3
const _ = require('lodash');
276✔
4
const BoxCLIError = require('./cli-error');
276✔
5
const os = require('os');
276✔
6
const path = require('path');
276✔
7
const fs = require('fs');
276✔
8
const mkdirp = require('mkdirp');
276✔
9

10
const REQUIRED_CONFIG_VALUES = Object.freeze([
276✔
11
        'boxAppSettings.clientID',
12
        'boxAppSettings.clientSecret',
13
        'boxAppSettings.appAuth.publicKeyID',
14
        'boxAppSettings.appAuth.passphrase',
15
        'enterpriseID',
16
]);
17

18
const REQUIRED_CONFIG_VALUES_CCG = Object.freeze([
276✔
19
        'boxAppSettings.clientID',
20
        'boxAppSettings.clientSecret',
21
        'enterpriseID',
22
]);
23

24
const NUMBER_REGEX = /^[-+]?\d*\.?\d+$/u;
276✔
25
const UNESCAPED_COMMA_REGEX = /(?<![^\\](?:\\\\)*\\),/gu;
276✔
26
const UNESCAPED_SUBSCRIPT_REGEX =
27
        /(?<![^\\](?:\\\\)*\\)\[(.*?)(?<![^\\](?:\\\\)*\\)\]/gu;
276✔
28

29
const PATH_ESCAPES = Object.freeze({
276✔
30
        '/': '~1',
31
        '~': '~0',
32
});
33

34
/**
35
 * Unescape a string that no longer needs to be escaped with slashes,
36
 * optionally replacing certain escaped characters with new
37
 * escape codes.
38
 *
39
 * @param {string} str The string to unescape
40
 * @param {Object} replacements A mapping of escaped characters to new escape codes
41
 * @returns {string} The unescaped string
42
 * @private
43
 */
44
function unescapeString(str, replacements = {}) {
723✔
45
        let ret = '',
1,503✔
46
                chars = [...str];
1,503✔
47

48
        while (chars.length > 0) {
1,503✔
49
                let char = chars.shift();
9,507✔
50
                if (char === '\\') {
9,507✔
51
                        let nextChar = chars.shift();
84✔
52
                        nextChar = replacements[nextChar] || nextChar;
84✔
53
                        if (nextChar) {
84!
54
                                ret += nextChar;
84✔
55
                        }
56
                } else {
57
                        ret += char;
9,423✔
58
                }
59
        }
60

61
        return ret;
1,503✔
62
}
63

64
/**
65
 * Parse a metadata key into the correct format, including escaping
66
 * for JSON Patch operations.
67
 *
68
 * This method accepts two formats:
69
 * 1) /path/to\/from/key => /path/to~1from/key
70
 * 2) path[to][] => /path/to/-
71
 *
72
 * @param {string} value The value to parse as a metadata key
73
 * @returns {string} The parsed key
74
 * @private
75
 */
76
function parseKey(value) {
77
        if (value.startsWith('/')) {
768✔
78
                // Treat as path
79
                return unescapeString(value, PATH_ESCAPES);
324✔
80
        }
81
        // Treat as key
82
        let parts = value.split(UNESCAPED_SUBSCRIPT_REGEX);
444✔
83
        if (parts[parts.length - 1] === '') {
444✔
84
                parts = parts.slice(0, -1);
39✔
85
        }
86
        return `/${parts
444✔
87
                .map((s) => (s ? unescapeString(s, PATH_ESCAPES) : '-'))
483✔
88
                .join('/')}`;
89
}
90

91
/**
92
 * Parses a metadata value from command-line input string
93
 * to actual value.
94
 *
95
 * This method supports the following formats:
96
 * 1) "#123.4" => 123.4
97
 * 2) "[foo,bar]" => [ "foo", "bar" ]
98
 * 3) Everything else => unescaped string
99
 *
100
 * @param {string} value The command-line value to parse
101
 * @returns {string|number|string[]} The parsed value
102
 * @private
103
 */
104
function parseValue(value) {
105
        if (value.startsWith('#')) {
609✔
106
                // Try parsing as number
107
                let valueStr = unescapeString(value.substr(1));
198✔
108
                if (valueStr.match(NUMBER_REGEX)) {
198✔
109
                        let parsedValue = parseFloat(valueStr);
174✔
110
                        if (!Number.isNaN(parsedValue)) {
174!
111
                                return parsedValue;
174✔
112
                        }
113
                }
114

115
                // Parsing failed, fall back to string value
116
        } else if (value.startsWith('[') && value.endsWith(']')) {
411✔
117
                // Handle as array
118
                let interiorStr = value.substring(1, value.length - 1);
150✔
119

120
                if (interiorStr.length === 0) {
150✔
121
                        return [];
30✔
122
                }
123

124
                return interiorStr
120✔
125
                        .split(UNESCAPED_COMMA_REGEX)
126
                        .map((el) => unescapeString(el));
240✔
127
        }
128

129
        // No other parsing applied; treat as string
130
        return unescapeString(value);
285✔
131
}
132

133
/**
134
 * Parses a metadata command line string into the correct operation
135
 * object.
136
 *
137
 * This method performs the following transformations:
138
 * 1) key=value => {path: '/key', value: 'value'}
139
 * 2) key1>key2 => {from: '/key1', path: '/key2'}
140
 *
141
 * @param {string} input The input string from the command line
142
 * @returns {Object} The parsed metadata operation object
143
 * @private
144
 */
145
function parseMetadataString(input) {
146
        let chars = [...input];
705✔
147
        let op = {};
705✔
148

149
        // Find the splitting point, if one exists
150
        let splitIndex = chars.findIndex((char, index, arr) => {
705✔
151
                if (char === '>' || char === '=') {
6,330✔
152
                        let escaped = false;
696✔
153
                        for (let i = index - 1; i >= 0; i--) {
696✔
154
                                if (arr[i] === '\\') {
732✔
155
                                        escaped = !escaped;
36✔
156
                                } else {
157
                                        break;
696✔
158
                                }
159
                        }
160

161
                        if (!escaped) {
696✔
162
                                return true;
672✔
163
                        }
164
                }
165

166
                return false;
5,658✔
167
        });
168

169
        if (splitIndex === -1) {
705✔
170
                // This is just a key, return it
171
                op.path = parseKey(chars.join(''));
33✔
172
                return op;
33✔
173
        }
174

175
        let separator = chars[splitIndex];
672✔
176
        if (separator === '>') {
672✔
177
                op.from = parseKey(chars.slice(0, splitIndex).join(''));
63✔
178
                op.path = parseKey(chars.slice(splitIndex + 1).join(''));
63✔
179
        } else {
180
                op.path = parseKey(chars.slice(0, splitIndex).join(''));
609✔
181
                op.value = parseValue(chars.slice(splitIndex + 1).join(''));
609✔
182
        }
183

184
        return op;
672✔
185
}
186

187
/**
188
 * Check if directory exists and creates it if shouldCreate flag was passed.
189
 *
190
 * @param {string} dirPath Directory path to check and create
191
 * @param {boolean} shouldCreate Flag indicating if the directory should be created
192
 * @returns {Promise<void>} empty promise
193
 * @throws BoxCLIError
194
 */
195
async function checkDir(dirPath, shouldCreate) {
196
        /* eslint-disable no-sync */
197
        if (!fs.existsSync(dirPath)) {
57✔
198
                if (shouldCreate) {
42✔
199
                        await mkdirp(dirPath);
36✔
200
                } else {
201
                        throw new BoxCLIError(
6✔
202
                                `The ${dirPath} path does not exist. Either create it, or pass the --create-path flag set to true`
203
                        );
204
                }
205
        }
206
}
207

208
/* eslint-disable require-jsdoc, require-await, no-shadow, promise/avoid-new, promise/prefer-await-to-callbacks */
209

210
async function readFileAsync(path, options) {
NEW
211
        return new Promise((resolve, reject) => {
×
NEW
212
                fs.readFile(path, options || {}, (err, result) => {
×
NEW
213
                        if (err) {
×
NEW
214
                                return reject(err);
×
215
                        }
NEW
216
                        return resolve(result);
×
217
                });
218
        });
219
}
220

221
async function writeFileAsync(file, data, options) {
222
        return new Promise((resolve, reject) => {
3✔
223
                fs.writeFile(file, data, options || {}, (err, result) => {
3!
224
                        if (err) {
3!
NEW
225
                                return reject(err);
×
226
                        }
227
                        return resolve(result);
3✔
228
                });
229
        });
230
}
231

232
async function readdirAsync(path, options) {
233
        return new Promise((resolve, reject) => {
12✔
234
                fs.readdir(path, options || {}, (err, result) => {
12✔
235
                        if (err) {
12!
NEW
236
                                return reject(err);
×
237
                        }
238
                        return resolve(result);
12✔
239
                });
240
        });
241
}
242

243
async function unlinkAsync(path, options) {
NEW
244
        return new Promise((resolve, reject) => {
×
NEW
245
                fs.unlink(path, options || {}, (err, result) => {
×
NEW
246
                        if (err) {
×
NEW
247
                                return reject(err);
×
248
                        }
NEW
249
                        return resolve(result);
×
250
                });
251
        });
252
}
253

254
/* eslint-enable require-jsdoc, require-await, no-shadow, promise/avoid-new, promise/prefer-await-to-callbacks */
255

256
module.exports = {
276✔
257
        /**
258
         * Validates the a configuration object has all required properties
259
         * @param {Object} configObj The config object to validate
260
         * @param {boolean} isCCG Whether the config object is used for CCG auth
261
         * @returns {void}
262
         * @throws BoxCLIError
263
         */
264
        validateConfigObject(configObj, isCCG) {
265
                let checkProp = _.propertyOf(configObj);
×
266
                let requiredConfigValues = isCCG
×
267
                        ? REQUIRED_CONFIG_VALUES_CCG
268
                        : REQUIRED_CONFIG_VALUES;
269
                let missingProp = requiredConfigValues.find((key) => !checkProp(key));
×
270

271
                if (missingProp) {
×
272
                        throw new BoxCLIError(`Config object missing key ${missingProp}`);
×
273
                }
274
        },
275

276
        /**
277
         * Parses a path argument or flag value into a full absolute path
278
         *
279
         * @param {string} value The raw path string from the command line flag or arg
280
         * @returns {string} The resolved absolute path
281
         */
282
        parsePath(value) {
283
                // Check for homedir and expand if necessary
284
                // @NOTE: This can occur when the user passes a path via --flag="~/my-stuff" syntax, since the shell
285
                // doesn't get a chance to expand the tilde
286
                value = value.replace(
291✔
287
                        /^~(\/|\\|$)/u,
288
                        (match, ending) => os.homedir() + ending
6✔
289
                );
290

291
                return path.resolve(value);
291✔
292
        },
293

294
        /**
295
         * Parses the key=val string format for metadata into an object {key: val}
296
         *
297
         * @param {string} value The string containing metadata key and value
298
         * @returns {Object} The parsed metadata key and value
299
         */
300
        parseMetadata(value) {
301
                let op = parseMetadataString(value);
333✔
302
                if (!op.hasOwnProperty('path') || !op.hasOwnProperty('value')) {
333!
303
                        throw new BoxCLIError('Metadata must be in the form key=value');
×
304
                }
305

306
                let pathSegments = op.path.slice(1).split('/');
333✔
307
                if (pathSegments.length !== 1) {
333!
308
                        throw new BoxCLIError(
×
309
                                `Metadata value must be assigned to a top-level key, instead got ${op.path}`
310
                        );
311
                }
312

313
                let key = pathSegments[0];
333✔
314

315
                return { [key]: op.value };
333✔
316
        },
317

318
        /**
319
         * Parses strings like key=value or key1>key2 into objects
320
         * used to help define JSON patch operations.
321
         *
322
         * key=value => {path: key, value: value}
323
         * key1>key2 => {from: key1, path: key2}
324
         * other => {path: other}
325
         *
326
         * @param {string} value The input string
327
         * @returns {Object} The parsed operation object
328
         */
329
        parseMetadataOp(value) {
330
                return parseMetadataString(value);
372✔
331
        },
332
        checkDir,
333
        readFileAsync,
334
        writeFileAsync,
335
        readdirAsync,
336
        unlinkAsync,
337
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc