mountfs
Version:
fs-compatible module with the ability to mount other fs-compatible modules at specific locations
230 lines (215 loc) • 10.6 kB
JavaScript
var pathModule = require('path'),
_ = require('underscore');
var MountFs = module.exports = function MountFs(options) {
var that = this;
// Don't require the new operator:
if (!(that instanceof MountFs)) {
return new MountFs(options);
}
if (options && options.readFile) {
options = {fs: options};
} else {
options = _.extend({}, options);
options.fs = options.fs || require('fs');
}
that.fs = options.fs;
that.mountedFsByMountPath = {};
that.mountPaths = [];
that.mount = function (mountPath, mountFs) {
mountPath = mountPath.replace(/\/?$/, '/'); // Ensure trailing slash
if (mountPath in that.mountedFsByMountPath) {
throw new Error('MountFs.mount: Another fs is already mounted at ' + mountPath);
}
that.mountPaths.push(mountPath);
that.mountedFsByMountPath[mountPath] = mountFs;
};
that.unmount = function (mountPath) {
mountPath = mountPath.replace(/\/?$/, '/'); // Ensure trailing slash
if (!(mountPath in that.mountedFsByMountPath)) {
throw new Error('MountFs.unmount: No fs is mounted at ' + mountPath);
}
delete that.mountedFsByMountPath[mountPath];
that.mountPaths.splice(that.mountPaths.indexOf(mountPath), 1);
};
function patchReaddirEntries(path, entries) {
var pathNoTrailing = path.replace(/([^\/])\/$/, '$1');
for (var i = 0 ; i < that.mountPaths.length ; i += 1) {
var mountPath = that.mountPaths[i];
var mountPathNoTrailing = mountPath.replace(/([^\/])\/$/, '$1');
var mountPathFragments = mountPathNoTrailing.split('/');
for (var j = mountPathFragments.length - 1 ; j > 0 ; j -= 1) {
var mountPathPrefix = mountPathFragments.slice(0, j).join('/');
if (mountPathPrefix === pathNoTrailing && entries.indexOf(mountPathFragments[j]) === -1) {
entries.push(mountPathFragments[j]);
}
}
}
}
function findVirtualPathAndMountedFsAndMountPath(path) {
var mountedFs = that.fs;
var foundMountPath = '/';
for (var i = 0 ; i < that.mountPaths.length ; i += 1) {
var mountPath = that.mountPaths[i];
var mountPathNoTrailing = mountPath.replace(/([^\/])\/$/, '$1');
if (path.indexOf(mountPathNoTrailing) === 0) {
// Adjust the path so that the mountPath is not included
// when we request the path from the mounted fs.
path = path.replace(mountPathNoTrailing, '');
// If we resolved something in the root of the mounted
// file system, we should make sure that it is a root
// relative path. Otherwise, it will break for fs's
// mounted on the path '/'.
path = path.replace(/^([^\/])/, '/$1') || '/';
mountedFs = that.mountedFsByMountPath[mountPath];
foundMountPath = mountPath;
break;
}
}
return [path, mountedFs, foundMountPath];
}
var mountPathByFd = {};
var fdMap = {};
var firstReservedFd = 100000000;
var nextFd = firstReservedFd;
Object.keys(that.fs).forEach(function (fsMethodName) {
var fsPropertyValue = that.fs[fsMethodName];
// We want to avoid matching: ReadStream, Stats, _toUnixTimestamp
if (typeof fsPropertyValue === 'function' && /^[a-z]/.test(fsMethodName)) {
that[fsMethodName] = function (firstArg) { // ...
var args = [].slice.call(arguments);
var mountedFs;
var absolutePath;
var mountPath;
if (typeof firstArg === 'number') {
if (typeof fdMap[firstArg] !== 'number') {
throw new Error('MountFs: fd ' + firstArg + ' is unknown');
}
mountPath = mountPathByFd[firstArg];
mountedFs = that.mountedFsByMountPath[mountPath] || that.fs;
args[0] = fdMap[args[0]];
} else if (typeof firstArg === 'string') {
absolutePath = pathModule.resolve(process.cwd(), firstArg);
var mountedFsAndPath = findVirtualPathAndMountedFsAndMountPath(absolutePath);
args[0] = mountedFsAndPath[0];
mountedFs = mountedFsAndPath[1];
mountPath = mountedFsAndPath[2];
} else {
throw new Error('MountFs: First argument must be either a string (path) or a number (an open fd)');
}
if (/^(?:rename|(?:sym)?link)(?:Sync)?$/.test(fsMethodName)) {
var mountedFsAndPathForTarget = findVirtualPathAndMountedFsAndMountPath(pathModule.resolve(process.cwd(), args[1]));
if (mountedFsAndPathForTarget[1] !== mountedFs) {
throw new Error('mountFs: Cannot fs.' + fsMethodName + ' between mounted file systems');
}
args[1] = mountedFsAndPathForTarget[0];
}
if (/(?:Sync|Stream)$/.test(fsMethodName)) {
var result;
try {
result = mountedFs[fsMethodName].apply(this, args);
if (fsMethodName === 'createReadStream' || fsMethodName === 'createWriteStream') {
var originalEmit = result.emit;
result.emit = function (eventName) { // ...
// mock-fs' readable/writable stream implementation does not seem to call our fs.open
// we'll hook into the 'open' event before any of the other listeners and add the mapping:
if (eventName === 'open' && this.fd < firstReservedFd) {
fdMap[this.fd] = this.fd;
mountPathByFd[this.fd] = mountPath;
} else if (eventName === 'close') {
delete mountPathByFd[this.fd];
delete fdMap[this.fd];
}
return originalEmit.apply(this, arguments);
};
}
if (fsMethodName === 'readdirSync') {
patchReaddirEntries(absolutePath, result);
}
if (fsMethodName === 'openSync' && typeof result === 'number') {
var mappedFd = nextFd;
nextFd += 1;
fdMap[mappedFd] = result;
result = mappedFd;
mountPathByFd[mappedFd] = mountPath;
}
return result;
} catch (err) {
if (fsMethodName === 'readdirSync' && err.code === 'ENOENT') {
var entries = [];
patchReaddirEntries(absolutePath, entries);
if (entries.length > 0) {
return entries;
}
}
if (err.name === 'OUTSIDETREE') {
args[0] = pathModule.resolve(mountPath, err.relativeTargetPath);
// TODO: There should be a mechanism for avoiding infinite loops:
result = that[fsMethodName].apply(this, args);
if (fsMethodName === 'readdirSync') {
patchReaddirEntries(absolutePath, result);
}
return result;
} else {
throw err;
}
}
} else {
var lastArgument = args[args.length - 1],
cb = function () {};
if (typeof lastArgument === 'function') {
cb = args.pop();
}
args.push(function (err, result) {
if (fsMethodName === 'readdir') {
if (!err) {
patchReaddirEntries(absolutePath, result);
} else if (err.code === 'ENOENT') {
var entries = [];
patchReaddirEntries(absolutePath, entries);
if (entries.length > 0) {
err = undefined;
result = entries;
}
}
} else if (fsMethodName === 'open' && typeof result === 'number') {
var mappedFd = nextFd;
nextFd += 1;
fdMap[mappedFd] = result;
result = mappedFd;
mountPathByFd[mappedFd] = mountPath;
}
if (err && err.name === 'OUTSIDETREE') {
args[0] = pathModule.resolve(mountPath, err.relativeTargetPath);
args.pop();
args.push(cb);
that[fsMethodName].apply(this, args);
} else {
cb.call(this, err, result);
}
});
return mountedFs[fsMethodName].apply(this, args);
}
};
}
});
};
MountFs.patchInPlace = function (fs) {
fs = fs || require('fs');
var fsShallowCopy = _.extend({}, fs),
mountFs = new MountFs({ fs: fsShallowCopy });
_.extend(fs, mountFs);
fs.unpatch = function () {
if ('unpatch' in fsShallowCopy) {
fs.unpatch = fsShallowCopy.unpatch;
} else {
delete fs.unpatch;
}
Object.keys(mountFs).forEach(function (propertyName) {
if (propertyName in fsShallowCopy) {
fs[propertyName] = fsShallowCopy[propertyName];
} else {
delete fs[propertyName];
}
});
};
};