UNPKG

browserfs

Version:

A filesystem in your browser!

257 lines (235 loc) 8.51 kB
import {BFSOneArgCallback, BFSCallback, FileSystemOptions} from '../core/file_system'; import {AsyncKeyValueROTransaction, AsyncKeyValueRWTransaction, AsyncKeyValueStore, AsyncKeyValueFileSystem} from '../generic/key_value_filesystem'; import {ApiError, ErrorCode} from '../core/api_error'; import global from '../core/global'; import {arrayBuffer2Buffer, buffer2ArrayBuffer, deprecationMessage} from '../core/util'; /** * Get the indexedDB constructor for the current browser. * @hidden */ const indexedDB: IDBFactory = global.indexedDB || (<any> global).mozIndexedDB || (<any> global).webkitIndexedDB || global.msIndexedDB; /** * Converts a DOMException or a DOMError from an IndexedDB event into a * standardized BrowserFS API error. * @hidden */ function convertError(e: {name: string}, message: string = e.toString()): ApiError { switch (e.name) { case "NotFoundError": return new ApiError(ErrorCode.ENOENT, message); case "QuotaExceededError": return new ApiError(ErrorCode.ENOSPC, message); default: // The rest do not seem to map cleanly to standard error codes. return new ApiError(ErrorCode.EIO, message); } } /** * Produces a new onerror handler for IDB. Our errors are always fatal, so we * handle them generically: Call the user-supplied callback with a translated * version of the error, and let the error bubble up. * @hidden */ function onErrorHandler(cb: (e: ApiError) => void, code: ErrorCode = ErrorCode.EIO, message: string | null = null): (e?: any) => void { return function(e?: any): void { // Prevent the error from canceling the transaction. e.preventDefault(); cb(new ApiError(code, message !== null ? message : undefined)); }; } /** * @hidden */ export class IndexedDBROTransaction implements AsyncKeyValueROTransaction { constructor(public tx: IDBTransaction, public store: IDBObjectStore) { } public get(key: string, cb: BFSCallback<Buffer>): void { try { const r: IDBRequest = this.store.get(key); r.onerror = onErrorHandler(cb); r.onsuccess = (event) => { // IDB returns the value 'undefined' when you try to get keys that // don't exist. The caller expects this behavior. const result: any = (<any> event.target).result; if (result === undefined) { cb(null, result); } else { // IDB data is stored as an ArrayBuffer cb(null, arrayBuffer2Buffer(result)); } }; } catch (e) { cb(convertError(e)); } } } /** * @hidden */ export class IndexedDBRWTransaction extends IndexedDBROTransaction implements AsyncKeyValueRWTransaction, AsyncKeyValueROTransaction { constructor(tx: IDBTransaction, store: IDBObjectStore) { super(tx, store); } public put(key: string, data: Buffer, overwrite: boolean, cb: BFSCallback<boolean>): void { try { const arraybuffer = buffer2ArrayBuffer(data); let r: IDBRequest; if (overwrite) { r = this.store.put(arraybuffer, key); } else { // 'add' will never overwrite an existing key. r = this.store.add(arraybuffer, key); } // XXX: NEED TO RETURN FALSE WHEN ADD HAS A KEY CONFLICT. NO ERROR. r.onerror = onErrorHandler(cb); r.onsuccess = (event) => { cb(null, true); }; } catch (e) { cb(convertError(e)); } } public del(key: string, cb: BFSOneArgCallback): void { try { // NOTE: IE8 has a bug with identifiers named 'delete' unless used as a string // like this. // http://stackoverflow.com/a/26479152 const r: IDBRequest = this.store['delete'](key); r.onerror = onErrorHandler(cb); r.onsuccess = (event) => { cb(); }; } catch (e) { cb(convertError(e)); } } public commit(cb: BFSOneArgCallback): void { // Return to the event loop to commit the transaction. setTimeout(cb, 0); } public abort(cb: BFSOneArgCallback): void { let _e: ApiError | null = null; try { this.tx.abort(); } catch (e) { _e = convertError(e); } finally { cb(_e); } } } export class IndexedDBStore implements AsyncKeyValueStore { private db: IDBDatabase; constructor(cb: BFSCallback<IndexedDBStore>, private storeName: string = 'browserfs') { const openReq: IDBOpenDBRequest = indexedDB.open(this.storeName, 1); openReq.onupgradeneeded = (event) => { const db: IDBDatabase = (<any> event.target).result; // Huh. This should never happen; we're at version 1. Why does another // database exist? if (db.objectStoreNames.contains(this.storeName)) { db.deleteObjectStore(this.storeName); } db.createObjectStore(this.storeName); }; openReq.onsuccess = (event) => { this.db = (<any> event.target).result; cb(null, this); }; openReq.onerror = onErrorHandler(cb, ErrorCode.EACCES); } public name(): string { return IndexedDBFileSystem.Name + " - " + this.storeName; } public clear(cb: BFSOneArgCallback): void { try { const tx = this.db.transaction(this.storeName, 'readwrite'), objectStore = tx.objectStore(this.storeName), r: IDBRequest = objectStore.clear(); r.onsuccess = (event) => { // Use setTimeout to commit transaction. setTimeout(cb, 0); }; r.onerror = onErrorHandler(cb); } catch (e) { cb(convertError(e)); } } public beginTransaction(type: 'readonly'): AsyncKeyValueROTransaction; public beginTransaction(type: 'readwrite'): AsyncKeyValueRWTransaction; public beginTransaction(type: string = 'readonly'): AsyncKeyValueROTransaction { const tx = this.db.transaction(this.storeName, type), objectStore = tx.objectStore(this.storeName); if (type === 'readwrite') { return new IndexedDBRWTransaction(tx, objectStore); } else if (type === 'readonly') { return new IndexedDBROTransaction(tx, objectStore); } else { throw new ApiError(ErrorCode.EINVAL, 'Invalid transaction type.'); } } } /** * Configuration options for the IndexedDB file system. */ export interface IndexedDBFileSystemOptions { // The name of this file system. You can have multiple IndexedDB file systems operating // at once, but each must have a different name. storeName?: string; } /** * A file system that uses the IndexedDB key value file system. */ export default class IndexedDBFileSystem extends AsyncKeyValueFileSystem { public static readonly Name = "IndexedDB"; public static readonly Options: FileSystemOptions = { storeName: { type: "string", optional: true, description: "The name of this file system. You can have multiple IndexedDB file systems operating at once, but each must have a different name." } }; /** * Constructs an IndexedDB file system with the given options. */ public static Create(opts: IndexedDBFileSystemOptions, cb: BFSCallback<IndexedDBFileSystem>): void { // tslint:disable-next-line:no-unused-new new IndexedDBFileSystem(cb, opts.storeName, false); // tslint:enable-next-line:no-unused-new } public static isAvailable(): boolean { // In Safari's private browsing mode, indexedDB.open returns NULL. // In Firefox, it throws an exception. // In Chrome, it "just works", and clears the database when you leave the page. // Untested: Opera, IE. try { return typeof indexedDB !== 'undefined' && null !== indexedDB.open("__browserfs_test__"); } catch (e) { return false; } } /** * **Deprecated. Use IndexedDB.Create() method instead.** * * Constructs an IndexedDB file system. * @param cb Called once the database is instantiated and ready for use. * Passes an error if there was an issue instantiating the database. * @param storeName The name of this file system. You can have * multiple IndexedDB file systems operating at once, but each must have * a different name. */ constructor(cb: BFSCallback<IndexedDBFileSystem>, storeName?: string, deprecateMsg: boolean = true) { super(); this.store = new IndexedDBStore((e): void => { if (e) { cb(e); } else { this.init(this.store, (e?) => { cb(e, this); }); } }, storeName); deprecationMessage(deprecateMsg, IndexedDBFileSystem.Name, {storeName: storeName}); } }