browserfs
Version:
A filesystem in your browser!
352 lines (328 loc) • 10.5 kB
text/typescript
import {default as Stats, FileType} from '../core/node_fs_stats';
import * as path from 'path';
/**
* A simple class for storing a filesystem index. Assumes that all paths passed
* to it are *absolute* paths.
*
* Can be used as a partial or a full index, although care must be taken if used
* for the former purpose, especially when directories are concerned.
*/
export class FileIndex<T> {
/**
* Static method for constructing indices from a JSON listing.
* @param listing Directory listing generated by tools/XHRIndexer.coffee
* @return A new FileIndex object.
*/
public static fromListing<T>(listing: any): FileIndex<T> {
const idx = new FileIndex<T>();
// Add a root DirNode.
const rootInode = new DirInode<T>();
idx._index['/'] = rootInode;
const queue = [['', listing, rootInode]];
while (queue.length > 0) {
let inode: Inode;
const next = queue.pop();
const pwd = next![0];
const tree = next![1];
const parent = next![2];
for (const node in tree) {
if (tree.hasOwnProperty(node)) {
const children = tree[node];
const name = `${pwd}/${node}`;
if (children) {
idx._index[name] = inode = new DirInode<T>();
queue.push([name, children, inode]);
} else {
// This inode doesn't have correct size information, noted with -1.
inode = new FileInode<Stats>(new Stats(FileType.FILE, -1, 0x16D));
}
if (parent) {
parent._ls[node] = inode;
}
}
}
}
return idx;
}
// Maps directory paths to directory inodes, which contain files.
private _index: {[path: string]: DirInode<T> };
/**
* Constructs a new FileIndex.
*/
constructor() {
// _index is a single-level key,value store that maps *directory* paths to
// DirInodes. File information is only contained in DirInodes themselves.
this._index = {};
// Create the root directory.
this.addPath('/', new DirInode());
}
/**
* Runs the given function over all files in the index.
*/
public fileIterator<T>(cb: (file: T | null) => void): void {
for (const path in this._index) {
if (this._index.hasOwnProperty(path)) {
const dir = this._index[path];
const files = dir.getListing();
for (const file of files) {
const item = dir.getItem(file);
if (isFileInode<T>(item)) {
cb(item.getData());
}
}
}
}
}
/**
* Adds the given absolute path to the index if it is not already in the index.
* Creates any needed parent directories.
* @param path The path to add to the index.
* @param inode The inode for the
* path to add.
* @return 'True' if it was added or already exists, 'false' if there
* was an issue adding it (e.g. item in path is a file, item exists but is
* different).
* @todo If adding fails and implicitly creates directories, we do not clean up
* the new empty directories.
*/
public addPath(path: string, inode: Inode): boolean {
if (!inode) {
throw new Error('Inode must be specified');
}
if (path[0] !== '/') {
throw new Error('Path must be absolute, got: ' + path);
}
// Check if it already exists.
if (this._index.hasOwnProperty(path)) {
return this._index[path] === inode;
}
const splitPath = this._split_path(path);
const dirpath = splitPath[0];
const itemname = splitPath[1];
// Try to add to its parent directory first.
let parent = this._index[dirpath];
if (parent === undefined && path !== '/') {
// Create parent.
parent = new DirInode<T>();
if (!this.addPath(dirpath, parent)) {
return false;
}
}
// Add myself to my parent.
if (path !== '/') {
if (!parent.addItem(itemname, inode)) {
return false;
}
}
// If I'm a directory, add myself to the index.
if (isDirInode<T>(inode)) {
this._index[path] = inode;
}
return true;
}
/**
* Adds the given absolute path to the index if it is not already in the index.
* The path is added without special treatment (no joining of adjacent separators, etc).
* Creates any needed parent directories.
* @param path The path to add to the index.
* @param inode The inode for the
* path to add.
* @return 'True' if it was added or already exists, 'false' if there
* was an issue adding it (e.g. item in path is a file, item exists but is
* different).
* @todo If adding fails and implicitly creates directories, we do not clean up
* the new empty directories.
*/
public addPathFast(path: string, inode: Inode): boolean {
const itemNameMark = path.lastIndexOf('/');
const parentPath = itemNameMark === 0 ? "/" : path.substring(0, itemNameMark);
const itemName = path.substring(itemNameMark + 1);
// Try to add to its parent directory first.
let parent = this._index[parentPath];
if (parent === undefined) {
// Create parent.
parent = new DirInode<T>();
this.addPathFast(parentPath, parent);
}
if (!parent.addItem(itemName, inode)) {
return false;
}
// If adding a directory, add to the index as well.
if (inode.isDir()) {
this._index[path] = <DirInode<T>> inode;
}
return true;
}
/**
* Removes the given path. Can be a file or a directory.
* @return The removed item,
* or null if it did not exist.
*/
public removePath(path: string): Inode | null {
const splitPath = this._split_path(path);
const dirpath = splitPath[0];
const itemname = splitPath[1];
// Try to remove it from its parent directory first.
const parent = this._index[dirpath];
if (parent === undefined) {
return null;
}
// Remove myself from my parent.
const inode = parent.remItem(itemname);
if (inode === null) {
return null;
}
// If I'm a directory, remove myself from the index, and remove my children.
if (isDirInode(inode)) {
const children = inode.getListing();
for (const child of children) {
this.removePath(path + '/' + child);
}
// Remove the directory from the index, unless it's the root.
if (path !== '/') {
delete this._index[path];
}
}
return inode;
}
/**
* Retrieves the directory listing of the given path.
* @return An array of files in the given path, or 'null' if it does not exist.
*/
public ls(path: string): string[] | null {
const item = this._index[path];
if (item === undefined) {
return null;
}
return item.getListing();
}
/**
* Returns the inode of the given item.
* @return Returns null if the item does not exist.
*/
public getInode(path: string): Inode | null {
const splitPath = this._split_path(path);
const dirpath = splitPath[0];
const itemname = splitPath[1];
// Retrieve from its parent directory.
const parent = this._index[dirpath];
if (parent === undefined) {
return null;
}
// Root case
if (dirpath === path) {
return parent;
}
return parent.getItem(itemname);
}
/**
* Split into a (directory path, item name) pair
*/
private _split_path(p: string): string[] {
const dirpath = path.dirname(p);
const itemname = p.substr(dirpath.length + (dirpath === "/" ? 0 : 1));
return [dirpath, itemname];
}
}
/**
* Generic interface for file/directory inodes.
* Note that Stats objects are what we use for file inodes.
*/
export interface Inode {
// Is this an inode for a file?
isFile(): boolean;
// Is this an inode for a directory?
isDir(): boolean;
}
/**
* Inode for a file. Stores an arbitrary (filesystem-specific) data payload.
*/
export class FileInode<T> implements Inode {
constructor(private data: T) { }
public isFile(): boolean { return true; }
public isDir(): boolean { return false; }
public getData(): T { return this.data; }
public setData(data: T): void { this.data = data; }
}
/**
* Inode for a directory. Currently only contains the directory listing.
*/
export class DirInode<T> implements Inode {
private _ls: {[path: string]: Inode} = {};
/**
* Constructs an inode for a directory.
*/
constructor(private data: T | null = null) {}
public isFile(): boolean {
return false;
}
public isDir(): boolean {
return true;
}
public getData(): T | null { return this.data; }
/**
* Return a Stats object for this inode.
* @todo Should probably remove this at some point. This isn't the
* responsibility of the FileIndex.
*/
public getStats(): Stats {
return new Stats(FileType.DIRECTORY, 4096, 0x16D);
}
/**
* Returns the directory listing for this directory. Paths in the directory are
* relative to the directory's path.
* @return The directory listing for this directory.
*/
public getListing(): string[] {
return Object.keys(this._ls);
}
/**
* Returns the inode for the indicated item, or null if it does not exist.
* @param p Name of item in this directory.
*/
public getItem(p: string): Inode | null {
const item = this._ls[p];
return item ? item : null;
}
/**
* Add the given item to the directory listing. Note that the given inode is
* not copied, and will be mutated by the DirInode if it is a DirInode.
* @param p Item name to add to the directory listing.
* @param inode The inode for the
* item to add to the directory inode.
* @return True if it was added, false if it already existed.
*/
public addItem(p: string, inode: Inode): boolean {
if (p in this._ls) {
return false;
}
this._ls[p] = inode;
return true;
}
/**
* Removes the given item from the directory listing.
* @param p Name of item to remove from the directory listing.
* @return Returns the item
* removed, or null if the item did not exist.
*/
public remItem(p: string): Inode | null {
const item = this._ls[p];
if (item === undefined) {
return null;
}
delete this._ls[p];
return item;
}
}
/**
* @hidden
*/
export function isFileInode<T>(inode: Inode | null): inode is FileInode<T> {
return !!inode && inode.isFile();
}
/**
* @hidden
*/
export function isDirInode<T>(inode: Inode | null): inode is DirInode<T> {
return !!inode && inode.isDir();
}