audiobookshelf/server/libs/archiver/readdir-glob/index.js

248 lines
6.9 KiB
JavaScript

//
// used by archiver
// Source: https://github.com/Yqnn/node-readdir-glob
//
module.exports = readdirGlob;
const fs = require('fs');
const { EventEmitter } = require('events');
const { Minimatch } = require('../archiverUtils/minimatch');
const { resolve } = require('path');
function readdir(dir, strict) {
return new Promise((resolve, reject) => {
fs.readdir(dir, { withFileTypes: true }, (err, files) => {
if (err) {
switch (err.code) {
case 'ENOTDIR': // Not a directory
if (strict) {
reject(err);
} else {
resolve([]);
}
break;
case 'ENOTSUP': // Operation not supported
case 'ENOENT': // No such file or directory
case 'ENAMETOOLONG': // Filename too long
case 'UNKNOWN':
resolve([]);
break;
case 'ELOOP': // Too many levels of symbolic links
default:
reject(err);
break;
}
} else {
resolve(files);
}
});
});
}
function stat(file, followSymlinks) {
return new Promise((resolve, reject) => {
const statFunc = followSymlinks ? fs.stat : fs.lstat;
statFunc(file, (err, stats) => {
if (err) {
switch (err.code) {
case 'ENOENT':
if (followSymlinks) {
// Fallback to lstat to handle broken links as files
resolve(stat(file, false));
} else {
resolve(null);
}
break;
default:
resolve(null);
break;
}
} else {
resolve(stats);
}
});
});
}
async function* exploreWalkAsync(dir, path, followSymlinks, useStat, shouldSkip, strict) {
let files = await readdir(path + dir, strict);
for (const file of files) {
let name = file.name;
if (name === undefined) {
// undefined file.name means the `withFileTypes` options is not supported by node
// we have to call the stat function to know if file is directory or not.
name = file;
useStat = true;
}
const filename = dir + '/' + name;
const relative = filename.slice(1); // Remove the leading /
const absolute = path + '/' + relative;
let stats = null;
if (useStat || followSymlinks) {
stats = await stat(absolute, followSymlinks);
}
if (!stats && file.name !== undefined) {
stats = file;
}
if (stats === null) {
stats = { isDirectory: () => false };
}
if (stats.isDirectory()) {
if (!shouldSkip(relative)) {
yield { relative, absolute, stats };
yield* exploreWalkAsync(filename, path, followSymlinks, useStat, shouldSkip, false);
}
} else {
yield { relative, absolute, stats };
}
}
}
async function* explore(path, followSymlinks, useStat, shouldSkip) {
yield* exploreWalkAsync('', path, followSymlinks, useStat, shouldSkip, true);
}
function readOptions(options) {
return {
pattern: options.pattern,
dot: !!options.dot,
noglobstar: !!options.noglobstar,
matchBase: !!options.matchBase,
nocase: !!options.nocase,
ignore: options.ignore,
skip: options.skip,
follow: !!options.follow,
stat: !!options.stat,
nodir: !!options.nodir,
mark: !!options.mark,
silent: !!options.silent,
absolute: !!options.absolute
};
}
class ReaddirGlob extends EventEmitter {
constructor(cwd, options, cb) {
super();
if (typeof options === 'function') {
cb = options;
options = null;
}
this.options = readOptions(options || {});
this.matchers = [];
if (this.options.pattern) {
const matchers = Array.isArray(this.options.pattern) ? this.options.pattern : [this.options.pattern];
this.matchers = matchers.map(m =>
new Minimatch(m, {
dot: this.options.dot,
noglobstar: this.options.noglobstar,
matchBase: this.options.matchBase,
nocase: this.options.nocase
})
);
}
this.ignoreMatchers = [];
if (this.options.ignore) {
const ignorePatterns = Array.isArray(this.options.ignore) ? this.options.ignore : [this.options.ignore];
this.ignoreMatchers = ignorePatterns.map(ignore =>
new Minimatch(ignore, { dot: true })
);
}
this.skipMatchers = [];
if (this.options.skip) {
const skipPatterns = Array.isArray(this.options.skip) ? this.options.skip : [this.options.skip];
this.skipMatchers = skipPatterns.map(skip =>
new Minimatch(skip, { dot: true })
);
}
this.iterator = explore(resolve(cwd || '.'), this.options.follow, this.options.stat, this._shouldSkipDirectory.bind(this));
this.paused = false;
this.inactive = false;
this.aborted = false;
if (cb) {
this._matches = [];
this.on('match', match => this._matches.push(this.options.absolute ? match.absolute : match.relative));
this.on('error', err => cb(err));
this.on('end', () => cb(null, this._matches));
}
setTimeout(() => this._next(), 0);
}
_shouldSkipDirectory(relative) {
//console.log(relative, this.skipMatchers.some(m => m.match(relative)));
return this.skipMatchers.some(m => m.match(relative));
}
_fileMatches(relative, isDirectory) {
const file = relative + (isDirectory ? '/' : '');
return (this.matchers.length === 0 || this.matchers.some(m => m.match(file)))
&& !this.ignoreMatchers.some(m => m.match(file))
&& (!this.options.nodir || !isDirectory);
}
_next() {
if (!this.paused && !this.aborted) {
this.iterator.next()
.then((obj) => {
if (!obj.done) {
const isDirectory = obj.value.stats.isDirectory();
if (this._fileMatches(obj.value.relative, isDirectory)) {
let relative = obj.value.relative;
let absolute = obj.value.absolute;
if (this.options.mark && isDirectory) {
relative += '/';
absolute += '/';
}
if (this.options.stat) {
this.emit('match', { relative, absolute, stat: obj.value.stats });
} else {
this.emit('match', { relative, absolute });
}
}
this._next(this.iterator);
} else {
this.emit('end');
}
})
.catch((err) => {
this.abort();
this.emit('error', err);
if (!err.code && !this.options.silent) {
console.error(err);
}
});
} else {
this.inactive = true;
}
}
abort() {
this.aborted = true;
}
pause() {
this.paused = true;
}
resume() {
this.paused = false;
if (this.inactive) {
this.inactive = false;
this._next();
}
}
}
function readdirGlob(pattern, options, cb) {
return new ReaddirGlob(pattern, options, cb);
}
readdirGlob.ReaddirGlob = ReaddirGlob;