UNPKG

akurath

Version:

IDE Frontend for codepsaces.io

855 lines (756 loc) 28.7 kB
/** * Copyright 2013 - Eric Bidelman * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * @fileoverview * Convenient wrapper library for the HTML5 Filesystem API, implementing * familiar UNIX commands (cp, mv, ls) for its API. * * @author Eric Bidelman (ebidel@gmail.com) * @version: 0.4.3 */ 'use strict'; var self = this; // window or worker context. self.URL = self.URL || self.webkitURL; self.requestFileSystem = self.requestFileSystem || self.webkitRequestFileSystem; self.resolveLocalFileSystemURL = self.resolveLocalFileSystemURL || self.webkitResolveLocalFileSystemURL; navigator.temporaryStorage = navigator.temporaryStorage || navigator.webkitTemporaryStorage; navigator.persistentStorage = navigator.persistentStorage || navigator.webkitPersistentStorage; self.BlobBuilder = self.BlobBuilder || self.MozBlobBuilder || self.WebKitBlobBuilder; // Prevent errors in browsers that don't support FileError. if (self.FileError === undefined) { var FileError = function() {}; FileError.prototype.prototype = Error.prototype; } var Util = { /** * Turns a NodeList into an array. * * @param {NodeList} list The array-like object. * @return {Array} The NodeList as an array. */ toArray: function(list) { return Array.prototype.slice.call(list || [], 0); }, /*toDataURL: function(contentType, uint8Array) { return 'data:' + contentType + ';base64,' + self.btoa(this.arrayToBinaryString(uint8Array)); },*/ /** * Creates a data: URL from string data. * * @param {string} str The content to encode the data: URL from. * @param {string} contentType The mimetype of the data str represents. * @param {bool=} opt_isBinary Whether the string data is a binary string * (and therefore should be base64 encoded). True by default. * @return {string} The created data: URL. */ strToDataURL: function(str, contentType, opt_isBinary) { var isBinary = opt_isBinary != undefined ? opt_isBinary : true; if (isBinary) { return 'data:' + contentType + ';base64,' + self.btoa(str); } else { return 'data:' + contentType + ',' + str; } }, /** * Creates a blob: URL from a binary str. * * @param {string} binStr The content as a binary string. * @param {string=} opt_contentType An optional mimetype of the data. * @return {string} A new blob: URL. */ strToObjectURL: function(binStr, opt_contentType) { var ui8a = new Uint8Array(binStr.length); for (var i = 0; i < ui8a.length; ++i) { ui8a[i] = binStr.charCodeAt(i); } var blob = new Blob([ui8a], opt_contentType ? {type: opt_contentType} : {}); return self.URL.createObjectURL(blob); }, /** * Creates a blob: URL from a File or Blob object. * * @param {Blob|File} blob The File or Blob data. * @return {string} A new blob: URL. */ fileToObjectURL: function(blob) { return self.URL.createObjectURL(blob); }, /** * Reads a File or Blob object and returns it as an ArrayBuffer. * * @param {Blob|File} blob The File or Blob data. * @param {Function} callback Success callback passed the array buffer. * @param {Function=} opt_error Optional error callback if the read fails. */ fileToArrayBuffer: function(blob, callback, opt_errorCallback) { var reader = new FileReader(); reader.onload = function(e) { callback(e.target.result); }; reader.onerror = function(e) { if (opt_errorCallback) { opt_errorCallback(e); } }; reader.readAsArrayBuffer(blob); }, /** * Creates and returns a blob from a data URL (either base64 encoded or not). * * @param {string} dataURL The data URL to convert. * @return {Blob} A blob representing the array buffer data. */ dataURLToBlob: function(dataURL) { var BASE64_MARKER = ';base64,'; if (dataURL.indexOf(BASE64_MARKER) == -1) { var parts = dataURL.split(','); var contentType = parts[0].split(':')[1]; var raw = parts[1]; return new Blob([raw], {type: contentType}); } var parts = dataURL.split(BASE64_MARKER); var contentType = parts[0].split(':')[1]; var raw = window.atob(parts[1]); var rawLength = raw.length; var uInt8Array = new Uint8Array(rawLength); for (var i = 0; i < rawLength; ++i) { uInt8Array[i] = raw.charCodeAt(i); } return new Blob([uInt8Array], {type: contentType}); }, /** * Reads an ArrayBuffer as returns its contents as a binary string. * * @param {ArrayBuffer} buffer The buffer of data. * @param {string=} opt_contentType An optional mimetype of the data. * @return {Blob} A blob representing the array buffer data. */ arrayBufferToBlob: function(buffer, opt_contentType) { var uInt8Array = new Uint8Array(buffer); return new Blob([uInt8Array], opt_contentType ? {type: opt_contentType} : {}); }, /** * Reads an ArrayBuffer as returns its contents as a binary string. * * @param {ArrayBuffer} buffer The buffer of data. * @param {Function} callback Success callback passed the binary string. * @param {Function=} opt_error Optional error callback if the read fails. */ arrayBufferToBinaryString: function(buffer, callback, opt_errorCallback) { var reader = new FileReader(); reader.onload = function(e) { callback(e.target.result); }; reader.onerror = function(e) { if (opt_errorCallback) { opt_errorCallback(e); } }; var uInt8Array = new Uint8Array(buffer); reader.readAsBinaryString(new Blob([uInt8Array])); }, /** * Create a binary string out of an array of numbers (bytes), each varying * from 0-255. * * @param {Array} bytes The array of numbers to transform into a binary str. * @return {string} The byte array as a string. */ arrayToBinaryString: function(bytes) { if (typeof bytes != typeof []) { return null; } var i = bytes.length; var bstr = new Array(i); while (i--) { bstr[i] = String.fromCharCode(bytes[i]); } return bstr.join(''); }, /** * Returns the file extension for a given filename. * * @param {string} filename The filename. * @return {string} The file's extension. */ getFileExtension: function(filename) { var idx = filename.lastIndexOf('.'); return idx != -1 ? filename.substring(idx) : ''; } }; var MyFileError = function(obj) { this.prototype = FileError.prototype; this.code = obj.code; this.name = obj.name; }; //MyFileError.prototype.__proto__ = FileError.prototype; // Extend FileError with custom errors and a convenience method to get error // code mnemonic. FileError.BROWSER_NOT_SUPPORTED = 1000; // TODO: remove when FileError.name is implemented (crbug.com/86014). FileError.prototype.__defineGetter__('name', function() { var keys = Object.keys(FileError); for (var i = 0, key; key = keys[i]; ++i) { if (FileError[key] == this.code) { return key; } } return 'Unknown Error'; }); var Filer = new function() { var FS_INIT_ERROR_MSG = 'Filesystem has not been initialized.'; var NOT_IMPLEMENTED_MSG = 'Not implemented.'; var NOT_A_DIRECTORY = 'Path was not a directory.'; var INCORRECT_ARGS = 'These method arguments are not supported.'; var FS_URL_SCHEME = 'filesystem:'; var DEFAULT_FS_SIZE = 1024 * 1024; // 1MB. var fs_ = null; var cwd_ = null; var isOpen_ = false; var isFsURL_ = function(path) { return path.indexOf(FS_URL_SCHEME) == 0; }; // Path can be relative or absolute. If relative, it's taken from the cwd_. // If a filesystem URL is passed it, it is simple returned var pathToFsURL_ = function(path) { if (!isFsURL_(path)) { if (path[0] == '/') { path = fs_.root.toURL() + path.substring(1); } else if (path.indexOf('./') == 0 || path.indexOf('../') == 0) { if (path == '../' && cwd_ != fs_.root) { path = cwd_.toURL() + '/' + path; } else { path = cwd_.toURL() + path; } } else { path = cwd_.toURL() + '/' + path; } } return path; }; /** * Looks up a FileEntry or DirectoryEntry for a given path. * * @param {function(...FileEntry|DirectorEntry)} callback A callback to be * passed the entry/entries that were fetched. The ordering of the * entries passed to the callback correspond to the same order passed * to this method. * @param {...string} var_args 1-2 paths to lookup and return entries for. * These can be paths or filesystem: URLs. */ var getEntry_ = function(callback, opt_errorHandler, var_args) { var srcStr = arguments[2]; var destStr = arguments[3]; var errorHandler = opt_errorHandler || function(e) { throw e; }; var onError = function(e) { if (e.code == FileError.NOT_FOUND_ERR) { if (destStr) { errorHandler(new Error('"' + srcStr + '" or "' + destStr + '" does not exist.')); return; } else { errorHandler(new Error('"' + srcStr + '" does not exist.')); return; } } else { errorHandler(new Error('Problem getting Entry for one or more paths.')); return; } }; // Build a filesystem: URL manually if we need to. var src = pathToFsURL_(srcStr); if (arguments.length == 4) { var dest = pathToFsURL_(destStr); self.resolveLocalFileSystemURL(src, function(srcEntry) { self.resolveLocalFileSystemURL(dest, function(destEntry) { callback(srcEntry, destEntry); }, onError); }, onError); } else { self.resolveLocalFileSystemURL(src, callback, onError); } }; /** * Copy or moves a file or directory to a destination. * * See public method's description (Filer.cp()) for rest of params. * @param {Boolean=} opt_deleteOrig True if the original entry should be * deleted after the copy takes place, essentially making the operation * a move instead of a copy. Defaults to false. */ var copyOrMove_ = function(src, dest, opt_newName, opt_successCallback, opt_errorHandler, opt_deleteOrig) { var self = this; if (!fs_) { throw new Error(FS_INIT_ERROR_MSG); } if (typeof src != typeof dest) { throw new Error(INCORRECT_ARGS); } var newName = opt_newName || null; var deleteOrig = opt_deleteOrig != undefined ? opt_deleteOrig : false; if ((src.isFile || dest.isDirectory) && dest.isDirectory) { if (deleteOrig) { src.moveTo(dest, newName, opt_successCallback, opt_errorHandler); } else { src.copyTo(dest, newName, opt_successCallback, opt_errorHandler); } } else { getEntry_(function(srcEntry, destDir) { if (!destDir.isDirectory) { var e = new Error('Oops! "' + destDir.name + ' is not a directory!'); if (opt_errorHandler) { opt_errorHandler(e); } else { throw e; } return; } if (deleteOrig) { srcEntry.moveTo(destDir, newName, opt_successCallback, opt_errorHandler); } else { srcEntry.copyTo(destDir, newName, opt_successCallback, opt_errorHandler); } }, opt_errorHandler, src, dest); } } function Filer(fs) { fs_ = fs || null; if (fs_) { cwd_ = fs_.root; isOpen_ = true; // TODO: this may not be the case. } } Filer.DEFAULT_FS_SIZE = DEFAULT_FS_SIZE; Filer.version = '0.4.3'; Filer.prototype = { get fs() { return fs_; }, get isOpen() { return isOpen_; }, get cwd() { return cwd_; } } /** * Constructs and returns a filesystem: URL given a path. * * @param {string=} path The path to construct a URL for. * size {int=} The storage size (in bytes) to open the filesystem with. * Defaults to DEFAULT_FS_SIZE. * @return {string} The filesystem: URL. */ Filer.prototype.pathToFilesystemURL = function(path) { return pathToFsURL_(path); } /** * Initializes (opens) the file system. * * @param {object=} opt_initObj Optional object literal with the following * properties. Note: If {} or null is passed, default values are used. * persistent {Boolean=} Whether the browser should use persistent quota. * Default is false. * size {int=} The storage size (in bytes) to open the filesystem with. * Defaults to DEFAULT_FS_SIZE. * @param {Function=} opt_successCallback Optional success handler passed a * DOMFileSystem object. * @param {Function=} opt_errorHandler Optional error callback. */ Filer.prototype.init = function(opt_initObj, opt_successCallback, opt_errorHandler) { if (!self.requestFileSystem) { throw new MyFileError({ code: FileError.BROWSER_NOT_SUPPORTED, name: 'BROWSER_NOT_SUPPORTED' }); } var initObj = opt_initObj ? opt_initObj : {}; // Use defaults if obj is null. var size = initObj.size || DEFAULT_FS_SIZE; this.type = self.TEMPORARY; if ('persistent' in initObj && initObj.persistent) { this.type = self.PERSISTENT; } var init = function(fs) { this.size = size; fs_ = fs; cwd_ = fs_.root; isOpen_ = true; opt_successCallback && opt_successCallback(fs); }; if (this.type == self.PERSISTENT && !!navigator.persistentStorage) { navigator.persistentStorage.requestQuota(size, function(grantedBytes) { self.requestFileSystem( this.type, grantedBytes, init.bind(this), opt_errorHandler); }.bind(this), opt_errorHandler); } else { self.requestFileSystem( this.type, size, init.bind(this), opt_errorHandler); } }; /** * Reads the contents of a directory. * * @param {string|DirectoryEntry} dirEntryOrPath A path relative to the * current working directory. In most cases that is the root entry, unless * cd() has been called. A DirectoryEntry or filesystem URL can also be * passed, in which case, the folder's contents will be returned. * @param {Function} successCallback Success handler passed an Array<Entry>. * @param {Function=} opt_errorHandler Optional error callback. */ Filer.prototype.ls = function(dirEntryOrPath, successCallback, opt_errorHandler) { if (!fs_) { throw new Error(FS_INIT_ERROR_MSG); } var callback = function(dirEntry) { //cwd_ = dirEntry; // Read contents of current working directory. According to spec, need to // keep calling readEntries() until length of result array is 0. We're // guarenteed the same entry won't be returned again. var entries_ = []; var reader = dirEntry.createReader(); var readEntries = function() { reader.readEntries(function(results) { if (!results.length) { // By default, sort the list by name. entries_.sort(function(a, b) { return a.name < b.name ? -1 : b.name < a.name ? 1 : 0; }); successCallback(entries_); } else { entries_ = entries_.concat(Util.toArray(results)); readEntries(); } }, opt_errorHandler); }; readEntries(); }; if (dirEntryOrPath.isDirectory) { // passed a DirectoryEntry. callback(dirEntryOrPath); } else if (isFsURL_(dirEntryOrPath)) { // passed a filesystem URL. getEntry_(callback, opt_errorHandler, pathToFsURL_(dirEntryOrPath)); } else { // Passed a path. Look up DirectoryEntry and proceeed. // TODO: Find way to use getEntry_(callback, dirEntryOrPath); with cwd_. cwd_.getDirectory(dirEntryOrPath, {}, callback, opt_errorHandler); } }; /** * Creates a new directory. * * @param {string} path The name of the directory to create. If a path is * given, each intermediate dir is created (e.g. similar to mkdir -p). * @param {bool=} opt_exclusive True if an error should be thrown if * one or more of the directories already exists. False by default. * @param {Function} opt_successCallback Success handler passed the * DirectoryEntry that was created. If we were passed a path, the last * directory that was created is passed back. * @param {Function=} opt_errorHandler Optional error callback. */ Filer.prototype.mkdir = function(path, opt_exclusive, opt_successCallback, opt_errorHandler) { if (!fs_) { throw new Error(FS_INIT_ERROR_MSG); } var exclusive = opt_exclusive != null ? opt_exclusive : false; var folderParts = path.split('/'); var createDir = function(rootDir, folders) { // Throw out './' or '/' and move on. Prevents: '/foo/.//bar'. if (folders[0] == '.' || folders[0] == '') { folders = folders.slice(1); } rootDir.getDirectory(folders[0], {create: true, exclusive: exclusive}, function (dirEntry) { if (dirEntry.isDirectory) { // TODO: check shouldn't be necessary. // Recursively add the new subfolder if we have more to create and // There was more than one folder to create. if (folders.length && folderParts.length != 1) { createDir(dirEntry, folders.slice(1)); } else { // Return the last directory that was created. if (opt_successCallback) opt_successCallback(dirEntry); } } else { var e = new Error(path + ' is not a directory'); if (opt_errorHandler) { opt_errorHandler(e); } else { throw e; } } }, function(e) { e.message = e.message || "'" + path + "' already exists"; if (opt_errorHandler) { opt_errorHandler(e); } else { throw e; } } ); }; createDir(cwd_, folderParts); }; /** * Looks up and return a File for a given file entry. * * @param {string|FileEntry} entryOrPath A path, filesystem URL, or FileEntry * of the file to lookup. * @param {Function} successCallback Success callback passed the File object. * @param {Function=} opt_errorHandler Optional error callback. */ Filer.prototype.open = function(entryOrPath, successCallback, opt_errorHandler) { if (!fs_) { throw new Error(FS_INIT_ERROR_MSG); } if (entryOrPath.isFile) { entryOrPath.file(successCallback, opt_errorHandler); } else { getEntry_(function(fileEntry) { fileEntry.file(successCallback, opt_errorHandler); }, opt_errorHandler, pathToFsURL_(entryOrPath)); } }; /** * Looks up a FileEntry or DirectoryEntry for a given path. * * @param {function(...FileEntry|DirectorEntry)} callback A callback to be * passed the entry/entries that were fetched. The ordering of the * entries passed to the callback correspond to the same order passed * to this method. */ Filer.prototype.getEntry = function(path, successCallback, opt_errorHandler) { if (!fs_) { throw new Error(FS_INIT_ERROR_MSG); } getEntry_(successCallback, opt_errorHandler, pathToFsURL_(path)); }; /** * Creates an empty file. * * @param {string} path The relative path of the file to create, from the * current working directory. * @param {bool=} opt_exclusive True (default) if an error should be thrown if * the file already exists. * @param {Function} successCallback A success callback, which is passed * the new FileEntry. * @param {Function=} opt_errorHandler Optional error callback. */ Filer.prototype.create = function(path, opt_exclusive, successCallback, opt_errorHandler) { if (!fs_) { throw new Error(FS_INIT_ERROR_MSG); } var exclusive = opt_exclusive != null ? opt_exclusive : true; cwd_.getFile(path, {create: true, exclusive: exclusive}, successCallback, function(e) { if (e.code == FileError.INVALID_MODIFICATION_ERR) { e.message = "'" + path + "' already exists"; } if (opt_errorHandler) { opt_errorHandler(e); } else { throw e; } } ); }; /** * Moves a file or directory. * * @param {string|FileEntry|DirectoryEntry} src The file/directory * to move. If src is a string, a path or filesystem: URL is accepted. * @param {string|DirectoryEntry} dest The directory to move the src into. * If dest is a string, a path or filesystem: URL is accepted. * Note: dest needs to be the same type as src. * @param {string=} opt_newName An optional new name for the moved entry. * @param {Function=} opt_successCallback Optional callback passed the moved * entry on a successful move. * @param {Function=} opt_errorHandler Optional error callback. */ Filer.prototype.mv = function(src, dest, opt_newName, opt_successCallback, opt_errorHandler) { copyOrMove_.bind(this, src, dest, opt_newName, opt_successCallback, opt_errorHandler, true)(); }; /** * Deletes a file or directory entry. * * @param {string|FileEntry|DirectoryEntry} entryOrPath The file or directory * to remove. If entry is a DirectoryEntry, its contents are removed * recursively. If entryOrPath is a string, a path or filesystem: URL is * accepted. * @param {Function} successCallback Zero arg callback invoked on * successful removal. * @param {Function=} opt_errorHandler Optional error callback. */ Filer.prototype.rm = function(entryOrPath, successCallback, opt_errorHandler) { if (!fs_) { throw new Error(FS_INIT_ERROR_MSG); } var removeIt = function(entry) { if (entry.isFile) { entry.remove(successCallback, opt_errorHandler); } else if (entry.isDirectory) { entry.removeRecursively(successCallback, opt_errorHandler); } }; if (entryOrPath.isFile || entryOrPath.isDirectory) { removeIt(entryOrPath); } else { getEntry_(removeIt, opt_errorHandler, entryOrPath); } }; /** * Changes the current working directory. * * @param {string|DirectoryEntry} dirEntryOrPath A DirectoryEntry to move into * or a path relative to the current working directory. A filesystem: URL * is also accepted * @param {Function=} opt_successCallback Optional success callback, which is * passed the DirectoryEntry of the new current directory. * @param {Function=} opt_errorHandler Optional error callback. */ Filer.prototype.cd = function(dirEntryOrPath, opt_successCallback, opt_errorHandler) { if (!fs_) { throw new Error(FS_INIT_ERROR_MSG); } if (dirEntryOrPath.isDirectory) { cwd_ = dirEntryOrPath; opt_successCallback && opt_successCallback(cwd_); } else { // Build a filesystem: URL manually if we need to. var dirEntryOrPath = pathToFsURL_(dirEntryOrPath); getEntry_(function(dirEntry) { if (dirEntry.isDirectory) { cwd_ = dirEntry; opt_successCallback && opt_successCallback(cwd_); } else { var e = new Error(NOT_A_DIRECTORY); if (opt_errorHandler) { opt_errorHandler(e); } else { throw e; } } }, opt_errorHandler, dirEntryOrPath); } }; /** * Copies a file or directory to a destination. * * @param {string|FileEntry|DirectoryEntry} src The file/directory * to copy. If src is a string, a path or filesystem: URL is accepted. * @param {string|DirectoryEntry} dest The directory to copy the src into. * If dest is a string, a path or filesystem: URL is accepted. * Note: dest needs to be the same type as src. * @param {string=} opt_newName An optional name for the copied entry. * @param {Function=} opt_successCallback Optional callback passed the moved * entry on a successful copy. * @param {Function=} opt_errorHandler Optional error callback. */ Filer.prototype.cp = function(src, dest, opt_newName, opt_successCallback, opt_errorHandler) { copyOrMove_.bind(this, src, dest, opt_newName, opt_successCallback, opt_errorHandler)(); }; /** * Writes data to a file. * * If the file already exists, its contents are overwritten. * * @param {string|FileEntry} entryOrPath A path, filesystem URL, or FileEntry * of the file to lookup. * @param {object} dataObj The data to write. Example: * {data: string|Blob|File|ArrayBuffer, type: mimetype, append: true} * If append is specified, data is appended to the end of the file. * @param {Function} opt_successCallback Success callback, which is passed * the created FileEntry and FileWriter object used to write the data. * @param {Function=} opt_errorHandler Optional error callback. */ Filer.prototype.write = function(entryOrPath, dataObj, opt_successCallback, opt_errorHandler) { if (!fs_) { throw new Error(FS_INIT_ERROR_MSG); } var writeFile_ = function(fileEntry) { fileEntry.createWriter(function(fileWriter) { fileWriter.onerror = opt_errorHandler; if (dataObj.append) { fileWriter.onwriteend = function(e) { if (opt_successCallback) opt_successCallback(fileEntry, this); }; fileWriter.seek(fileWriter.length); // Start write position at EOF. } else { var truncated = false; fileWriter.onwriteend = function(e) { // Truncate file to newly written file size. if (!truncated) { truncated = true; this.truncate(this.position); return; } if (opt_successCallback) opt_successCallback(fileEntry, this); }; } // Blob() takes ArrayBufferView, not ArrayBuffer. if (dataObj.data.__proto__ == ArrayBuffer.prototype) { dataObj.data = new Uint8Array(dataObj.data); } var blob = new Blob([dataObj.data], dataObj.type ? {type: dataObj.type} : {}); fileWriter.write(blob); }, opt_errorHandler); }; if (entryOrPath.isFile) { writeFile_(entryOrPath); } else if (isFsURL_(entryOrPath)) { getEntry_(writeFile_, opt_errorHandler, entryOrPath); } else { cwd_.getFile(entryOrPath, {create: true, exclusive: false}, writeFile_, opt_errorHandler); } }; /** * Displays disk space usage. * * @param {Function} successCallback Success callback, which is passed * Used space, Free space and Currently allocated total space in bytes. * @param {Function=} opt_errorHandler Optional error callback. */ Filer.prototype.df = function(successCallback, opt_errorHandler) { var queryCallback = function(byteUsed, byteCap) { successCallback(byteUsed, byteCap - byteUsed, byteCap); } if (!(navigator.temporaryStorage.queryUsageAndQuota && navigator.persistentStorage.queryUsageAndQuota)) { throw new Error(NOT_IMPLEMENTED_MSG); } if (self.TEMPORARY == this.type) { navigator.temporaryStorage.queryUsageAndQuota(queryCallback, opt_errorHandler); } else if (self.PERSISTENT == this.type) { navigator.persistentStorage.queryUsageAndQuota(queryCallback, opt_errorHandler); } }; return Filer; };