mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-11-07 16:44:16 +01:00
Update:Remove proper-lockfile dependency
This commit is contained in:
parent
b7e546f2f5
commit
e06a015d6e
44
package-lock.json
generated
44
package-lock.json
generated
@ -24,7 +24,6 @@
|
||||
"libgen": "^2.1.0",
|
||||
"node-ffprobe": "^3.0.0",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"recursive-readdir-async": "^1.1.8",
|
||||
"socket.io": "^4.4.1",
|
||||
"xml2js": "^0.4.23"
|
||||
@ -1488,16 +1487,6 @@
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"node_modules/proper-lockfile": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
||||
"integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"retry": "^0.12.0",
|
||||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@ -1608,14 +1597,6 @@
|
||||
"lowercase-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@ -1713,11 +1694,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.1.tgz",
|
||||
@ -3084,16 +3060,6 @@
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"proper-lockfile": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
||||
"integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"retry": "^0.12.0",
|
||||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@ -3177,11 +3143,6 @@
|
||||
"lowercase-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@ -3255,11 +3216,6 @@
|
||||
"object-inspect": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
||||
},
|
||||
"socket.io": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.1.tgz",
|
||||
|
@ -43,7 +43,6 @@
|
||||
"libgen": "^2.1.0",
|
||||
"node-ffprobe": "^3.0.0",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"recursive-readdir-async": "^1.1.8",
|
||||
"socket.io": "^4.4.1",
|
||||
"xml2js": "^0.4.23"
|
||||
|
@ -1,6 +1,5 @@
|
||||
const Path = require('path')
|
||||
const njodb = require('./njodb')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const njodb = require('./libs/njodb')
|
||||
const Logger = require('./Logger')
|
||||
const { version } = require('../package.json')
|
||||
const LibraryItem = require('./objects/LibraryItem')
|
||||
|
@ -27,7 +27,7 @@ const {
|
||||
checkSync,
|
||||
lock,
|
||||
lockSync
|
||||
} = require("proper-lockfile");
|
||||
} = require("../properLockfile");
|
||||
|
||||
const {
|
||||
deleteFile,
|
40
server/libs/properLockfile/index.js
Normal file
40
server/libs/properLockfile/index.js
Normal file
@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
const lockfile = require('./lib/lockfile');
|
||||
const { toPromise, toSync, toSyncOptions } = require('./lib/adapter');
|
||||
|
||||
async function lock(file, options) {
|
||||
const release = await toPromise(lockfile.lock)(file, options);
|
||||
|
||||
return toPromise(release);
|
||||
}
|
||||
|
||||
function lockSync(file, options) {
|
||||
const release = toSync(lockfile.lock)(file, toSyncOptions(options));
|
||||
|
||||
return toSync(release);
|
||||
}
|
||||
|
||||
function unlock(file, options) {
|
||||
return toPromise(lockfile.unlock)(file, options);
|
||||
}
|
||||
|
||||
function unlockSync(file, options) {
|
||||
return toSync(lockfile.unlock)(file, toSyncOptions(options));
|
||||
}
|
||||
|
||||
function check(file, options) {
|
||||
return toPromise(lockfile.check)(file, options);
|
||||
}
|
||||
|
||||
function checkSync(file, options) {
|
||||
return toSync(lockfile.check)(file, toSyncOptions(options));
|
||||
}
|
||||
|
||||
module.exports = lock;
|
||||
module.exports.lock = lock;
|
||||
module.exports.unlock = unlock;
|
||||
module.exports.lockSync = lockSync;
|
||||
module.exports.unlockSync = unlockSync;
|
||||
module.exports.check = check;
|
||||
module.exports.checkSync = checkSync;
|
85
server/libs/properLockfile/lib/adapter.js
Normal file
85
server/libs/properLockfile/lib/adapter.js
Normal file
@ -0,0 +1,85 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('graceful-fs');
|
||||
|
||||
function createSyncFs(fs) {
|
||||
const methods = ['mkdir', 'realpath', 'stat', 'rmdir', 'utimes'];
|
||||
const newFs = { ...fs };
|
||||
|
||||
methods.forEach((method) => {
|
||||
newFs[method] = (...args) => {
|
||||
const callback = args.pop();
|
||||
let ret;
|
||||
|
||||
try {
|
||||
ret = fs[`${method}Sync`](...args);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, ret);
|
||||
};
|
||||
});
|
||||
|
||||
return newFs;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
function toPromise(method) {
|
||||
return (...args) => new Promise((resolve, reject) => {
|
||||
args.push((err, result) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
|
||||
method(...args);
|
||||
});
|
||||
}
|
||||
|
||||
function toSync(method) {
|
||||
return (...args) => {
|
||||
let err;
|
||||
let result;
|
||||
|
||||
args.push((_err, _result) => {
|
||||
err = _err;
|
||||
result = _result;
|
||||
});
|
||||
|
||||
method(...args);
|
||||
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
function toSyncOptions(options) {
|
||||
// Shallow clone options because we are oging to mutate them
|
||||
options = { ...options };
|
||||
|
||||
// Transform fs to use the sync methods instead
|
||||
options.fs = createSyncFs(options.fs || fs);
|
||||
|
||||
// Retries are not allowed because it requires the flow to be sync
|
||||
if (
|
||||
(typeof options.retries === 'number' && options.retries > 0) ||
|
||||
(options.retries && typeof options.retries.retries === 'number' && options.retries.retries > 0)
|
||||
) {
|
||||
throw Object.assign(new Error('Cannot use retries with the sync api'), { code: 'ESYNC' });
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
toPromise,
|
||||
toSync,
|
||||
toSyncOptions,
|
||||
};
|
342
server/libs/properLockfile/lib/lockfile.js
Normal file
342
server/libs/properLockfile/lib/lockfile.js
Normal file
@ -0,0 +1,342 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('graceful-fs');
|
||||
const retry = require('../../retry');
|
||||
const onExit = require('../../signalExit');
|
||||
const mtimePrecision = require('./mtime-precision');
|
||||
|
||||
const locks = {};
|
||||
|
||||
function getLockFile(file, options) {
|
||||
return options.lockfilePath || `${file}.lock`;
|
||||
}
|
||||
|
||||
function resolveCanonicalPath(file, options, callback) {
|
||||
if (!options.realpath) {
|
||||
return callback(null, path.resolve(file));
|
||||
}
|
||||
|
||||
// Use realpath to resolve symlinks
|
||||
// It also resolves relative paths
|
||||
options.fs.realpath(file, callback);
|
||||
}
|
||||
|
||||
function acquireLock(file, options, callback) {
|
||||
const lockfilePath = getLockFile(file, options);
|
||||
|
||||
// Use mkdir to create the lockfile (atomic operation)
|
||||
options.fs.mkdir(lockfilePath, (err) => {
|
||||
if (!err) {
|
||||
// At this point, we acquired the lock!
|
||||
// Probe the mtime precision
|
||||
return mtimePrecision.probe(lockfilePath, options.fs, (err, mtime, mtimePrecision) => {
|
||||
// If it failed, try to remove the lock..
|
||||
/* istanbul ignore if */
|
||||
if (err) {
|
||||
options.fs.rmdir(lockfilePath, () => { });
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, mtime, mtimePrecision);
|
||||
});
|
||||
}
|
||||
|
||||
// If error is not EEXIST then some other error occurred while locking
|
||||
if (err.code !== 'EEXIST') {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Otherwise, check if lock is stale by analyzing the file mtime
|
||||
if (options.stale <= 0) {
|
||||
return callback(Object.assign(new Error('Lock file is already being held'), { code: 'ELOCKED', file }));
|
||||
}
|
||||
|
||||
options.fs.stat(lockfilePath, (err, stat) => {
|
||||
if (err) {
|
||||
// Retry if the lockfile has been removed (meanwhile)
|
||||
// Skip stale check to avoid recursiveness
|
||||
if (err.code === 'ENOENT') {
|
||||
return acquireLock(file, { ...options, stale: 0 }, callback);
|
||||
}
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!isLockStale(stat, options)) {
|
||||
return callback(Object.assign(new Error('Lock file is already being held'), { code: 'ELOCKED', file }));
|
||||
}
|
||||
|
||||
// If it's stale, remove it and try again!
|
||||
// Skip stale check to avoid recursiveness
|
||||
removeLock(file, options, (err) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
acquireLock(file, { ...options, stale: 0 }, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isLockStale(stat, options) {
|
||||
return stat.mtime.getTime() < Date.now() - options.stale;
|
||||
}
|
||||
|
||||
function removeLock(file, options, callback) {
|
||||
// Remove lockfile, ignoring ENOENT errors
|
||||
options.fs.rmdir(getLockFile(file, options), (err) => {
|
||||
if (err && err.code !== 'ENOENT') {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function updateLock(file, options) {
|
||||
const lock = locks[file];
|
||||
|
||||
// Just for safety, should never happen
|
||||
/* istanbul ignore if */
|
||||
if (lock.updateTimeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
lock.updateDelay = lock.updateDelay || options.update;
|
||||
lock.updateTimeout = setTimeout(() => {
|
||||
lock.updateTimeout = null;
|
||||
|
||||
// Stat the file to check if mtime is still ours
|
||||
// If it is, we can still recover from a system sleep or a busy event loop
|
||||
options.fs.stat(lock.lockfilePath, (err, stat) => {
|
||||
const isOverThreshold = lock.lastUpdate + options.stale < Date.now();
|
||||
|
||||
// If it failed to update the lockfile, keep trying unless
|
||||
// the lockfile was deleted or we are over the threshold
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT' || isOverThreshold) {
|
||||
return setLockAsCompromised(file, lock, Object.assign(err, { code: 'ECOMPROMISED' }));
|
||||
}
|
||||
|
||||
lock.updateDelay = 1000;
|
||||
|
||||
return updateLock(file, options);
|
||||
}
|
||||
|
||||
const isMtimeOurs = lock.mtime.getTime() === stat.mtime.getTime();
|
||||
|
||||
if (!isMtimeOurs) {
|
||||
return setLockAsCompromised(
|
||||
file,
|
||||
lock,
|
||||
Object.assign(
|
||||
new Error('Unable to update lock within the stale threshold'),
|
||||
{ code: 'ECOMPROMISED' }
|
||||
));
|
||||
}
|
||||
|
||||
const mtime = mtimePrecision.getMtime(lock.mtimePrecision);
|
||||
|
||||
options.fs.utimes(lock.lockfilePath, mtime, mtime, (err) => {
|
||||
const isOverThreshold = lock.lastUpdate + options.stale < Date.now();
|
||||
|
||||
// Ignore if the lock was released
|
||||
if (lock.released) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If it failed to update the lockfile, keep trying unless
|
||||
// the lockfile was deleted or we are over the threshold
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT' || isOverThreshold) {
|
||||
return setLockAsCompromised(file, lock, Object.assign(err, { code: 'ECOMPROMISED' }));
|
||||
}
|
||||
|
||||
lock.updateDelay = 1000;
|
||||
|
||||
return updateLock(file, options);
|
||||
}
|
||||
|
||||
// All ok, keep updating..
|
||||
lock.mtime = mtime;
|
||||
lock.lastUpdate = Date.now();
|
||||
lock.updateDelay = null;
|
||||
updateLock(file, options);
|
||||
});
|
||||
});
|
||||
}, lock.updateDelay);
|
||||
|
||||
// Unref the timer so that the nodejs process can exit freely
|
||||
// This is safe because all acquired locks will be automatically released
|
||||
// on process exit
|
||||
|
||||
// We first check that `lock.updateTimeout.unref` exists because some users
|
||||
// may be using this module outside of NodeJS (e.g., in an electron app),
|
||||
// and in those cases `setTimeout` return an integer.
|
||||
/* istanbul ignore else */
|
||||
if (lock.updateTimeout.unref) {
|
||||
lock.updateTimeout.unref();
|
||||
}
|
||||
}
|
||||
|
||||
function setLockAsCompromised(file, lock, err) {
|
||||
// Signal the lock has been released
|
||||
lock.released = true;
|
||||
|
||||
// Cancel lock mtime update
|
||||
// Just for safety, at this point updateTimeout should be null
|
||||
/* istanbul ignore if */
|
||||
if (lock.updateTimeout) {
|
||||
clearTimeout(lock.updateTimeout);
|
||||
}
|
||||
|
||||
if (locks[file] === lock) {
|
||||
delete locks[file];
|
||||
}
|
||||
|
||||
lock.options.onCompromised(err);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
function lock(file, options, callback) {
|
||||
/* istanbul ignore next */
|
||||
options = {
|
||||
stale: 10000,
|
||||
update: null,
|
||||
realpath: true,
|
||||
retries: 0,
|
||||
fs,
|
||||
onCompromised: (err) => { throw err; },
|
||||
...options,
|
||||
};
|
||||
|
||||
options.retries = options.retries || 0;
|
||||
options.retries = typeof options.retries === 'number' ? { retries: options.retries } : options.retries;
|
||||
options.stale = Math.max(options.stale || 0, 2000);
|
||||
options.update = options.update == null ? options.stale / 2 : options.update || 0;
|
||||
options.update = Math.max(Math.min(options.update, options.stale / 2), 1000);
|
||||
|
||||
// Resolve to a canonical file path
|
||||
resolveCanonicalPath(file, options, (err, file) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Attempt to acquire the lock
|
||||
const operation = retry.operation(options.retries);
|
||||
|
||||
operation.attempt(() => {
|
||||
acquireLock(file, options, (err, mtime, mtimePrecision) => {
|
||||
if (operation.retry(err)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return callback(operation.mainError());
|
||||
}
|
||||
|
||||
// We now own the lock
|
||||
const lock = locks[file] = {
|
||||
lockfilePath: getLockFile(file, options),
|
||||
mtime,
|
||||
mtimePrecision,
|
||||
options,
|
||||
lastUpdate: Date.now(),
|
||||
};
|
||||
|
||||
// We must keep the lock fresh to avoid staleness
|
||||
updateLock(file, options);
|
||||
|
||||
callback(null, (releasedCallback) => {
|
||||
if (lock.released) {
|
||||
return releasedCallback &&
|
||||
releasedCallback(Object.assign(new Error('Lock is already released'), { code: 'ERELEASED' }));
|
||||
}
|
||||
|
||||
// Not necessary to use realpath twice when unlocking
|
||||
unlock(file, { ...options, realpath: false }, releasedCallback);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function unlock(file, options, callback) {
|
||||
options = {
|
||||
fs,
|
||||
realpath: true,
|
||||
...options,
|
||||
};
|
||||
|
||||
// Resolve to a canonical file path
|
||||
resolveCanonicalPath(file, options, (err, file) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Skip if the lock is not acquired
|
||||
const lock = locks[file];
|
||||
|
||||
if (!lock) {
|
||||
return callback(Object.assign(new Error('Lock is not acquired/owned by you'), { code: 'ENOTACQUIRED' }));
|
||||
}
|
||||
|
||||
lock.updateTimeout && clearTimeout(lock.updateTimeout); // Cancel lock mtime update
|
||||
lock.released = true; // Signal the lock has been released
|
||||
delete locks[file]; // Delete from locks
|
||||
|
||||
removeLock(file, options, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function check(file, options, callback) {
|
||||
options = {
|
||||
stale: 10000,
|
||||
realpath: true,
|
||||
fs,
|
||||
...options,
|
||||
};
|
||||
|
||||
options.stale = Math.max(options.stale || 0, 2000);
|
||||
|
||||
// Resolve to a canonical file path
|
||||
resolveCanonicalPath(file, options, (err, file) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Check if lockfile exists
|
||||
options.fs.stat(getLockFile(file, options), (err, stat) => {
|
||||
if (err) {
|
||||
// If does not exist, file is not locked. Otherwise, callback with error
|
||||
return err.code === 'ENOENT' ? callback(null, false) : callback(err);
|
||||
}
|
||||
|
||||
// Otherwise, check if lock is stale by analyzing the file mtime
|
||||
return callback(null, !isLockStale(stat, options));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getLocks() {
|
||||
return locks;
|
||||
}
|
||||
|
||||
// Remove acquired locks on exit
|
||||
/* istanbul ignore next */
|
||||
onExit(() => {
|
||||
for (const file in locks) {
|
||||
const options = locks[file].options;
|
||||
|
||||
try { options.fs.rmdirSync(getLockFile(file, options)); } catch (e) { /* Empty */ }
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.lock = lock;
|
||||
module.exports.unlock = unlock;
|
||||
module.exports.check = check;
|
||||
module.exports.getLocks = getLocks;
|
55
server/libs/properLockfile/lib/mtime-precision.js
Normal file
55
server/libs/properLockfile/lib/mtime-precision.js
Normal file
@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
const cacheSymbol = Symbol();
|
||||
|
||||
function probe(file, fs, callback) {
|
||||
const cachedPrecision = fs[cacheSymbol];
|
||||
|
||||
if (cachedPrecision) {
|
||||
return fs.stat(file, (err, stat) => {
|
||||
/* istanbul ignore if */
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, stat.mtime, cachedPrecision);
|
||||
});
|
||||
}
|
||||
|
||||
// Set mtime by ceiling Date.now() to seconds + 5ms so that it's "not on the second"
|
||||
const mtime = new Date((Math.ceil(Date.now() / 1000) * 1000) + 5);
|
||||
|
||||
fs.utimes(file, mtime, mtime, (err) => {
|
||||
/* istanbul ignore if */
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
fs.stat(file, (err, stat) => {
|
||||
/* istanbul ignore if */
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const precision = stat.mtime.getTime() % 1000 === 0 ? 's' : 'ms';
|
||||
|
||||
// Cache the precision in a non-enumerable way
|
||||
Object.defineProperty(fs, cacheSymbol, { value: precision });
|
||||
|
||||
callback(null, stat.mtime, precision);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getMtime(precision) {
|
||||
let now = Date.now();
|
||||
|
||||
if (precision === 's') {
|
||||
now = Math.ceil(now / 1000) * 1000;
|
||||
}
|
||||
|
||||
return new Date(now);
|
||||
}
|
||||
|
||||
module.exports.probe = probe;
|
||||
module.exports.getMtime = getMtime;
|
100
server/libs/retry/index.js
Normal file
100
server/libs/retry/index.js
Normal file
@ -0,0 +1,100 @@
|
||||
var RetryOperation = require('./retry_operation');
|
||||
|
||||
exports.operation = function(options) {
|
||||
var timeouts = exports.timeouts(options);
|
||||
return new RetryOperation(timeouts, {
|
||||
forever: options && options.forever,
|
||||
unref: options && options.unref,
|
||||
maxRetryTime: options && options.maxRetryTime
|
||||
});
|
||||
};
|
||||
|
||||
exports.timeouts = function(options) {
|
||||
if (options instanceof Array) {
|
||||
return [].concat(options);
|
||||
}
|
||||
|
||||
var opts = {
|
||||
retries: 10,
|
||||
factor: 2,
|
||||
minTimeout: 1 * 1000,
|
||||
maxTimeout: Infinity,
|
||||
randomize: false
|
||||
};
|
||||
for (var key in options) {
|
||||
opts[key] = options[key];
|
||||
}
|
||||
|
||||
if (opts.minTimeout > opts.maxTimeout) {
|
||||
throw new Error('minTimeout is greater than maxTimeout');
|
||||
}
|
||||
|
||||
var timeouts = [];
|
||||
for (var i = 0; i < opts.retries; i++) {
|
||||
timeouts.push(this.createTimeout(i, opts));
|
||||
}
|
||||
|
||||
if (options && options.forever && !timeouts.length) {
|
||||
timeouts.push(this.createTimeout(i, opts));
|
||||
}
|
||||
|
||||
// sort the array numerically ascending
|
||||
timeouts.sort(function(a,b) {
|
||||
return a - b;
|
||||
});
|
||||
|
||||
return timeouts;
|
||||
};
|
||||
|
||||
exports.createTimeout = function(attempt, opts) {
|
||||
var random = (opts.randomize)
|
||||
? (Math.random() + 1)
|
||||
: 1;
|
||||
|
||||
var timeout = Math.round(random * opts.minTimeout * Math.pow(opts.factor, attempt));
|
||||
timeout = Math.min(timeout, opts.maxTimeout);
|
||||
|
||||
return timeout;
|
||||
};
|
||||
|
||||
exports.wrap = function(obj, options, methods) {
|
||||
if (options instanceof Array) {
|
||||
methods = options;
|
||||
options = null;
|
||||
}
|
||||
|
||||
if (!methods) {
|
||||
methods = [];
|
||||
for (var key in obj) {
|
||||
if (typeof obj[key] === 'function') {
|
||||
methods.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < methods.length; i++) {
|
||||
var method = methods[i];
|
||||
var original = obj[method];
|
||||
|
||||
obj[method] = function retryWrapper(original) {
|
||||
var op = exports.operation(options);
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
var callback = args.pop();
|
||||
|
||||
args.push(function(err) {
|
||||
if (op.retry(err)) {
|
||||
return;
|
||||
}
|
||||
if (err) {
|
||||
arguments[0] = op.mainError();
|
||||
}
|
||||
callback.apply(this, arguments);
|
||||
});
|
||||
|
||||
op.attempt(function() {
|
||||
original.apply(obj, args);
|
||||
});
|
||||
}.bind(obj, original);
|
||||
obj[method].options = options;
|
||||
}
|
||||
};
|
158
server/libs/retry/retry_operation.js
Normal file
158
server/libs/retry/retry_operation.js
Normal file
@ -0,0 +1,158 @@
|
||||
function RetryOperation(timeouts, options) {
|
||||
// Compatibility for the old (timeouts, retryForever) signature
|
||||
if (typeof options === 'boolean') {
|
||||
options = { forever: options };
|
||||
}
|
||||
|
||||
this._originalTimeouts = JSON.parse(JSON.stringify(timeouts));
|
||||
this._timeouts = timeouts;
|
||||
this._options = options || {};
|
||||
this._maxRetryTime = options && options.maxRetryTime || Infinity;
|
||||
this._fn = null;
|
||||
this._errors = [];
|
||||
this._attempts = 1;
|
||||
this._operationTimeout = null;
|
||||
this._operationTimeoutCb = null;
|
||||
this._timeout = null;
|
||||
this._operationStart = null;
|
||||
|
||||
if (this._options.forever) {
|
||||
this._cachedTimeouts = this._timeouts.slice(0);
|
||||
}
|
||||
}
|
||||
module.exports = RetryOperation;
|
||||
|
||||
RetryOperation.prototype.reset = function() {
|
||||
this._attempts = 1;
|
||||
this._timeouts = this._originalTimeouts;
|
||||
}
|
||||
|
||||
RetryOperation.prototype.stop = function() {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
}
|
||||
|
||||
this._timeouts = [];
|
||||
this._cachedTimeouts = null;
|
||||
};
|
||||
|
||||
RetryOperation.prototype.retry = function(err) {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
}
|
||||
|
||||
if (!err) {
|
||||
return false;
|
||||
}
|
||||
var currentTime = new Date().getTime();
|
||||
if (err && currentTime - this._operationStart >= this._maxRetryTime) {
|
||||
this._errors.unshift(new Error('RetryOperation timeout occurred'));
|
||||
return false;
|
||||
}
|
||||
|
||||
this._errors.push(err);
|
||||
|
||||
var timeout = this._timeouts.shift();
|
||||
if (timeout === undefined) {
|
||||
if (this._cachedTimeouts) {
|
||||
// retry forever, only keep last error
|
||||
this._errors.splice(this._errors.length - 1, this._errors.length);
|
||||
this._timeouts = this._cachedTimeouts.slice(0);
|
||||
timeout = this._timeouts.shift();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var timer = setTimeout(function() {
|
||||
self._attempts++;
|
||||
|
||||
if (self._operationTimeoutCb) {
|
||||
self._timeout = setTimeout(function() {
|
||||
self._operationTimeoutCb(self._attempts);
|
||||
}, self._operationTimeout);
|
||||
|
||||
if (self._options.unref) {
|
||||
self._timeout.unref();
|
||||
}
|
||||
}
|
||||
|
||||
self._fn(self._attempts);
|
||||
}, timeout);
|
||||
|
||||
if (this._options.unref) {
|
||||
timer.unref();
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
RetryOperation.prototype.attempt = function(fn, timeoutOps) {
|
||||
this._fn = fn;
|
||||
|
||||
if (timeoutOps) {
|
||||
if (timeoutOps.timeout) {
|
||||
this._operationTimeout = timeoutOps.timeout;
|
||||
}
|
||||
if (timeoutOps.cb) {
|
||||
this._operationTimeoutCb = timeoutOps.cb;
|
||||
}
|
||||
}
|
||||
|
||||
var self = this;
|
||||
if (this._operationTimeoutCb) {
|
||||
this._timeout = setTimeout(function() {
|
||||
self._operationTimeoutCb();
|
||||
}, self._operationTimeout);
|
||||
}
|
||||
|
||||
this._operationStart = new Date().getTime();
|
||||
|
||||
this._fn(this._attempts);
|
||||
};
|
||||
|
||||
RetryOperation.prototype.try = function(fn) {
|
||||
console.log('Using RetryOperation.try() is deprecated');
|
||||
this.attempt(fn);
|
||||
};
|
||||
|
||||
RetryOperation.prototype.start = function(fn) {
|
||||
console.log('Using RetryOperation.start() is deprecated');
|
||||
this.attempt(fn);
|
||||
};
|
||||
|
||||
RetryOperation.prototype.start = RetryOperation.prototype.try;
|
||||
|
||||
RetryOperation.prototype.errors = function() {
|
||||
return this._errors;
|
||||
};
|
||||
|
||||
RetryOperation.prototype.attempts = function() {
|
||||
return this._attempts;
|
||||
};
|
||||
|
||||
RetryOperation.prototype.mainError = function() {
|
||||
if (this._errors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var counts = {};
|
||||
var mainError = null;
|
||||
var mainErrorCount = 0;
|
||||
|
||||
for (var i = 0; i < this._errors.length; i++) {
|
||||
var error = this._errors[i];
|
||||
var message = error.message;
|
||||
var count = (counts[message] || 0) + 1;
|
||||
|
||||
counts[message] = count;
|
||||
|
||||
if (count >= mainErrorCount) {
|
||||
mainError = error;
|
||||
mainErrorCount = count;
|
||||
}
|
||||
}
|
||||
|
||||
return mainError;
|
||||
};
|
202
server/libs/signalExit/index.js
Normal file
202
server/libs/signalExit/index.js
Normal file
@ -0,0 +1,202 @@
|
||||
// Note: since nyc uses this module to output coverage, any lines
|
||||
// that are in the direct sync flow of nyc's outputCoverage are
|
||||
// ignored, since we can never get coverage for them.
|
||||
// grab a reference to node's real process object right away
|
||||
var process = global.process
|
||||
|
||||
const processOk = function (process) {
|
||||
return process &&
|
||||
typeof process === 'object' &&
|
||||
typeof process.removeListener === 'function' &&
|
||||
typeof process.emit === 'function' &&
|
||||
typeof process.reallyExit === 'function' &&
|
||||
typeof process.listeners === 'function' &&
|
||||
typeof process.kill === 'function' &&
|
||||
typeof process.pid === 'number' &&
|
||||
typeof process.on === 'function'
|
||||
}
|
||||
|
||||
// some kind of non-node environment, just no-op
|
||||
/* istanbul ignore if */
|
||||
if (!processOk(process)) {
|
||||
module.exports = function () {
|
||||
return function () {}
|
||||
}
|
||||
} else {
|
||||
var assert = require('assert')
|
||||
var signals = require('./signals.js')
|
||||
var isWin = /^win/i.test(process.platform)
|
||||
|
||||
var EE = require('events')
|
||||
/* istanbul ignore if */
|
||||
if (typeof EE !== 'function') {
|
||||
EE = EE.EventEmitter
|
||||
}
|
||||
|
||||
var emitter
|
||||
if (process.__signal_exit_emitter__) {
|
||||
emitter = process.__signal_exit_emitter__
|
||||
} else {
|
||||
emitter = process.__signal_exit_emitter__ = new EE()
|
||||
emitter.count = 0
|
||||
emitter.emitted = {}
|
||||
}
|
||||
|
||||
// Because this emitter is a global, we have to check to see if a
|
||||
// previous version of this library failed to enable infinite listeners.
|
||||
// I know what you're about to say. But literally everything about
|
||||
// signal-exit is a compromise with evil. Get used to it.
|
||||
if (!emitter.infinite) {
|
||||
emitter.setMaxListeners(Infinity)
|
||||
emitter.infinite = true
|
||||
}
|
||||
|
||||
module.exports = function (cb, opts) {
|
||||
/* istanbul ignore if */
|
||||
if (!processOk(global.process)) {
|
||||
return function () {}
|
||||
}
|
||||
assert.equal(typeof cb, 'function', 'a callback must be provided for exit handler')
|
||||
|
||||
if (loaded === false) {
|
||||
load()
|
||||
}
|
||||
|
||||
var ev = 'exit'
|
||||
if (opts && opts.alwaysLast) {
|
||||
ev = 'afterexit'
|
||||
}
|
||||
|
||||
var remove = function () {
|
||||
emitter.removeListener(ev, cb)
|
||||
if (emitter.listeners('exit').length === 0 &&
|
||||
emitter.listeners('afterexit').length === 0) {
|
||||
unload()
|
||||
}
|
||||
}
|
||||
emitter.on(ev, cb)
|
||||
|
||||
return remove
|
||||
}
|
||||
|
||||
var unload = function unload () {
|
||||
if (!loaded || !processOk(global.process)) {
|
||||
return
|
||||
}
|
||||
loaded = false
|
||||
|
||||
signals.forEach(function (sig) {
|
||||
try {
|
||||
process.removeListener(sig, sigListeners[sig])
|
||||
} catch (er) {}
|
||||
})
|
||||
process.emit = originalProcessEmit
|
||||
process.reallyExit = originalProcessReallyExit
|
||||
emitter.count -= 1
|
||||
}
|
||||
module.exports.unload = unload
|
||||
|
||||
var emit = function emit (event, code, signal) {
|
||||
/* istanbul ignore if */
|
||||
if (emitter.emitted[event]) {
|
||||
return
|
||||
}
|
||||
emitter.emitted[event] = true
|
||||
emitter.emit(event, code, signal)
|
||||
}
|
||||
|
||||
// { <signal>: <listener fn>, ... }
|
||||
var sigListeners = {}
|
||||
signals.forEach(function (sig) {
|
||||
sigListeners[sig] = function listener () {
|
||||
/* istanbul ignore if */
|
||||
if (!processOk(global.process)) {
|
||||
return
|
||||
}
|
||||
// If there are no other listeners, an exit is coming!
|
||||
// Simplest way: remove us and then re-send the signal.
|
||||
// We know that this will kill the process, so we can
|
||||
// safely emit now.
|
||||
var listeners = process.listeners(sig)
|
||||
if (listeners.length === emitter.count) {
|
||||
unload()
|
||||
emit('exit', null, sig)
|
||||
/* istanbul ignore next */
|
||||
emit('afterexit', null, sig)
|
||||
/* istanbul ignore next */
|
||||
if (isWin && sig === 'SIGHUP') {
|
||||
// "SIGHUP" throws an `ENOSYS` error on Windows,
|
||||
// so use a supported signal instead
|
||||
sig = 'SIGINT'
|
||||
}
|
||||
/* istanbul ignore next */
|
||||
process.kill(process.pid, sig)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
module.exports.signals = function () {
|
||||
return signals
|
||||
}
|
||||
|
||||
var loaded = false
|
||||
|
||||
var load = function load () {
|
||||
if (loaded || !processOk(global.process)) {
|
||||
return
|
||||
}
|
||||
loaded = true
|
||||
|
||||
// This is the number of onSignalExit's that are in play.
|
||||
// It's important so that we can count the correct number of
|
||||
// listeners on signals, and don't wait for the other one to
|
||||
// handle it instead of us.
|
||||
emitter.count += 1
|
||||
|
||||
signals = signals.filter(function (sig) {
|
||||
try {
|
||||
process.on(sig, sigListeners[sig])
|
||||
return true
|
||||
} catch (er) {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
process.emit = processEmit
|
||||
process.reallyExit = processReallyExit
|
||||
}
|
||||
module.exports.load = load
|
||||
|
||||
var originalProcessReallyExit = process.reallyExit
|
||||
var processReallyExit = function processReallyExit (code) {
|
||||
/* istanbul ignore if */
|
||||
if (!processOk(global.process)) {
|
||||
return
|
||||
}
|
||||
process.exitCode = code || /* istanbul ignore next */ 0
|
||||
emit('exit', process.exitCode, null)
|
||||
/* istanbul ignore next */
|
||||
emit('afterexit', process.exitCode, null)
|
||||
/* istanbul ignore next */
|
||||
originalProcessReallyExit.call(process, process.exitCode)
|
||||
}
|
||||
|
||||
var originalProcessEmit = process.emit
|
||||
var processEmit = function processEmit (ev, arg) {
|
||||
if (ev === 'exit' && processOk(global.process)) {
|
||||
/* istanbul ignore else */
|
||||
if (arg !== undefined) {
|
||||
process.exitCode = arg
|
||||
}
|
||||
var ret = originalProcessEmit.apply(this, arguments)
|
||||
/* istanbul ignore next */
|
||||
emit('exit', process.exitCode, null)
|
||||
/* istanbul ignore next */
|
||||
emit('afterexit', process.exitCode, null)
|
||||
/* istanbul ignore next */
|
||||
return ret
|
||||
} else {
|
||||
return originalProcessEmit.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
}
|
53
server/libs/signalExit/signals.js
Normal file
53
server/libs/signalExit/signals.js
Normal file
@ -0,0 +1,53 @@
|
||||
// This is not the set of all possible signals.
|
||||
//
|
||||
// It IS, however, the set of all signals that trigger
|
||||
// an exit on either Linux or BSD systems. Linux is a
|
||||
// superset of the signal names supported on BSD, and
|
||||
// the unknown signals just fail to register, so we can
|
||||
// catch that easily enough.
|
||||
//
|
||||
// Don't bother with SIGKILL. It's uncatchable, which
|
||||
// means that we can't fire any callbacks anyway.
|
||||
//
|
||||
// If a user does happen to register a handler on a non-
|
||||
// fatal signal like SIGWINCH or something, and then
|
||||
// exit, it'll end up firing `process.emit('exit')`, so
|
||||
// the handler will be fired anyway.
|
||||
//
|
||||
// SIGBUS, SIGFPE, SIGSEGV and SIGILL, when not raised
|
||||
// artificially, inherently leave the process in a
|
||||
// state from which it is not safe to try and enter JS
|
||||
// listeners.
|
||||
module.exports = [
|
||||
'SIGABRT',
|
||||
'SIGALRM',
|
||||
'SIGHUP',
|
||||
'SIGINT',
|
||||
'SIGTERM'
|
||||
]
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
module.exports.push(
|
||||
'SIGVTALRM',
|
||||
'SIGXCPU',
|
||||
'SIGXFSZ',
|
||||
'SIGUSR2',
|
||||
'SIGTRAP',
|
||||
'SIGSYS',
|
||||
'SIGQUIT',
|
||||
'SIGIOT'
|
||||
// should detect profiler and enable/disable accordingly.
|
||||
// see #21
|
||||
// 'SIGPROF'
|
||||
)
|
||||
}
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
module.exports.push(
|
||||
'SIGIO',
|
||||
'SIGPOLL',
|
||||
'SIGPWR',
|
||||
'SIGSTKFLT',
|
||||
'SIGUNUSED'
|
||||
)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
const Path = require('path')
|
||||
const fs = require('fs-extra')
|
||||
const njodb = require('../njodb')
|
||||
const njodb = require('../libs/njodb')
|
||||
|
||||
const { SupportedEbookTypes } = require('./globals')
|
||||
const { PlayMethod } = require('./constants')
|
||||
|
Loading…
Reference in New Issue
Block a user