UNPKG

browserfs

Version:

A filesystem in your browser!

352 lines (328 loc) 10.5 kB
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(); }