UNPKG

@tobshub/browser-file-system

Version:

File system for your browser

334 lines (330 loc) 10 kB
import localforage, { INDEXEDDB, LOCALSTORAGE } from 'localforage'; const storage = localforage.createInstance({ name: "browser-file-storage", description: "file storage for your browser" }); class Store { constructor(key, options) { this.key = key; this.options = options; this.storage = storage; this.init(); } storage; init() { if (!this.options || this.options.driver === "indexeddb") { this.storage.config({ driver: INDEXEDDB }); } else { this.storage.config({ driver: LOCALSTORAGE }); } } get() { return this.storage.getItem(this.key); } set(value) { return this.storage.setItem(this.key, value); } } class BrowserFS { constructor(key, storage) { this.key = key; this.pathTo = []; this.name = "root"; this.type = "root"; this.children = []; this.storage = new Store(this.key, storage ? { driver: storage } : void 0); } pathTo; name; type; children; storage; /** Initialize BrowserFS or load existing data from storage */ async init() { const root = await this.storage.get(); if (root) { this.children = root.children; return; } this.save(); } /** Save the current file system in the storage */ async save() { await this.storage.set({ children: this.children, name: this.name, type: this.type }); } /** * Takes a path (relative or absolute) to an item in the file system and returns an array of the absolute path to the input * @returns an array containing moves to a specific directory */ normalisePath(pathTo) { const path = pathTo.split("/"); let absolutePath = [...this.pathTo]; if (path[0] === "") { absolutePath = []; path.shift(); } for (let dir of path) { switch (dir) { case "..": { absolutePath.pop(); } case ".": case "": { break; } default: { absolutePath.push(dir); } } } return absolutePath; } /** @returns the current active directory path */ getCurrentPath() { return this.pathTo.join("/"); } /** * Takes a path to an item in the file system and returns that item if it is found * * Returns null if the item isn't found * @param pathTo - a relative or absolute path to an item in the file system * @returns the item at the end of the path or null if the item is not found */ getItemAtPath(pathTo) { const path = this.normalisePath(pathTo ?? "."); let item = this; for (let move of path) { if (!item || item.type === "file") { throw new Error("item doesn't exist"); } const nextItem = item.children.find((child) => child.name === move) ?? null; item = nextItem; } return { item: item ? item.type === "root" ? this : item.type === "dir" ? new BrowserFSDir(item.name, item.children, this) : new BrowserFSFile(item.name, item.content, this) : null, path }; } /** * Takes a path to an item in the file system and returns that item if it is found * * Returns null if the item isn't found * @param pathTo - a relative or absolute path to an item in the file system * @returns the item at the end of the path or null if the item is not found */ getRawItemAtPath(pathTo) { const path = this.normalisePath(pathTo ?? "."); let parent = null; let item = this; for (let move of path) { if (!item || item.type === "file") { throw new Error("path does not exist"); } const nextItem = item.children.find((child) => child.name === move) ?? null; parent = item; item = nextItem; } return { item, parent, path }; } /** * Takes a path to an directory in the file system and sets the `this.pathTo` to the absolute path of that directory * * Fails if the path points to a file or does not exist * @param path - the relative or absolute path to the directory in the file system * */ setCurrentDir(path) { const { item, path: pathTo } = this.getItemAtPath(path); if (!item) { throw new Error("directory does not exist"); } if (item.type === "file") { throw new Error("can't change active directory to a file"); } this.pathTo = pathTo; } /** * Adds the children to the item at the provided path * * Throws an error if the item at the path does not exist or is a file * * @param path - a relative or absolute path to a directory in the file system * @param children - an array of children to add to the item at the specified path */ async addChildren(path, children) { const { item } = this.getItemAtPath(path); if (!item) { throw new Error("folder does not exist"); } if (item.type === "file") { throw new Error("can't add children file"); } for (let newChild of children) { if (item.children.find((child) => child.name === newChild.name)) { throw new Error("item already exists"); } item.children.push(newChild); } await this.save(); } /** * Removes an item from the file system * * Throws an error if the item's parent does not exist * * @param pathTo - a relative or absolute path to an item in the file system */ async removeItem(pathTo) { const { item, parent, path } = this.getRawItemAtPath(pathTo); if (!item || !parent) { throw new Error("item does not exist"); } if (this.getCurrentPath().startsWith(path.join("/"))) { throw new Error("can't remove that directory"); } parent.children = parent.children.filter((child) => child.name !== item.name); await this.save(); } /** * Renames item at the given path * * Throws an error if the item is a direct or indirect parent of the current path * Throws an error if the item is the root node or the BrowserFS instance * * @param {String} pathTo the path the item to rename * @param {String} newName the new name to give the item * */ async renameItem(pathTo, newName) { if (this.getCurrentPath().startsWith(this.normalisePath(pathTo).join("/"))) { throw new Error("cannot rename that item"); } const { parent, item } = this.getRawItemAtPath(pathTo); if (!item) { throw new Error("item does not exist"); } if (item.type === "root") { throw new Error("can't rename root"); } if (parent && parent.children.find((child) => child.name === newName)) { throw new Error("item already exists"); } item.name = newName; await this.save(); } /** * Move an item at `pathTo` to `newParentPath` * * Copies the item and leaves the original by default * * The name of the item stays the same unless `newName` is provided * @param {String} pathTo the path to the the item * @param {String} newParentPath the parent path of the new location * @param {Object} options (optional) options to change the type of move, e.g. full move/copy * */ async moveItem(pathTo, newParentPath, options) { if (this.getCurrentPath().startsWith(this.normalisePath(pathTo).join("/"))) { throw new Error("cannot move that item"); } const { item, parent: oldParent } = this.getRawItemAtPath(pathTo); if (!item || !oldParent) { throw new Error("item not found"); } if (item.type === "root") { throw new Error("can't move the root directory"); } const { item: newItem, parent: newParent, path: newPath } = this.getRawItemAtPath(newParentPath); if (!newParent && newItem?.type !== "root") { throw new Error("can't move item to specified path"); } if (!newItem && newParent) { const name = newPath.pop(); const parent = new BrowserFSDir(newParent.name, newParent.children, this); await parent.addChildren([{ ...item, name }]); } else if (newItem) { if (newItem.type === "file") { throw new Error("item already exists at that path."); } const newItemParent = new BrowserFSDir(newItem.name, newItem.children, this); await newItemParent.addChildren([{ ...item }]); } else { throw new Error("Error: unknown"); } if (options && options.moveType === "move") { await this.removeItem(pathTo); } await this.save(); } } class BrowserFSDir { constructor(name, children, root) { this.name = name; this.root = root; this.type = "dir"; this.children = children; } type; children; /** * Adds the children to the item at the provided path * * Throws an error if the item at the path does not exist or is a file * * @param path - a relative or absolute path to a directory in the file system * @param children - an array of children to add to the item at the specified path */ async addChildren(children) { for (let newChild of children) { if (this.children.find((child) => child.name === newChild.name)) { throw new Error("item already exists"); } this.children.push(newChild); } await this.save(); } /** * Calls the parent save function * * Stops when the root save function is called * */ async save() { await this.root.save(); } } class BrowserFSFile { constructor(name, content, root) { this.name = name; this.content = content; this.root = root; this.type = "file"; this.children = null; } type; children; /** * Writes (or overwrites) the content on the node * * @param content set the content of the node * @returns the new content */ async write(content) { this.content = content; await this.save(); return content; } /** * Reads the content of the node * * @returns the content of the node */ read() { return this.content; } /** * Calls the parent save function * * Stops when the root save function is called * */ async save() { await this.root.save(); } } export { BrowserFS as default };