browserfs
Version:
A filesystem in your browser!
964 lines (852 loc) • 29.1 kB
text/typescript
import {FileSystem, BaseFileSystem} from '../core/file_system';
import {ApiError, ErrorCode} from '../core/api_error';
import {FileFlag, ActionType} from '../core/file_flag';
import util = require('../core/util');
import {File} from '../core/file';
import {default as Stats, FileType} from '../core/node_fs_stats';
import {PreloadFile} from '../generic/preload_file';
import LockedFS from '../generic/locked_fs';
import path = require('path');
const deletionLogPath = '/.deletedFiles.log';
/**
* Given a read-only mode, makes it writable.
*/
function makeModeWritable(mode: number): number {
return 0o222 | mode;
}
function getFlag(f: string): FileFlag {
return FileFlag.getFileFlag(f);
}
/**
* Overlays a RO file to make it writable.
*/
class OverlayFile extends PreloadFile<UnlockedOverlayFS> implements File {
constructor(fs: UnlockedOverlayFS, path: string, flag: FileFlag, stats: Stats, data: Buffer) {
super(fs, path, flag, stats, data);
}
public sync(cb: (e?: ApiError) => void): void {
if (!this.isDirty()) {
cb(null);
return;
}
this._fs._syncAsync(this, (err: ApiError) => {
this.resetDirty();
cb(err);
});
}
public syncSync(): void {
if (this.isDirty()) {
this._fs._syncSync(this);
this.resetDirty();
}
}
public close(cb: (e?: ApiError) => void): void {
this.sync(cb);
}
public closeSync(): void {
this.syncSync();
}
}
/**
* OverlayFS makes a read-only filesystem writable by storing writes on a second,
* writable file system. Deletes are persisted via metadata stored on the writable
* file system.
*/
export class UnlockedOverlayFS extends BaseFileSystem implements FileSystem {
private _writable: FileSystem;
private _readable: FileSystem;
private _isInitialized: boolean = false;
private _initializeCallbacks: ((e?: ApiError) => void)[] = [];
private _deletedFiles: {[path: string]: boolean} = {};
private _deleteLog: string = '';
// If 'true', we have scheduled a delete log update.
private _deleteLogUpdatePending: boolean = false;
// If 'true', a delete log update is needed after the scheduled delete log
// update finishes.
private _deleteLogUpdateNeeded: boolean = false;
// If there was an error updating the delete log...
private _deleteLogError: ApiError = null;
constructor(writable: FileSystem, readable: FileSystem) {
super();
this._writable = writable;
this._readable = readable;
if (this._writable.isReadOnly()) {
throw new ApiError(ErrorCode.EINVAL, "Writable file system must be writable.");
}
}
private checkInitialized(): void {
if (!this._isInitialized) {
throw new ApiError(ErrorCode.EPERM, "OverlayFS is not initialized. Please initialize OverlayFS using its initialize() method before using it.");
} else if (this._deleteLogError !== null) {
const e = this._deleteLogError;
this._deleteLogError = null;
throw e;
}
}
private checkInitAsync(cb: (e?: ApiError) => void): boolean {
if (!this._isInitialized) {
cb(new ApiError(ErrorCode.EPERM, "OverlayFS is not initialized. Please initialize OverlayFS using its initialize() method before using it."));
return false;
} else if (this._deleteLogError !== null) {
const e = this._deleteLogError;
this._deleteLogError = null;
cb(e);
return false;
}
return true;
}
private checkPath(p: string): void {
if (p === deletionLogPath) {
throw ApiError.EPERM(p);
}
}
private checkPathAsync(p: string, cb: (e?: ApiError) => void): boolean {
if (p === deletionLogPath) {
cb(ApiError.EPERM(p));
return true;
}
return false;
}
public getOverlayedFileSystems(): { readable: FileSystem; writable: FileSystem; } {
return {
readable: this._readable,
writable: this._writable
};
}
private createParentDirectoriesAsync(p: string, cb: (err?: ApiError)=>void): void {
let parent = path.dirname(p)
let toCreate: string[] = [];
let _this = this;
this._writable.stat(parent, false, statDone);
function statDone(err: ApiError, stat?: Stats): void {
if (err) {
toCreate.push(parent);
parent = path.dirname(parent);
_this._writable.stat(parent, false, statDone);
} else {
createParents();
}
}
function createParents(): void {
if (!toCreate.length) {
return cb();
}
let dir = toCreate.pop();
_this._readable.stat(dir, false, (err: ApiError, stats?: Stats) => {
// stop if we couldn't read the dir
if (!stats) {
return cb();
}
_this._writable.mkdir(dir, stats.mode, (err?: ApiError) => {
if (err) {
return cb(err);
}
createParents();
});
});
}
}
/**
* With the given path, create the needed parent directories on the writable storage
* should they not exist. Use modes from the read-only storage.
*/
private createParentDirectories(p: string): void {
var parent = path.dirname(p), toCreate: string[] = [];
while (!this._writable.existsSync(parent)) {
toCreate.push(parent);
parent = path.dirname(parent);
}
toCreate = toCreate.reverse();
toCreate.forEach((p: string) => {
this._writable.mkdirSync(p, this.statSync(p, false).mode);
});
}
public static isAvailable(): boolean {
return true;
}
public _syncAsync(file: PreloadFile<UnlockedOverlayFS>, cb: (err: ApiError)=>void): void {
this.createParentDirectoriesAsync(file.getPath(), (err?: ApiError) => {
if (err) {
return cb(err);
}
this._writable.writeFile(file.getPath(), file.getBuffer(), null, getFlag('w'), file.getStats().mode, cb);
});
}
public _syncSync(file: PreloadFile<UnlockedOverlayFS>): void {
this.createParentDirectories(file.getPath());
this._writable.writeFileSync(file.getPath(), file.getBuffer(), null, getFlag('w'), file.getStats().mode);
}
public getName() {
return "OverlayFS";
}
/**
* Called once to load up metadata stored on the writable file system.
*/
public initialize(cb: (err?: ApiError) => void): void {
const callbackArray = this._initializeCallbacks;
const end = (e?: ApiError): void => {
this._isInitialized = !e;
this._initializeCallbacks = [];
callbackArray.forEach(((cb) => cb(e)));
};
// if we're already initialized, immediately invoke the callback
if (this._isInitialized) {
return cb();
}
callbackArray.push(cb);
// The first call to initialize initializes, the rest wait for it to complete.
if (callbackArray.length !== 1) {
return;
}
// Read deletion log, process into metadata.
this._writable.readFile(deletionLogPath, 'utf8', getFlag('r'), (err: ApiError, data?: string) => {
if (err) {
// ENOENT === Newly-instantiated file system, and thus empty log.
if (err.errno !== ErrorCode.ENOENT) {
return end(err);
}
} else {
this._deleteLog = data;
}
this._reparseDeletionLog();
end(null);
});
}
public isReadOnly(): boolean { return false; }
public supportsSynch(): boolean { return this._readable.supportsSynch() && this._writable.supportsSynch(); }
public supportsLinks(): boolean { return false; }
public supportsProps(): boolean { return this._readable.supportsProps() && this._writable.supportsProps(); }
private deletePath(p: string): void {
this._deletedFiles[p] = true;
this.updateLog(`d${p}\n`);
}
private updateLog(addition: string) {
this._deleteLog += addition;
if (this._deleteLogUpdatePending) {
this._deleteLogUpdateNeeded = true;
} else {
this._deleteLogUpdatePending = true;
this._writable.writeFile(deletionLogPath, this._deleteLog, 'utf8', FileFlag.getFileFlag('w'), 0o644, (e) => {
this._deleteLogUpdatePending = false;
if (e) {
this._deleteLogError = e;
} else if (this._deleteLogUpdateNeeded) {
this._deleteLogUpdateNeeded = false;
this.updateLog('');
}
});
}
}
public getDeletionLog(): string {
return this._deleteLog;
}
private _reparseDeletionLog(): void {
this._deletedFiles = {};
this._deleteLog.split('\n').forEach((path: string) => {
// If the log entry begins w/ 'd', it's a deletion.
this._deletedFiles[path.slice(1)] = path.slice(0, 1) === 'd';
});
}
public restoreDeletionLog(log: string): void {
this._deleteLog = log;
this._reparseDeletionLog();
this.updateLog('');
}
public rename(oldPath: string, newPath: string, cb: (err?: ApiError) => void): void {
if (!this.checkInitAsync(cb) || this.checkPathAsync(oldPath, cb) || this.checkPathAsync(newPath, cb)) return;
if (oldPath === deletionLogPath || newPath === deletionLogPath) {
return cb(ApiError.EPERM('Cannot rename deletion log.'));
}
// nothing to do if paths match
if (oldPath === newPath) {
return cb();
}
this.stat(oldPath, false, (oldErr: ApiError, oldStats?: Stats) => {
if (oldErr) {
return cb(oldErr);
}
return this.stat(newPath, false, (newErr: ApiError, newStats?: Stats) => {
// precondition: both oldPath and newPath exist and are dirs.
// decreases: |files|
// Need to move *every file/folder* currently stored on
// readable to its new location on writable.
function copyDirContents(files: string[]): void {
let file = files.shift();
if (!file) {
return cb();
}
let oldFile = path.resolve(oldPath, file);
let newFile = path.resolve(newPath, file);
// Recursion! Should work for any nested files / folders.
this.rename(oldFile, newFile, (err?: ApiError) => {
if (err) {
return cb(err);
}
copyDirContents(files);
});
}
let mode = 0o777;
// from linux's rename(2) manpage: oldpath can specify a
// directory. In this case, newpath must either not exist, or
// it must specify an empty directory.
if (oldStats.isDirectory()) {
if (newErr) {
if (newErr.errno !== ErrorCode.ENOENT) {
return cb(newErr);
}
return this._writable.exists(oldPath, (exists: boolean) => {
// simple case - both old and new are on the writable layer
if (exists) {
return this._writable.rename(oldPath, newPath, cb);
}
this._writable.mkdir(newPath, mode, (mkdirErr?: ApiError) => {
if (mkdirErr) {
return cb(mkdirErr);
}
this._readable.readdir(oldPath, (err: ApiError, files?: string[]) => {
if (err) {
return cb();
}
copyDirContents(files);
});
});
});
}
mode = newStats.mode;
if (!newStats.isDirectory()) {
return cb(ApiError.ENOTDIR(newPath));
}
this.readdir(newPath, (readdirErr: ApiError, files?: string[]) => {
if (files && files.length) {
return cb(ApiError.ENOTEMPTY(newPath));
}
this._readable.readdir(oldPath, (err: ApiError, files?: string[]) => {
if (err) {
return cb();
}
copyDirContents(files);
});
});
}
if (newStats && newStats.isDirectory()) {
return cb(ApiError.EISDIR(newPath));
}
this.readFile(oldPath, null, getFlag('r'), (err: ApiError, data?: any) => {
if (err) {
return cb(err);
}
return this.writeFile(newPath, data, null, getFlag('w'), oldStats.mode, (err: ApiError) => {
if (err) {
return cb(err);
}
return this.unlink(oldPath, cb);
});
});
});
});
}
public renameSync(oldPath: string, newPath: string): void {
this.checkInitialized();
this.checkPath(oldPath);
this.checkPath(newPath);
if (oldPath === deletionLogPath || newPath === deletionLogPath) {
throw ApiError.EPERM('Cannot rename deletion log.');
}
// Write newPath using oldPath's contents, delete oldPath.
var oldStats = this.statSync(oldPath, false);
if (oldStats.isDirectory()) {
// Optimization: Don't bother moving if old === new.
if (oldPath === newPath) {
return;
}
var mode = 0o777;
if (this.existsSync(newPath)) {
var stats = this.statSync(newPath, false),
mode = stats.mode;
if (stats.isDirectory()) {
if (this.readdirSync(newPath).length > 0) {
throw ApiError.ENOTEMPTY(newPath);
}
} else {
throw ApiError.ENOTDIR(newPath);
}
}
// Take care of writable first. Move any files there, or create an empty directory
// if it doesn't exist.
if (this._writable.existsSync(oldPath)) {
this._writable.renameSync(oldPath, newPath);
} else if (!this._writable.existsSync(newPath)) {
this._writable.mkdirSync(newPath, mode);
}
// Need to move *every file/folder* currently stored on readable to its new location
// on writable.
if (this._readable.existsSync(oldPath)) {
this._readable.readdirSync(oldPath).forEach((name) => {
// Recursion! Should work for any nested files / folders.
this.renameSync(path.resolve(oldPath, name), path.resolve(newPath, name));
});
}
} else {
if (this.existsSync(newPath) && this.statSync(newPath, false).isDirectory()) {
throw ApiError.EISDIR(newPath);
}
this.writeFileSync(newPath,
this.readFileSync(oldPath, null, getFlag('r')), null, getFlag('w'), oldStats.mode);
}
if (oldPath !== newPath && this.existsSync(oldPath)) {
this.unlinkSync(oldPath);
}
}
public stat(p: string, isLstat: boolean, cb: (err: ApiError, stat?: Stats) => void): void {
if (!this.checkInitAsync(cb)) return;
this._writable.stat(p, isLstat, (err: ApiError, stat?: Stats) => {
if (err && err.errno === ErrorCode.ENOENT) {
if (this._deletedFiles[p]) {
cb(ApiError.ENOENT(p));
}
this._readable.stat(p, isLstat, (err: ApiError, stat?: Stats) => {
if (stat) {
// Make the oldStat's mode writable. Preserve the topmost
// part of the mode, which specifies if it is a file or a
// directory.
stat = stat.clone();
stat.mode = makeModeWritable(stat.mode);
}
cb(err, stat);
});
} else {
cb(err, stat);
}
});
}
public statSync(p: string, isLstat: boolean): Stats {
this.checkInitialized();
try {
return this._writable.statSync(p, isLstat);
} catch (e) {
if (this._deletedFiles[p]) {
throw ApiError.ENOENT(p);
}
var oldStat = this._readable.statSync(p, isLstat).clone();
// Make the oldStat's mode writable. Preserve the topmost part of the
// mode, which specifies if it is a file or a directory.
oldStat.mode = makeModeWritable(oldStat.mode);
return oldStat;
}
}
public open(p: string, flag: FileFlag, mode: number, cb: (err: ApiError, fd?: File) => any): void {
if (!this.checkInitAsync(cb) || this.checkPathAsync(p, cb)) return;
this.stat(p, false, (err: ApiError, stats?: Stats) => {
if (stats) {
switch (flag.pathExistsAction()) {
case ActionType.TRUNCATE_FILE:
return this.createParentDirectoriesAsync(p, (err?: ApiError)=> {
if (err) {
return cb(err);
}
this._writable.open(p, flag, mode, cb);
});
case ActionType.NOP:
return this._writable.exists(p, (exists: boolean) => {
if (exists) {
this._writable.open(p, flag, mode, cb);
} else {
// at this point we know the stats object we got is from
// the readable FS.
stats = stats.clone();
stats.mode = mode;
this._readable.readFile(p, null, getFlag('r'), (readFileErr: ApiError, data?: any) => {
if (readFileErr) {
return cb(readFileErr);
}
if (stats.size === -1) {
stats.size = data.length;
}
let f = new OverlayFile(this, p, flag, stats, data);
cb(null, f);
});
}
});
default:
return cb(ApiError.EEXIST(p));
}
} else {
switch(flag.pathNotExistsAction()) {
case ActionType.CREATE_FILE:
return this.createParentDirectoriesAsync(p, (err?: ApiError) => {
if (err) {
return cb(err);
}
return this._writable.open(p, flag, mode, cb);
});
default:
return cb(ApiError.ENOENT(p));
}
}
});
}
public openSync(p: string, flag: FileFlag, mode: number): File {
this.checkInitialized();
this.checkPath(p);
if (p === deletionLogPath) {
throw ApiError.EPERM('Cannot open deletion log.');
}
if (this.existsSync(p)) {
switch (flag.pathExistsAction()) {
case ActionType.TRUNCATE_FILE:
this.createParentDirectories(p);
return this._writable.openSync(p, flag, mode);
case ActionType.NOP:
if (this._writable.existsSync(p)) {
return this._writable.openSync(p, flag, mode);
} else {
// Create an OverlayFile.
var buf = this._readable.readFileSync(p, null, getFlag('r'));
var stats = this._readable.statSync(p, false).clone();
stats.mode = mode;
return new OverlayFile(this, p, flag, stats, buf);
}
default:
throw ApiError.EEXIST(p);
}
} else {
switch(flag.pathNotExistsAction()) {
case ActionType.CREATE_FILE:
this.createParentDirectories(p);
return this._writable.openSync(p, flag, mode);
default:
throw ApiError.ENOENT(p);
}
}
}
public unlink(p: string, cb: (err: ApiError) => void): void {
if (!this.checkInitAsync(cb) || this.checkPathAsync(p, cb)) return;
this.exists(p, (exists: boolean) => {
if (!exists)
return cb(ApiError.ENOENT(p));
this._writable.exists(p, (writableExists: boolean) => {
if (writableExists) {
return this._writable.unlink(p, (err: ApiError) => {
if (err) {
return cb(err);
}
this.exists(p, (readableExists: boolean) => {
if (readableExists) {
this.deletePath(p);
}
cb(null);
});
});
} else {
// if this only exists on the readable FS, add it to the
// delete map.
this.deletePath(p);
cb(null);
}
});
});
}
public unlinkSync(p: string): void {
this.checkInitialized();
this.checkPath(p);
if (this.existsSync(p)) {
if (this._writable.existsSync(p)) {
this._writable.unlinkSync(p);
}
// if it still exists add to the delete log
if (this.existsSync(p)) {
this.deletePath(p);
}
} else {
throw ApiError.ENOENT(p);
}
}
public rmdir(p: string, cb: (err?: ApiError) => void): void {
if (!this.checkInitAsync(cb)) return;
let rmdirLower = (): void => {
this.readdir(p, (err: ApiError, files: string[]): void => {
if (err) {
return cb(err);
}
if (files.length) {
return cb(ApiError.ENOTEMPTY(p));
}
this.deletePath(p);
cb(null);
});
};
this.exists(p, (exists: boolean) => {
if (!exists) {
return cb(ApiError.ENOENT(p));
}
this._writable.exists(p, (writableExists: boolean) => {
if (writableExists) {
this._writable.rmdir(p, (err: ApiError) => {
if (err) {
return cb(err);
}
this._readable.exists(p, (readableExists: boolean) => {
if (readableExists) {
rmdirLower();
} else {
cb();
}
});
});
} else {
rmdirLower();
}
});
});
}
public rmdirSync(p: string): void {
this.checkInitialized();
if (this.existsSync(p)) {
if (this._writable.existsSync(p)) {
this._writable.rmdirSync(p);
}
if (this.existsSync(p)) {
// Check if directory is empty.
if (this.readdirSync(p).length > 0) {
throw ApiError.ENOTEMPTY(p);
} else {
this.deletePath(p);
}
}
} else {
throw ApiError.ENOENT(p);
}
}
public mkdir(p: string, mode: number, cb: (err: ApiError, stat?: Stats) => void): void {
if (!this.checkInitAsync(cb)) return;
this.exists(p, (exists: boolean) => {
if (exists) {
return cb(ApiError.EEXIST(p));
}
// The below will throw should any of the parent directories
// fail to exist on _writable.
this.createParentDirectoriesAsync(p, (err: ApiError) => {
if (err) {
return cb(err);
}
this._writable.mkdir(p, mode, cb);
});
});
}
public mkdirSync(p: string, mode: number): void {
this.checkInitialized();
if (this.existsSync(p)) {
throw ApiError.EEXIST(p);
} else {
// The below will throw should any of the parent directories fail to exist
// on _writable.
this.createParentDirectories(p);
this._writable.mkdirSync(p, mode);
}
}
public readdir(p: string, cb: (error: ApiError, files?: string[]) => void): void {
if (!this.checkInitAsync(cb)) return;
this.stat(p, false, (err: ApiError, dirStats?: Stats) => {
if (err) {
return cb(err);
}
if (!dirStats.isDirectory()) {
return cb(ApiError.ENOTDIR(p));
}
this._writable.readdir(p, (err: ApiError, wFiles: string[]) => {
if (err && err.code !== 'ENOENT') {
return cb(err);
} else if (err || !wFiles) {
wFiles = [];
}
this._readable.readdir(p, (err: ApiError, rFiles: string[]) => {
// if the directory doesn't exist on the lower FS set rFiles
// here to simplify the following code.
if (err || !rFiles) {
rFiles = [];
}
// Readdir in both, check delete log on read-only file system's files, merge, return.
let seenMap: {[name: string]: boolean} = {};
let filtered: string[] = wFiles.concat(rFiles.filter((fPath: string) =>
!this._deletedFiles[`${p}/${fPath}`]
)).filter((fPath: string) => {
// Remove duplicates.
let result = !seenMap[fPath];
seenMap[fPath] = true;
return result;
});
cb(null, filtered);
});
});
});
}
public readdirSync(p: string): string[] {
this.checkInitialized();
var dirStats = this.statSync(p, false);
if (!dirStats.isDirectory()) {
throw ApiError.ENOTDIR(p);
}
// Readdir in both, check delete log on RO file system's listing, merge, return.
var contents: string[] = [];
try {
contents = contents.concat(this._writable.readdirSync(p));
} catch (e) {
}
try {
contents = contents.concat(this._readable.readdirSync(p).filter((fPath: string) =>
!this._deletedFiles[`${p}/${fPath}`]
));
} catch (e) {
}
var seenMap: {[name: string]: boolean} = {};
return contents.filter((fileP: string) => {
var result = !seenMap[fileP];
seenMap[fileP] = true;
return result;
});
}
public exists(p: string, cb: (exists: boolean) => void): void {
// Cannot pass an error back to callback, so throw an exception instead
// if not initialized.
this.checkInitialized();
this._writable.exists(p, (existsWritable: boolean) => {
if (existsWritable) {
return cb(true);
}
this._readable.exists(p, (existsReadable: boolean) => {
cb(existsReadable && this._deletedFiles[p] !== true);
});
});
}
public existsSync(p: string): boolean {
this.checkInitialized();
return this._writable.existsSync(p) || (this._readable.existsSync(p) && this._deletedFiles[p] !== true);
}
public chmod(p: string, isLchmod: boolean, mode: number, cb: (error?: ApiError) => void): void {
if (!this.checkInitAsync(cb)) return;
this.operateOnWritableAsync(p, (err?: ApiError) => {
if (err) {
return cb(err);
} else {
this._writable.chmod(p, isLchmod, mode, cb);
}
});
}
public chmodSync(p: string, isLchmod: boolean, mode: number): void {
this.checkInitialized();
this.operateOnWritable(p, () => {
this._writable.chmodSync(p, isLchmod, mode);
});
}
public chown(p: string, isLchmod: boolean, uid: number, gid: number, cb: (error?: ApiError) => void): void {
if (!this.checkInitAsync(cb)) return;
this.operateOnWritableAsync(p, (err?: ApiError) => {
if (err) {
return cb(err);
} else {
this._writable.chown(p, isLchmod, uid, gid, cb);
}
});
}
public chownSync(p: string, isLchown: boolean, uid: number, gid: number): void {
this.checkInitialized();
this.operateOnWritable(p, () => {
this._writable.chownSync(p, isLchown, uid, gid);
});
}
public utimes(p: string, atime: Date, mtime: Date, cb: (error?: ApiError) => void): void {
if (!this.checkInitAsync(cb)) return;
this.operateOnWritableAsync(p, (err?: ApiError) => {
if (err) {
return cb(err);
} else {
this._writable.utimes(p, atime, mtime, cb);
}
});
}
public utimesSync(p: string, atime: Date, mtime: Date): void {
this.checkInitialized();
this.operateOnWritable(p, () => {
this._writable.utimesSync(p, atime, mtime);
});
}
/**
* Helper function:
* - Ensures p is on writable before proceeding. Throws an error if it doesn't exist.
* - Calls f to perform operation on writable.
*/
private operateOnWritable(p: string, f: () => void): void {
if (this.existsSync(p)) {
if (!this._writable.existsSync(p)) {
// File is on readable storage. Copy to writable storage before
// changing its mode.
this.copyToWritable(p);
}
f();
} else {
throw ApiError.ENOENT(p);
}
}
private operateOnWritableAsync(p: string, cb: (error?: ApiError) => void): void {
this.exists(p, (exists: boolean) => {
if (!exists) {
return cb(ApiError.ENOENT(p));
}
this._writable.exists(p, (existsWritable: boolean) => {
if (existsWritable) {
cb();
} else {
return this.copyToWritableAsync(p, cb);
}
});
});
}
/**
* Copy from readable to writable storage.
* PRECONDITION: File does not exist on writable storage.
*/
private copyToWritable(p: string): void {
var pStats = this.statSync(p, false);
if (pStats.isDirectory()) {
this._writable.mkdirSync(p, pStats.mode);
} else {
this.writeFileSync(p,
this._readable.readFileSync(p, null, getFlag('r')), null,
getFlag('w'), this.statSync(p, false).mode);
}
}
private copyToWritableAsync(p: string, cb: (err?: ApiError) => void): void {
this.stat(p, false, (err: ApiError, pStats?: Stats) => {
if (err) {
return cb(err);
}
if (pStats.isDirectory()) {
return this._writable.mkdir(p, pStats.mode, cb);
}
// need to copy file.
this._readable.readFile(p, null, getFlag('r'), (err: ApiError, data?: Buffer) => {
if (err) {
return cb(err);
}
this.writeFile(p, data, null, getFlag('w'), pStats.mode, cb);
});
});
}
}
export default class OverlayFS extends LockedFS<UnlockedOverlayFS> {
constructor(writable: FileSystem, readable: FileSystem) {
super(new UnlockedOverlayFS(writable, readable));
}
initialize(cb: (err?: ApiError) => void): void {
super.initialize(cb);
}
static isAvailable(): boolean {
return UnlockedOverlayFS.isAvailable();
}
getOverlayedFileSystems(): { readable: FileSystem; writable: FileSystem; } {
return super.getFSUnlocked().getOverlayedFileSystems();
}
unwrap(): UnlockedOverlayFS {
return super.getFSUnlocked();
}
}