-
Notifications
You must be signed in to change notification settings - Fork 1
/
util.js
executable file
·424 lines (368 loc) · 11.9 KB
/
util.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
/*
* util.js
*
* This module includes a bunch of miscellaneous utiltiy functions.
*
* Note that it has the same name as nodes internal util module.
* Node's util is included with require('util') and this module with require('./util')
*/
// updated for RPi "Bookworm"
const path = require('path');
const log = require('./log').logger('util');
const fs = require('fs');
const uuid = require('uuid');
const escapeRE = require('escape-regexp-component');
const exec = require('child_process').exec;
const Q = require('q');
const http = require('http');
const restify = require('restify');
const errors = restify.errors;
let mime;
// Function to dynamically import mime module
(async () => {
mime = await import('mime');
})();
// Run a function that returns a promise. If that promise rejects,
// retry the function at the specified interval for the specified number of retries.
// fn - The function to run
// retries - The number of retries (0 means don't retry at all)
// timeout - The amount of time in milliseconds to wait before retrying
function retry(fn, retries, timeout) {
retries = retries || 0;
timeout = timeout || 0;
return function() {
return fn().then(function(data) {
return data;
}, function(err) {
if(retries === 0) {
throw err;
}
if(err) { log.warn(err); }
if(timeout) {
log.warn('Retrying after ' + timeout + 'ms...');
} else {
log.warn('Retrying...');
}
return Q.delay(timeout).then(function() {
return retry(fn, retries-1, timeout)();
});
});
}
}
// Turn a non-list thing into a list
function listify(x) {
if(x instanceof Array) {
return x;
} else {
return [x];
}
}
// Execute a command in the shell, and call the provided callback with the data from stdout
// command - The command to execute in the shell
// callback - The callback
function doshell(command, callback){
exec(command, function(error, stdout, stderr) {
callback(stdout);
});
}
// Conduct the specified shell operation, and return a promise that resolves upon completion of the command.
// If the command was successful (0 error code) the promise resolves with all the stdout data from the process.
// If the command fails (nonzero error code) the promise rejects with all the stderr data from the process.
// command - The command to execute
// options - Command options
// slient - If true, don't log this command
function doshell_promise(command, options) {
var deferred = Q.defer();
options = options || {};
if(!options.silent) {
log.command(command);
}
try {
exec(command, options, function(err, stdout, stderr) {
if(!options.silent) {
log.stdout(stdout);
log.stderr(stderr);
}
if(err) {
deferred.reject(err);
} else {
deferred.resolve(stdout);
}
});
} catch(e) {
deferred.reject(e);
}
return deferred.promise;
}
// Merge the two objects a and b, extending a with keys/values from b
// force - if true, add keys to be that are not in a. Otherwise only update keys already in a
function extend(a,b, force) {
for(k in b) {
if(a.hasOwnProperty(k) || force) {
if(typeof b[k] === 'object' && b[k] !== null) {
if(typeof a[k] === 'object' && a[k] !== null) {
extend(a[k], b[k]);
} else {
if(force) {
a[k] = b[k];
} else {
log.warn('Object format error in extend.');
}
}
} else {
a[k] = b[k];
}
}
}
}
// Return only the filename for the provided path
exports.filename = function(pathname) {
parts = pathname.split(path.sep);
return parts[parts.legnth-1];
};
// Create a unique filename with the same path/extension as the provided filename
var createUniqueFilename = function (filename) {
var extension = (/[.]/.exec(filename)) ? /[^.]+$/.exec(filename) : undefined;
return uuid.v1() + (extension ? ('.' + extension) : '');
};
// Simple queue, faster than using array.shift
function Queue(){
var queue = [];
var offset = 0;
this.getLength = function(){ return (queue.length - offset); };
this.getContents = function() { return queue; };
this.isEmpty = function(){ return (queue.length === 0); };
this.enqueue = function(item){ queue.push(item); };
this.dequeue = function(){
// if the queue is empty, return immediately
if (queue.length === 0) return undefined;
// store the item at the front of the queue
var item = queue[offset];
// increment the offset and remove the free space if necessary
if (++ offset * 2 >= queue.length){
queue = queue.slice(offset);
offset = 0;
}
// return the dequeued item
return item;
};
this.multiDequeue = function(count) {
// If asking for more items than are in the queue, return everything
count = count > queue.length ? queue.length : count;
// store the item at the front of the queue
var items = queue.slice(offset, offset+count);
// add to the offset and remove the free space if necessary
offset += count;
if (offset * 2 >= queue.length){
queue = queue.slice(offset);
offset = 0;
}
// return the dequeued item
return items;
};
this.peek = function(){
return (queue.length > 0 ? queue[offset] : undefined);
};
this.clear = function() {
queue = [];
offset = 0;
};
}
/**
* Move a file from src to dest, avoiding cross-device rename failures.
* This method will first try fs.rename and call the supplied callback if it succeeds. Otherwise
* it will pump the content of src into dest and unlink src upon completion.
*
* This might take a little more time than a single fs.rename, but it avoids error when
* trying to rename files from one device to the other.
*/
var move = function (src, dest, cb) {
var renameDeferred = q.defer();
fs.rename(src, dest, function (err) {
if (err) {
renameDeferred.reject(err);
}
else {
renameDeferred.resolve();
}
});
renameDeferred.promise.then(function () {
// rename worked
return cb(null);
}, function (err) {
log.warn('io.move: standard rename failed, trying stream pipe... (' + err + ')');
// rename didn't work, try pumping
var is = fs.createReadStream(src),
os = fs.createWriteStream(dest);
is.pipe(os);
is.on('end', function () {
fs.unlinkSync(src);
cb(null);
});
is.on('error', function (err) {
return cb(err);
});
os.on('error', function (err) {
return cb(err);
});
});
};
// // restify serveStatic shim
function serveStatic(opts) {
opts = opts || {};
const p = path.normalize(opts.directory).replace(/\\/g, '/');
const re = new RegExp('^' + escapeRE(p) + '/?.*');
async function serveFileFromStats(file, err, stats, isGzip, req, res, next) {
if (err) {
next(new errors.ResourceNotFoundError(err, req.path()));
return;
} else if (!stats.isFile()) {
next(new errors.ResourceNotFoundError('%s does not exist', req.path()));
return;
}
if (res.handledGzip && isGzip) {
res.handledGzip();
}
const fstream = fs.createReadStream(file + (isGzip ? '.gz' : ''));
const maxAge = opts.maxAge === undefined ? 3600 : opts.maxAge;
fstream.once('open', async function (fd) {
res.cache({ maxAge: maxAge });
res.set('Content-Length', stats.size);
res.set('Content-Type', mime.default.getType(file)); // Using mime.default for ES module
res.set('Last-Modified', stats.mtime);
if (opts.charSet) {
const type = res.getHeader('Content-Type') + '; charset=' + opts.charSet;
res.setHeader('Content-Type', type);
}
if (opts.etag) {
res.set('ETag', opts.etag(stats, opts));
}
res.writeHead(200);
fstream.pipe(res);
fstream.once('end', function () {
next(false);
});
});
}
function serveNormal(file, req, res, next) {
fs.stat(file, function (err, stats) {
if (!err && stats.isDirectory() && opts.default) {
file = path.join(file, opts.default);
fs.stat(file, function (dirErr, dirStats) {
serveFileFromStats(file, dirErr, dirStats, false, req, res, next);
});
} else {
serveFileFromStats(file, err, stats, false, req, res, next);
}
});
}
function serve(req, res, next) {
const uricomp = decodeURIComponent(req.path());
const file = path.join(opts.directory, uricomp);
if (req.method !== 'GET' && req.method !== 'HEAD') {
next(new errors.MethodNotAllowedError(req.method));
return;
}
if (!re.test(file.replace(/\\/g, '/'))) {
next(new errors.NotAuthorizedError(req.path()));
return;
}
if (opts.match && !opts.match.test(file)) {
next(new errors.NotAuthorizedError(req.path()));
return;
}
if (opts.gzip && req.acceptsEncoding('gzip')) {
fs.stat(file + '.gz', function (err, stats) {
if (!err) {
res.setHeader('Content-Encoding', 'gzip');
serveFileFromStats(file, err, stats, true, req, res, next);
} else {
serveNormal(file, req, res, next);
}
});
} else {
serveNormal(file, req, res, next);
}
}
return serve;
}
// Return a directorey tree that is the result of walking the tree provided by filename
// TODO better error handling here
// filename - The root of the walk
function walkDir(filename) {
var stats = fs.lstatSync(filename),
info = {
path: filename,
text: path.basename(filename),
name: path.basename(filename)
};
if (stats.isDirectory()) {
info.type = "dir";
info.children = fs.readdirSync(filename).map(function(child) {
return walkDir(filename + '/' + child);
});
} else {
// Assuming it's a file. In real life it could be a symlink or
// something else!
info.type = "file";
info.children = null;
}
return info;
}
// Clean up the provided JSON data.
// TODO - This function is insane and nobody should ever need it
function fixJSON(json) {
var retval = {};
for(var key in json) {
if (typeof json[key] === 'object') {
var value = fixJSON(json[key]);
} else {
var value = Number(json[key]);
if(typeof value === 'undefined' || isNaN(value)) {
if(json[key] === 'true') {
value = true;
} else if(json[key] === 'false') {
value = false;
} else {
value = json[key];
}
}
}
if(key[0] === '_') {
key = key.slice(1);
}
retval[key] = value;
}
return retval;
}
// Given a restify request object, return the client's address
// req - The request to examine
var getClientAddress = function (req) {
return (req.headers['x-forwarded-for'] || '').split(',')[0]
|| req.connection.remoteAddress;
};
// Quit this process, replacing it with a new process created by exeucting the provided command
// command - The command to run
// args - List of arguments for the command
function eject(command, args) {
var child = require('child_process').spawn(command, args, {
detached: true,
stdio: 'ignore'
});
child.unref();
process.exit(0);
}
module.exports = {
getClientAddress,
serveStatic,
Queue,
move,
walkDir,
createUniqueFilename,
fixJSON,
extend,
doshell,
doshell_promise,
eject,
retry
};