diff --git a/libraries/idb.filesystem.js b/libraries/idb.filesystem.js new file mode 100644 index 0000000..520d523 --- /dev/null +++ b/libraries/idb.filesystem.js @@ -0,0 +1,999 @@ +/** + * 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 + * A polyfill implementation of the HTML5 Filesystem API which sits on top of + * IndexedDB as storage layer. Files and folders are stored as FileEntry and + * FolderEntry objects in a single object store. IDBKeyRanges are used to query + * into a folder. A single object store is sufficient because we can utilize the + * properties of ASCII. Namely, ASCII / is followed by ASCII 0. Thus, + * "/one/two/" comes before "/one/two/ANYTHING" comes before "/one/two/0". + * + * @author Eric Bidelman (ebidel@gmail.com) + */ + +'use strict'; + +(function(exports) { + +// Bomb out if the Filesystem API is available natively. +if (exports.requestFileSystem || exports.webkitRequestFileSystem) { + return; +} + +// Bomb out if no indexedDB available +const indexedDB = exports.indexedDB || exports.mozIndexedDB || + exports.msIndexedDB; +if (!indexedDB) { + return; +} + +let IDB_SUPPORTS_BLOB = true; + +// Check to see if IndexedDB support blobs. +const support = new function() { + var dbName = "blob-support"; + indexedDB.deleteDatabase(dbName).onsuccess = function() { + var request = indexedDB.open(dbName, 1); + request.onerror = function() { + IDB_SUPPORTS_BLOB = false; + }; + request.onsuccess = function() { + var db = request.result; + try { + var blob = new Blob(["test"], {type: "text/plain"}); + var transaction = db.transaction("store", "readwrite"); + transaction.objectStore("store").put(blob, "key"); + IDB_SUPPORTS_BLOB = true; + } catch (err) { + IDB_SUPPORTS_BLOB = false; + } finally { + db.close(); + indexedDB.deleteDatabase(dbName); + } + }; + request.onupgradeneeded = function() { + request.result.createObjectStore("store"); + }; + }; +}; + +const Base64ToBlob = function(dataURL) { + var BASE64_MARKER = ';base64,'; + if (dataURL.indexOf(BASE64_MARKER) == -1) { + var parts = dataURL.split(','); + var contentType = parts[0].split(':')[1]; + var raw = decodeURIComponent(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}); +}; + +const BlobToBase64 = function(blob, onload) { + var reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = function() { + onload(reader.result); + }; +}; + +if (!exports.PERSISTENT) { + exports.TEMPORARY = 0; + exports.PERSISTENT = 1; +} + +// Prevent errors in browsers that don't support FileError. +// TODO: FF 13+ supports DOM4 Events (DOMError). Use them instead? +if (exports.FileError === undefined) { + window.FileError = function() {}; + FileError.prototype.prototype = Error.prototype; +} + +if (!FileError.INVALID_MODIFICATION_ERR) { + FileError.INVALID_MODIFICATION_ERR = 9; + FileError.NOT_FOUND_ERR = 1; +} + +function MyFileError(obj) { + var code_ = obj.code; + var name_ = obj.name; + + // Required for FF 11. + Object.defineProperty(this, 'code', { + set: function(code) { + code_ = code; + }, + get: function() { + return code_; + } + }); + + Object.defineProperty(this, 'name', { + set: function(name) { + name_ = name; + }, + get: function() { + return name_; + } + }); +} + +MyFileError.prototype = FileError.prototype; +MyFileError.prototype.toString = Error.prototype.toString; + +const INVALID_MODIFICATION_ERR = new MyFileError({ + code: FileError.INVALID_MODIFICATION_ERR, + name: 'INVALID_MODIFICATION_ERR'}); +const NOT_IMPLEMENTED_ERR = new MyFileError({code: 1000, + name: 'Not implemented'}); +const NOT_FOUND_ERR = new MyFileError({code: FileError.NOT_FOUND_ERR, + name: 'Not found'}); + +let fs_ = null; + +// Browsers other than Chrome don't implement persistent vs. temporary storage. +// but default to temporary anyway. +let storageType_ = 'temporary'; +const idb_ = {db: null}; +const FILE_STORE_ = 'entries'; + +const DIR_SEPARATOR = '/'; +const DIR_OPEN_BOUND = String.fromCharCode(DIR_SEPARATOR.charCodeAt(0) + 1); + +// When saving an entry, the fullPath should always lead with a slash and never +// end with one (e.g. a directory). Also, resolve '.' and '..' to an absolute +// one. This method ensures path is legit! +function resolveToFullPath_(cwdFullPath, path) { + var fullPath = path; + + var relativePath = path[0] != DIR_SEPARATOR; + if (relativePath) { + fullPath = cwdFullPath + DIR_SEPARATOR + path; + } + + // Normalize '.'s, '..'s and '//'s. + var parts = fullPath.split(DIR_SEPARATOR); + var finalParts = []; + for (var i = 0; i < parts.length; ++i) { + var part = parts[i]; + if (part === '..') { + // Go up one level. + if (!finalParts.length) { + throw Error('Invalid path'); + } + finalParts.pop(); + } else if (part === '.') { + // Skip over the current directory. + } else if (part !== '') { + // Eliminate sequences of '/'s as well as possible leading/trailing '/'s. + finalParts.push(part); + } + } + + fullPath = DIR_SEPARATOR + finalParts.join(DIR_SEPARATOR); + + // fullPath is guaranteed to be normalized by construction at this point: + // '.'s, '..'s, '//'s will never appear in it. + + return fullPath; +} + +// // 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 +// function pathToFsURL_(path) { +// path = resolveToFullPath_(cwdFullPath, path); +// path = fs_.root.toURL() + path.substring(1); +// return path; +// }; + +/** + * Interface to wrap the native File interface. + * + * This interface is necessary for creating zero-length (empty) files, + * something the Filesystem API allows you to do. Unfortunately, File's + * constructor cannot be called directly, making it impossible to instantiate + * an empty File in JS. + * + * @param {Object} opts Initial values. + * @constructor + */ +function MyFile(opts) { + var blob_ = null; + + this.size = opts.size || 0; + this.name = opts.name || ''; + this.type = opts.type || ''; + this.lastModifiedDate = opts.lastModifiedDate || null; + //this.slice = Blob.prototype.slice; // Doesn't work with structured clones. + + // Need some black magic to correct the object's size/name/type based on the + // blob that is saved. + Object.defineProperty(this, 'blob_', { + enumerable: true, + get: function() { + return blob_; + }, + set: function (val) { + blob_ = val; + this.size = blob_.size; + this.name = blob_.name; + this.type = blob_.type; + this.lastModifiedDate = blob_.lastModifiedDate; + }.bind(this) + }); +} +MyFile.prototype.constructor = MyFile; +//MyFile.prototype.slice = Blob.prototype.slice; + +/** + * Interface to writing a Blob/File. + * + * Modeled from: + * dev.w3.org/2009/dap/file-system/file-writer.html#the-filewriter-interface + * + * @param {FileEntry} fileEntry The FileEntry associated with this writer. + * @constructor + */ +function FileWriter(fileEntry) { + if (!fileEntry) { + throw Error('Expected fileEntry argument to write.'); + } + + var position_ = 0; + var blob_ = fileEntry.file_ ? fileEntry.file_.blob_ : null; + + Object.defineProperty(this, 'position', { + get: function() { + return position_; + } + }); + + Object.defineProperty(this, 'length', { + get: function() { + return blob_ ? blob_.size : 0; + } + }); + + this.seek = function(offset) { + position_ = offset; + + if (position_ > this.length) { + position_ = this.length; + } + if (position_ < 0) { + position_ += this.length; + } + if (position_ < 0) { + position_ = 0; + } + }; + + this.truncate = function(size) { + if (blob_) { + if (size < this.length) { + blob_ = blob_.slice(0, size); + } else { + blob_ = new Blob([blob_, new Uint8Array(size - this.length)], + {type: blob_.type}); + } + } else { + blob_ = new Blob([]); + } + + position_ = 0; // truncate from beginning of file. + + this.write(blob_); // calls onwritestart and onwriteend. + }; + + this.write = function(data) { + if (!data) { + throw Error('Expected blob argument to write.'); + } + + // Call onwritestart if it was defined. + if (this.onwritestart) { + this.onwritestart(); + } + + // TODO: not handling onprogress, onwrite, onabort. Throw an error if + // they're defined. + + if (blob_) { + // Calc the head and tail fragments + var head = blob_.slice(0, position_); + var tail = blob_.slice(position_ + data.size); + + // Calc the padding + var padding = position_ - head.size; + if (padding < 0) { + padding = 0; + } + + // Do the "write". In fact, a full overwrite of the Blob. + // TODO: figure out if data.type should overwrite the exist blob's type. + blob_ = new Blob([head, new Uint8Array(padding), data, tail], + {type: blob_.type}); + } else { + blob_ = new Blob([data], {type: data.type}); + } + + const writeFile = function(blob) { + // Blob might be a DataURI depending on browser support. + fileEntry.file_.blob_ = blob; + fileEntry.file_.lastModifiedDate = data.lastModifiedDate || new Date(); + idb_.put(fileEntry, function(entry) { + if (!IDB_SUPPORTS_BLOB) { + // Set the blob we're writing on this file entry so we can recall it later. + fileEntry.file_.blob_ = blob_; + fileEntry.file_.lastModifiedDate = data.lastModifiedDate || null; + } + + // Add size of data written to writer.position. + position_ += data.size; + + if (this.onwriteend) { + this.onwriteend(); + } + }.bind(this), this.onerror); + }.bind(this); + + if (IDB_SUPPORTS_BLOB) { + writeFile(blob_); + } else { + BlobToBase64(blob_, writeFile); + } + }; +} + + +/** + * Interface for listing a directory's contents (files and folders). + * + * Modeled from: + * dev.w3.org/2009/dap/file-system/pub/FileSystem/#idl-def-DirectoryReader + * + * @constructor + */ +function DirectoryReader(dirEntry) { + var dirEntry_ = dirEntry; + var used_ = false; + + this.readEntries = function(successCallback, opt_errorCallback) { + if (!successCallback) { + throw Error('Expected successCallback argument.'); + } + + // This is necessary to mimic the way DirectoryReader.readEntries() should + // normally behavior. According to spec, readEntries() needs to be called + // until the length of result array is 0. To handle someone implementing + // a recursive call to readEntries(), get everything from indexedDB on the + // first shot. Then (DirectoryReader has been used), return an empty + // result array. + if (!used_) { + idb_.getAllEntries(dirEntry_.fullPath, function(entries) { + used_= true; + successCallback(entries); + }, opt_errorCallback); + } else { + successCallback([]); + } + }; +}; + +/** + * Interface supplies information about the state of a file or directory. + * + * Modeled from: + * dev.w3.org/2009/dap/file-system/file-dir-sys.html#idl-def-Metadata + * + * @constructor + */ +function Metadata(modificationTime, size) { + this.modificationTime_ = modificationTime || null; + this.size_ = size || 0; +} + +Metadata.prototype = { + get modificationTime() { + return this.modificationTime_; + }, + get size() { + return this.size_; + } +} + +/** + * Interface representing entries in a filesystem, each of which may be a File + * or DirectoryEntry. + * + * Modeled from: + * dev.w3.org/2009/dap/file-system/pub/FileSystem/#idl-def-Entry + * + * @constructor + */ +function Entry() {} + +Entry.prototype = { + name: null, + fullPath: null, + filesystem: null, + copyTo: function() { + throw NOT_IMPLEMENTED_ERR; + }, + getMetadata: function(successCallback, opt_errorCallback) { + if (!successCallback) { + throw Error('Expected successCallback argument.'); + } + + try { + if (this.isFile) { + successCallback( + new Metadata(this.file_.lastModifiedDate, this.file_.size)); + } else { + opt_errorCallback(new MyFileError({code: 1001, + name: 'getMetadata() not implemented for DirectoryEntry'})); + } + } catch(e) { + opt_errorCallback && opt_errorCallback(e); + } + }, + getParent: function() { + throw NOT_IMPLEMENTED_ERR; + }, + moveTo: function() { + throw NOT_IMPLEMENTED_ERR; + }, + remove: function(successCallback, opt_errorCallback) { + if (!successCallback) { + throw Error('Expected successCallback argument.'); + } + // TODO: This doesn't protect against directories that have content in it. + // Should throw an error instead if the dirEntry is not empty. + idb_['delete'](this.fullPath, function() { + successCallback(); + }, opt_errorCallback); + }, + toURL: function() { + var origin = location.protocol + '//' + location.host; + return 'filesystem:' + origin + DIR_SEPARATOR + storageType_.toLowerCase() + + this.fullPath; + }, +}; + +/** + * Interface representing a file in the filesystem. + * + * Modeled from: + * dev.w3.org/2009/dap/file-system/pub/FileSystem/#the-fileentry-interface + * + * @param {FileEntry} opt_fileEntry Optional FileEntry to initialize this + * object from. + * @constructor + * @extends {Entry} + */ +function FileEntry(opt_fileEntry) { + this.file_ = null; + + Object.defineProperty(this, 'isFile', { + enumerable: true, + get: function() { + return true; + } + }); + Object.defineProperty(this, 'isDirectory', { + enumerable: true, + get: function() { + return false; + } + }); + + // Create this entry from properties from an existing FileEntry. + if (opt_fileEntry) { + this.file_ = opt_fileEntry.file_; + this.name = opt_fileEntry.name; + this.fullPath = opt_fileEntry.fullPath; + this.filesystem = opt_fileEntry.filesystem; + if (typeof(this.file_.blob_) === "string") { + this.file_.blob_ = Base64ToBlob(this.file_.blob_); + } + } +} +FileEntry.prototype = new Entry(); +FileEntry.prototype.constructor = FileEntry; +FileEntry.prototype.createWriter = function(callback) { + // TODO: figure out if there's a way to dispatch onwrite event as we're writing + // data to IDB. Right now, we're only calling onwritend/onerror + // FileEntry.write(). + callback(new FileWriter(this)); +}; +FileEntry.prototype.file = function(successCallback, opt_errorCallback) { + if (!successCallback) { + throw Error('Expected successCallback argument.'); + } + + if (this.file_ == null) { + if (opt_errorCallback) { + opt_errorCallback(NOT_FOUND_ERR); + } else { + throw NOT_FOUND_ERR; + } + return; + } + + // If we're returning a zero-length (empty) file, return the fake file obj. + // Otherwise, return the native File object that we've stashed. + var file = this.file_.blob_ == null ? this.file_ : this.file_.blob_; + file.lastModifiedDate = this.file_.lastModifiedDate; + + // Add Blob.slice() to this wrapped object. Currently won't work :( + /*if (!val.slice) { + val.slice = Blob.prototype.slice; // Hack to add back in .slice(). + }*/ + successCallback(file); +}; + +/** + * Interface representing a directory in the filesystem. + * + * Modeled from: + * dev.w3.org/2009/dap/file-system/pub/FileSystem/#the-directoryentry-interface + * + * @param {DirectoryEntry} opt_folderEntry Optional DirectoryEntry to + * initialize this object from. + * @constructor + * @extends {Entry} + */ +function DirectoryEntry(opt_folderEntry) { + Object.defineProperty(this, 'isFile', { + enumerable: true, + get: function() { + return false; + } + }); + Object.defineProperty(this, 'isDirectory', { + enumerable: true, + get: function() { + return true; + } + }); + + // Create this entry from properties from an existing DirectoryEntry. + if (opt_folderEntry) { + this.name = opt_folderEntry.name; + this.fullPath = opt_folderEntry.fullPath; + this.filesystem = opt_folderEntry.filesystem; + } +} +DirectoryEntry.prototype = new Entry(); +DirectoryEntry.prototype.constructor = DirectoryEntry; +DirectoryEntry.prototype.createReader = function() { + return new DirectoryReader(this); +}; +DirectoryEntry.prototype.getDirectory = function(path, options, successCallback, + opt_errorCallback) { + + // Create an absolute path if we were handed a relative one. + path = resolveToFullPath_(this.fullPath, path); + + idb_.get(path, function(folderEntry) { + if (!options) { + options = {}; + } + + if (options.create === true && options.exclusive === true && folderEntry) { + // If create and exclusive are both true, and the path already exists, + // getDirectory must fail. + if (opt_errorCallback) { + opt_errorCallback(INVALID_MODIFICATION_ERR); + return; + } + } else if (options.create === true && !folderEntry) { + // If create is true, the path doesn't exist, and no other error occurs, + // getDirectory must create it as a zero-length file and return a corresponding + // DirectoryEntry. + var dirEntry = new DirectoryEntry(); + dirEntry.name = path.split(DIR_SEPARATOR).pop(); // Just need filename. + dirEntry.fullPath = path; + dirEntry.filesystem = fs_; + + idb_.put(dirEntry, successCallback, opt_errorCallback); + } else if (options.create === true && folderEntry) { + + if (folderEntry.isDirectory) { + // IDB won't save methods, so we need re-create the DirectoryEntry. + successCallback(new DirectoryEntry(folderEntry)); + } else { + if (opt_errorCallback) { + opt_errorCallback(INVALID_MODIFICATION_ERR); + return; + } + } + } else if ((!options.create || options.create === false) && !folderEntry) { + // Handle root special. It should always exist. + if (path == DIR_SEPARATOR) { + folderEntry = new DirectoryEntry(); + folderEntry.name = ''; + folderEntry.fullPath = DIR_SEPARATOR; + folderEntry.filesystem = fs_; + successCallback(folderEntry); + return; + } + + // If create is not true and the path doesn't exist, getDirectory must fail. + if (opt_errorCallback) { + opt_errorCallback(NOT_FOUND_ERR); + return; + } + } else if ((!options.create || options.create === false) && folderEntry && + folderEntry.isFile) { + // If create is not true and the path exists, but is a file, getDirectory + // must fail. + if (opt_errorCallback) { + opt_errorCallback(INVALID_MODIFICATION_ERR); + return; + } + } else { + // Otherwise, if no other error occurs, getDirectory must return a + // DirectoryEntry corresponding to path. + + // IDB won't' save methods, so we need re-create DirectoryEntry. + successCallback(new DirectoryEntry(folderEntry)); + } + }, opt_errorCallback); +}; + +DirectoryEntry.prototype.getFile = function(path, options, successCallback, + opt_errorCallback) { + + // Create an absolute path if we were handed a relative one. + path = resolveToFullPath_(this.fullPath, path); + + idb_.get(path, function(fileEntry) { + if (!options) { + options = {}; + } + + if (options.create === true && options.exclusive === true && fileEntry) { + // If create and exclusive are both true, and the path already exists, + // getFile must fail. + + if (opt_errorCallback) { + opt_errorCallback(INVALID_MODIFICATION_ERR); + return; + } + } else if (options.create === true && !fileEntry) { + // If create is true, the path doesn't exist, and no other error occurs, + // getFile must create it as a zero-length file and return a corresponding + // FileEntry. + var fileEntry = new FileEntry(); + fileEntry.name = path.split(DIR_SEPARATOR).pop(); // Just need filename. + fileEntry.fullPath = path; + fileEntry.filesystem = fs_; + fileEntry.file_ = new MyFile({size: 0, name: fileEntry.name, + lastModifiedDate: new Date()}); + + idb_.put(fileEntry, successCallback, opt_errorCallback); + + } else if (options.create === true && fileEntry) { + if (fileEntry.isFile) { + // IDB won't save methods, so we need re-create the FileEntry. + successCallback(new FileEntry(fileEntry)); + } else { + if (opt_errorCallback) { + opt_errorCallback(INVALID_MODIFICATION_ERR); + return; + } + } + } else if ((!options.create || options.create === false) && !fileEntry) { + // If create is not true and the path doesn't exist, getFile must fail. + if (opt_errorCallback) { + opt_errorCallback(NOT_FOUND_ERR); + return; + } + } else if ((!options.create || options.create === false) && fileEntry && + fileEntry.isDirectory) { + // If create is not true and the path exists, but is a directory, getFile + // must fail. + if (opt_errorCallback) { + opt_errorCallback(INVALID_MODIFICATION_ERR); + return; + } + } else { + // Otherwise, if no other error occurs, getFile must return a FileEntry + // corresponding to path. + + // IDB won't' save methods, so we need re-create the FileEntry. + successCallback(new FileEntry(fileEntry)); + } + }, opt_errorCallback); +}; + +DirectoryEntry.prototype.removeRecursively = function(successCallback, + opt_errorCallback) { + if (!successCallback) { + throw Error('Expected successCallback argument.'); + } + + this.remove(successCallback, opt_errorCallback); +}; + +/** + * Interface representing a filesystem. + * + * Modeled from: + * dev.w3.org/2009/dap/file-system/pub/FileSystem/#idl-def-LocalFileSystem + * + * @param {number} type Kind of storage to use, either TEMPORARY or PERSISTENT. + * @param {number} size Storage space (bytes) the application expects to need. + * @constructor + */ +function DOMFileSystem(type, size) { + storageType_ = type == exports.TEMPORARY ? 'Temporary' : 'Persistent'; + this.name = (location.protocol + location.host).replace(/:/g, '_') + + ':' + storageType_; + this.root = new DirectoryEntry(); + this.root.fullPath = DIR_SEPARATOR; + this.root.filesystem = this; + this.root.name = ''; +} + +function requestFileSystem(type, size, successCallback, opt_errorCallback) { + if (type != exports.TEMPORARY && type != exports.PERSISTENT) { + if (opt_errorCallback) { + opt_errorCallback(INVALID_MODIFICATION_ERR); + return; + } + } + + fs_ = new DOMFileSystem(type, size); + idb_.open(fs_.name, function(e) { + successCallback(fs_); + }, opt_errorCallback); +} + +function resolveLocalFileSystemURL(url, successCallback, opt_errorCallback) { + var origin = location.protocol + '//' + location.host; + var base = 'filesystem:' + origin + DIR_SEPARATOR + storageType_.toLowerCase(); + url = url.replace(base, ''); + if (url.substr(-1) === '/') { + url = url.slice(0, -1); + } + if (url) { + idb_.get(url, function(entry) { + if (entry) { + if (entry.isFile) { + return successCallback(new FileEntry(entry)); + } else if (entry.isDirectory) { + return successCallback(new DirectoryEntry(entry)); + } + } else { + opt_errorCallback && opt_errorCallback(NOT_FOUND_ERR); + } + }, opt_errorCallback); + } else { + successCallback(fs_.root); + } +} + +// Core logic to handle IDB operations ========================================= + +idb_.open = function(dbName, successCallback, opt_errorCallback) { + var self = this; + + // TODO: FF 12.0a1 isn't liking a db name with : in it. + var request = indexedDB.open(dbName.replace(':', '_')/*, 1 /*version*/); + + request.onerror = opt_errorCallback || onError; + + request.onupgradeneeded = function(e) { + // First open was called or higher db version was used. + + // console.log('onupgradeneeded: oldVersion:' + e.oldVersion, + // 'newVersion:' + e.newVersion); + + self.db = e.target.result; + self.db.onerror = onError; + + if (!self.db.objectStoreNames.contains(FILE_STORE_)) { + var store = self.db.createObjectStore(FILE_STORE_/*,{keyPath: 'id', autoIncrement: true}*/); + } + }; + + request.onsuccess = function(e) { + self.db = e.target.result; + self.db.onerror = onError; + successCallback(e); + }; + + request.onblocked = opt_errorCallback || onError; +}; + +idb_.close = function() { + this.db.close(); + this.db = null; +}; + +// TODO: figure out if we should ever call this method. The filesystem API +// doesn't allow you to delete a filesystem once it is 'created'. Users should +// use the public remove/removeRecursively API instead. +idb_.drop = function(successCallback, opt_errorCallback) { + if (!this.db) { + return; + } + + var dbName = this.db.name; + + var request = indexedDB.deleteDatabase(dbName); + request.onsuccess = function(e) { + successCallback(e); + }; + request.onerror = opt_errorCallback || onError; + + idb_.close(); +}; + +idb_.get = function(fullPath, successCallback, opt_errorCallback) { + if (!this.db) { + return; + } + + var tx = this.db.transaction([FILE_STORE_], 'readonly'); + + //var request = tx.objectStore(FILE_STORE_).get(fullPath); + var range = IDBKeyRange.bound(fullPath, fullPath + DIR_OPEN_BOUND, + false, true); + var request = tx.objectStore(FILE_STORE_).get(range); + + tx.onabort = opt_errorCallback || onError; + tx.oncomplete = function(e) { + successCallback(request.result); + }; +}; + +idb_.getAllEntries = function(fullPath, successCallback, opt_errorCallback) { + if (!this.db) { + return; + } + + var results = []; + + //var range = IDBKeyRange.lowerBound(fullPath, true); + //var range = IDBKeyRange.upperBound(fullPath, true); + + // Treat the root entry special. Querying it returns all entries because + // they match '/'. + var range = null; + if (fullPath != DIR_SEPARATOR) { + //console.log(fullPath + '/', fullPath + DIR_OPEN_BOUND) + range = IDBKeyRange.bound( + fullPath + DIR_SEPARATOR, fullPath + DIR_OPEN_BOUND, false, true); + } + + var tx = this.db.transaction([FILE_STORE_], 'readonly'); + tx.onabort = opt_errorCallback || onError; + tx.oncomplete = function(e) { + // TODO: figure out how to do be range queries instead of filtering result + // in memory :( + results = results.filter(function(val) { + var valPartsLen = val.fullPath.split(DIR_SEPARATOR).length; + var fullPathPartsLen = fullPath.split(DIR_SEPARATOR).length; + + if (fullPath == DIR_SEPARATOR && valPartsLen < fullPathPartsLen + 1) { + // Hack to filter out entries in the root folder. This is inefficient + // because reading the entires of fs.root (e.g. '/') returns ALL + // results in the database, then filters out the entries not in '/'. + return val; + } else if (fullPath != DIR_SEPARATOR && + valPartsLen == fullPathPartsLen + 1) { + // If this a subfolder and entry is a direct child, include it in + // the results. Otherwise, it's not an entry of this folder. + return val; + } + }); + + successCallback(results); + }; + + var request = tx.objectStore(FILE_STORE_).openCursor(range); + + request.onsuccess = function(e) { + var cursor = e.target.result; + if (cursor) { + var val = cursor.value; + + results.push(val.isFile ? new FileEntry(val) : new DirectoryEntry(val)); + cursor['continue'](); + } + }; +}; + +idb_['delete'] = function(fullPath, successCallback, opt_errorCallback) { + if (!this.db) { + return; + } + + var tx = this.db.transaction([FILE_STORE_], 'readwrite'); + tx.oncomplete = successCallback; + tx.onabort = opt_errorCallback || onError; + + //var request = tx.objectStore(FILE_STORE_).delete(fullPath); + var range = IDBKeyRange.bound( + fullPath, fullPath + DIR_OPEN_BOUND, false, true); + var request = tx.objectStore(FILE_STORE_)['delete'](range); +}; + +idb_.put = function(entry, successCallback, opt_errorCallback) { + if (!this.db) { + return; + } + + var tx = this.db.transaction([FILE_STORE_], 'readwrite'); + tx.onabort = opt_errorCallback || onError; + tx.oncomplete = function(e) { + // TODO: Error is thrown if we pass the request event back instead. + successCallback(entry); + }; + + var request = tx.objectStore(FILE_STORE_).put(entry, entry.fullPath); +}; + +// Global error handler. Errors bubble from request, to transaction, to db. +function onError(e) { + switch (e.target.errorCode) { + case 12: + console.log('Error - Attempt to open db with a lower version than the ' + + 'current one.'); + break; + default: + console.log('errorCode: ' + e.target.errorCode); + } + + console.log(e, e.code, e.message); +} + +// Clean up. +// TODO: decide if this is the best place for this. +exports.addEventListener('beforeunload', function(e) { + idb_.db && idb_.db.close(); +}, false); + +//exports.idb = idb_; +exports.requestFileSystem = requestFileSystem; +exports.resolveLocalFileSystemURL = resolveLocalFileSystemURL; + +// Export more stuff (to window) for unit tests to do their thing. +if (exports === window && exports.RUNNING_TESTS) { + exports['Entry'] = Entry; + exports['FileEntry'] = FileEntry; + exports['DirectoryEntry'] = DirectoryEntry; + exports['resolveToFullPath_'] = resolveToFullPath_; + exports['Metadata'] = Metadata; + exports['Base64ToBlob'] = Base64ToBlob; +} + +})(self); // Don't use window because we want to run in workers. diff --git a/libraries/idbfs/css/app.css b/libraries/idbfs/css/app.css new file mode 100644 index 0000000..b329ee6 --- /dev/null +++ b/libraries/idbfs/css/app.css @@ -0,0 +1,566 @@ +body ::-webkit-scrollbar { + height: 16px; + overflow: visible; + width: 16px; +} + +body ::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, .2); + background-clip: padding-box; + border: solid transparent; + min-height: 28px; + padding: 100px 0 0; + box-shadow: inset 1px 1px 0 rgba(0,0,0,.1), inset 0 -1px 0 rgba(0,0,0,.07); + border-width: 1px 1px 1px 6px; +} + +body ::-webkit-scrollbar-button { + height: 0; + width: 0; +} + +body ::-webkit-scrollbar-track { + background-clip: padding-box; + border: solid transparent; + border-width: 0 0 0 4px; + } + +body ::-webkit-scrollbar-corner { + background: transparent; +} + +html, body { + font: 15px 'Open Sans', Trebuchet, Arial, sans-serif; + color: #444; + margin: 0; + height: 100%; + width: 100%; + overflow: hidden; +} +body { + background: -webkit-radial-gradient(center center, #fff, #eee) no-repeat; + background: -moz-radial-gradient(center center, #fff, #eee) no-repeat; + background: -ms-radial-gradient(center center, #fff, #eee) no-repeat; + background: -o-radial-gradient(center center, #fff, #eee) no-repeat; + color: #222; + overflow-x: hidden; + -webkit-transition: -webkit-transform 300ms ease-in-out; + -webkit-transform-origin: 50% 100%; + -webkit-font-smoothing: antialiased; + -moz-transition: -moz-transform 300ms ease-in-out; + -moz-transform-origin: 50% 100%; + -moz-font-smoothing: antialiased; + -ms-transition: -moz-transform 300ms ease-in-out; + -ms-transform-origin: 50% 100%; + -ms-font-smoothing: antialiased; + -o-transition: -moz-transform 300ms ease-in-out; + -o-transform-origin: 50% 100%; + -o-font-smoothing: antialiased; +} +label { + cursor: pointer; + margin-left: 3px; +} +[contenteditable] { + border: 1px dashed transparent; + outline: none; + border-radius: 5px; +} +[contenteditable]:hover { + border: 1px dashed #999; +} +a { + color: navy; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +ul, li { + margin: 0; + padding: 0; + list-style: none; +} +li { + padding-bottom: 2px; +} +.dropping { + -webkit-transform: scale(0.95); + -moz-transform: scale(0.95); + -ms-transform: scale(0.95); + -o-transform: scale(0.95); + background-image: -webkit-gradient( + linear, left bottom, left top, + color-stop(0.13, rgb(209,144,23)), + color-stop(0.57, rgb(251,173,51)), + color-stop(0.79, rgb(255,208,77)) + ); + background-image: -moz-gradient( + linear, left bottom, left top, + color-stop(0.13, rgb(209,144,23)), + color-stop(0.57, rgb(251,173,51)), + color-stop(0.79, rgb(255,208,77)) + ); + background-image: -ms-gradient( + linear, left bottom, left top, + color-stop(0.13, rgb(209,144,23)), + color-stop(0.57, rgb(251,173,51)), + color-stop(0.79, rgb(255,208,77)) + ); + background-image: -o-gradient( + linear, left bottom, left top, + color-stop(0.13, rgb(209,144,23)), + color-stop(0.57, rgb(251,173,51)), + color-stop(0.79, rgb(255,208,77)) + ); +} +.dropping #container { + background: white; + border-radius: 10px; + opacity: 0.8; +} +.fakebutton { + text-align: center; + line-height: 0; + display: inline-block; +} +.fakebutton > div { + width: 15px; + height: 15px; + display: inline-block; + background: url(../images/icons/sprite_black.png) -65px -23px no-repeat; +} +:-webkit-any(button, .fakebutton), +input[type="file"].button:before { + background: -webkit-linear-gradient(#F9F9F9 40%, #E3E3E3 70%); + border: 1px solid #888; + border-radius: 3px; + margin: 0 8px 0 0; + color: black; + padding: 5px 8px; + outline: none; + white-space: nowrap; + vertical-align: middle; + -webkit-user-select:none; + user-select: none; + cursor: pointer; + text-shadow: 1px 1px #fff; + font-weight: 700; +} +:-webkit-any(button, .fakebutton):not(:disabled):hover, +input[type="file"].button:not(:disabled):hover:before { + border: 1px solid #000; +} +:-webkit-any(button, .fakebutton):not(:disabled):active, +input[type="file"].button:not(:disabled):active:before { + background: -webkit-linear-gradient(#E3E3E3 40%, #F9F9F9 70%); +} +:-moz-any(button, .fakebutton), +input[type="file"].button:before { + background: -moz-linear-gradient(#F9F9F9 40%, #E3E3E3 70%); + border: 1px solid #888; + border-radius: 3px; + margin: 0 8px 0 0; + color: black; + padding: 5px 8px; + outline: none; + white-space: nowrap; + vertical-align: middle; + -moz-user-select:none; + user-select: none; + cursor: pointer; + text-shadow: 1px 1px #fff; + font-weight: 700; +} +:-moz-any(button, .fakebutton):not(:disabled):hover, +input[type="file"].button:not(:disabled):hover:before { + border: 1px solid #000; +} +:-moz-any(button, .fakebutton):not(:disabled):active, +input[type="file"].button:not(:disabled):active:before { + background: -moz-linear-gradient(#E3E3E3 40%, #F9F9F9 70%); +} +input[type="file"].button { + width: 112px; + height: 26px; +} +input[type="file"].button::-webkit-file-upload-button { + visibility: hidden; +} +input[type="file"].button:before { + content: 'Import directory'; + display: inline-block; + outline: none; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: pointer; +} +input[type="file"]#fileElem { + /* Note: display:none on the input won't trigger the click event in WebKit. + Setting visibility to hidden and width 0 works.*/ + visibility: hidden; + width: 0; + height: 0; +} +:disabled, +input[type="file"].button:disabled:before { + color: #ccc; +} +input[type='text'], textarea, iframe { + margin: 0; + padding: 5px; + border-radius: 3px; + border: 1px solid #ccc; + box-shadow: 0 3px 3px #eee inset; + outline: none; + background: white; +} +input { + vertical-align: middle; +} +body #container { + padding: 1em; +} + +header, footer { + padding: 2px 0; + display: -webkit-flex; + /*display: -moz-box;*/ + border-radius: 5px; + box-shadow: 0 5px 5px #ccc; + background: white; + background: rgb(207,231,250); + background: -webkit-linear-gradient(top, rgba(207,231,250,1) 0%, rgba(99,147,193,1) 100%); + background: -moz-linear-gradient(top, rgba(207,231,250,1) 0%, rgba(99,147,193,1) 100%); + background: -ms-linear-gradient(top, rgba(207,231,250,1) 0%, rgba(99,147,193,1) 100%); + background: -o-linear-gradient(top, rgba(207,231,250,1) 0%, rgba(99,147,193,1) 100%); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select:none; + user-select: none; + -webkit-transform: translateY(0) translateZ(0); + -webkit-transition: -webkit-transform 1s ease-in-out; + /*-webkit-transition-delay: 0.5s;*/ + -moz-transform: translateY(0) translateZ(0); + -moz-transition: -moz-transform 1s ease-in-out; + -ms-transform: translateY(0) translateZ(0); + -ms-transition: -ms-transform 1s ease-in-out; + -o-transform: translateY(0) translateZ(0); + -o-transition: -o-transform 1s ease-in-out; +} +footer { + height: 38px; + margin-top: 10px; + clear: both; +} +.offscreen header { + -webkit-transform: translateY(-500px); + -moz-transform: translateY(-500px); + -ms-transform: translateY(-500px); + -o-transform: translateY(-500px); +} +.offscreen footer { + -webkit-transform: translateY(500px); + -moz-transform: translateY(500px); + -ms-transform: translateY(500px); + -o-transform: translateY(500px); +} +header > * { + -webkit-align-items: center; + -moz-align-items: center; + -o-align-items: center; + -ms-align-items: center; + display: inline-block; +} +header > div { + -webkit-flex: 1; + -moz-flex: 1; + -ms-flex: 1; + -o-flex: 1; + text-align: right; +} +header > div > span { + border-right: 2px solid rgba(255,255,255,0.25); + padding: 0 10px; + vertical-align: middle; + color: white; +} +header > div > span:last-child { + border-right: none; +} +header > div > span a { + color: inherit; + font-weight: bold; +} +header img { + height: 45px; + width: 45px; + margin: -15px 7px 0 7px; +} +header h1 { + font-family: 'Open Sans'; + font-size: 21px; + text-shadow: 0px -1px black; + color: white; + margin: 0; +} +header h1 a { + color: inherit; +} +#files { + float: left; + clear: both; + height: 400px; + display: inline-block; + overflow-y: auto; + overflow-x: hidden; + border: 1px solid #eee; + border-radius: 7px; + box-shadow: 0 -2px 3px #ddd inset, 0 2px 3px #ddd inset; + padding: 10px; + background: -webkit-linear-gradient(#fff 80%, #eee); + background: -moz-linear-gradient(#fff 80%, #eee); + background: -o-linear-gradient(#fff 80%, #eee); + background: -ms-linear-gradient(#fff 80%, #eee); + box-sizing: border-box; +} +#files li { + -webkit-transition: all 0.2s ease-out; + -webkit-column-break-inside: avoid; + -moz-transition: all 0.2s ease-out; + -moz-column-break-inside: avoid; + -ms-transition: all 0.2s ease-out; + -ms-column-break-inside: avoid; + -o-transition: all 0.2s ease-out; + -o-column-break-inside: avoid; + white-space: nowrap; + padding: 10px; + border-radius: 12px; + border: 3px solid transparent; +} +#files li.on { + background: rgba(207,231,250,1); + border-color: rgba(99,147,193,1); +} +#files li img { + height: 16px; + width: 16px; +} +img.folder, +img.file, +img.download { + cursor: pointer; +} +.parentDir:hover { + text-decoration: none; +} +.parentDir img { + vertical-align: bottom; +} +#files li img, +#files li > div { + vertical-align: middle; + display: inline-block; +} +#files li > a { + font-size: small; +} +#files li:hover img.icon { + opacity: 0.25; +} +#files li a img.icon { + height: 25px; + width: 25px; + opacity: 0; + /*-webkit-transition: opacity 0.4s ease-out;*/ +} +#files li a[download] img.icon { + height: 21px; + width: 21px; + margin: 0 2px 0 6px; +} +#files li a img.icon:hover { + opacity: 1; +} +#files li.fadeout { + opacity: 0; +} +#files li div[data-filename] { + width: 150px; + min-width: 150px; + margin: 0 5px; + padding: 1px 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +#files li div[data-filename]:hover { + overflow: visible; + width: auto; +} +#file-info { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + box-sizing: border-box; + text-align: center; + display: none; +} +#file-info.show { + z-index: 1000; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-box-pack: center; + display: -moz-box; + -moz-box-orient: vertical; + -moz-box-pack: center; + display: -ms-box; + -ms-box-orient: vertical; + -ms-box-pack: center; + display: -o-box; + -o-box-orient: vertical; + -o-box-pack: center; +} +#file-info > div, +#file-info p { + padding: 10px; + background: #fff; + margin-bottom: 2em; + box-shadow: 0 0 15px #222; +} +#file-info p { + display: inline-block; + margin: 1em 0 0 0; + padding: 5em; +} +#file-info textarea { + width: 400px; + height: 100px; +} +#file-info img { + max-height: 350px; + border-radius: 5px; + box-shadow: 0 0 15px #222; +} +#file-info iframe { + width: 500px; + height: 300px; +} +#errors:not(:empty) { + margin: 15px 0 -15px 0; + text-align: center; +} +#errors::before { + content: ''; +} +.error { + color: red !important; +} +.error::before { + content: 'Error: '; +} +header .active { + text-decoration: underline; +} +#log { + font: 90% 'Courier New'; + position: absolute; + background: -webkit-linear-gradient(#fff 40%, #eee); + background: -moz-linear-gradient(#fff 40%, #eee); + background: -ms-linear-gradient(#fff 40%, #eee); + background: -o-linear-gradient(#fff 40%, #eee); + top: 0; + right: 0; + width: 375px; + height: 100%; + overflow: auto; + overflow-x: hidden; + padding: 40px 10px 10px 10px; + box-sizing: border-box; + box-shadow: -3px 0 10px #ddd; + -webkit-transition: -webkit-transform 0.2s ease-out; + -webkit-transform: translate(500px) translateZ(0); + -moz-transition: -moz-transform 0.2s ease-out; + -moz-transform: translate(500px) translateZ(0); + -ms-transition: -ms-transform 0.2s ease-out; + -ms-transform: translate(500px) translateZ(0); + -o-transition: -webkit-transform 0.2s ease-out; + -o-transform: translate(500px) translateZ(0); + z-index: 100; +} +#log.active { + -webkit-transform: translateX(0) translateZ(0); + -moz-transform: translateX(0) translateZ(0); + -ms-transform: translateX(0) translateZ(0); + -o-transform: translateX(0) translateZ(0); +} +#log button { + position: absolute; + right: 0; + top: 0; + margin: 10px; +} +#log p { + color: navy; + margin: 3px 0; +} +#files.large { + float: none; + width: 100%; + /*height: auto;*/ +} +#files.large ul { + -webkit-column-count: 3; + -webkit-column-gap: 0; + -moz-column-count: 3; + -moz-column-gap: 0; + -ms-column-count: 3; + -ms-column-gap: 0; + -o-column-count: 3; + -o-column-gap: 0; +} +#files.large li { + padding-bottom: 10px; +} +#files.large li:first-child { + -webkit-column-span: all; + -moz-column-span: all; + -ms-column-span: all; + -o-column-span: all; +} +#files.large li .parentDir img { + height: 16px; + width: 16px; +} +#files.large li img { + height: 50px; + width: 50px; +} +#cwd { + width: 80%; + -webkit-user-select:none; + -moz-user-select: none; + user-select: none; +} +#ticker { + opacity: 0; + -webkit-transition: opacity 1s ease-in-out; + -moz-transition: opacity 1s ease-in-out; + -ms-transition: opacity 1s ease-in-out; + -o-transition: opacity 1s ease-in-out; +} +#ticker.fadedIn { + opacity: 1; +} +#ticker .icon { + height: 25px; + width: 25px; + vertical-align: middle; +} diff --git a/libraries/idbfs/images/demo_screenshot.png b/libraries/idbfs/images/demo_screenshot.png new file mode 100644 index 0000000..8f1a2d8 Binary files /dev/null and b/libraries/idbfs/images/demo_screenshot.png differ diff --git a/libraries/idbfs/images/icons/automatic_updates.png b/libraries/idbfs/images/icons/automatic_updates.png new file mode 100644 index 0000000..29f2976 Binary files /dev/null and b/libraries/idbfs/images/icons/automatic_updates.png differ diff --git a/libraries/idbfs/images/icons/download.png b/libraries/idbfs/images/icons/download.png new file mode 100644 index 0000000..d2ffe45 Binary files /dev/null and b/libraries/idbfs/images/icons/download.png differ diff --git a/libraries/idbfs/images/icons/easy_eject.png b/libraries/idbfs/images/icons/easy_eject.png new file mode 100644 index 0000000..3d29c81 Binary files /dev/null and b/libraries/idbfs/images/icons/easy_eject.png differ diff --git a/libraries/idbfs/images/icons/favicon.png b/libraries/idbfs/images/icons/favicon.png new file mode 100644 index 0000000..dd2c78c Binary files /dev/null and b/libraries/idbfs/images/icons/favicon.png differ diff --git a/libraries/idbfs/images/icons/file.png b/libraries/idbfs/images/icons/file.png new file mode 100644 index 0000000..653ef2b Binary files /dev/null and b/libraries/idbfs/images/icons/file.png differ diff --git a/libraries/idbfs/images/icons/folder.png b/libraries/idbfs/images/icons/folder.png new file mode 100644 index 0000000..c6a9e73 Binary files /dev/null and b/libraries/idbfs/images/icons/folder.png differ diff --git a/libraries/idbfs/images/icons/library.png b/libraries/idbfs/images/icons/library.png new file mode 100644 index 0000000..edef90c Binary files /dev/null and b/libraries/idbfs/images/icons/library.png differ diff --git a/libraries/idbfs/images/icons/settings.png b/libraries/idbfs/images/icons/settings.png new file mode 100644 index 0000000..7694331 Binary files /dev/null and b/libraries/idbfs/images/icons/settings.png differ diff --git a/libraries/idbfs/images/icons/sprite_black.png b/libraries/idbfs/images/icons/sprite_black.png new file mode 100644 index 0000000..e675952 Binary files /dev/null and b/libraries/idbfs/images/icons/sprite_black.png differ diff --git a/libraries/idbfs/images/icons/tools.png b/libraries/idbfs/images/icons/tools.png new file mode 100644 index 0000000..64d07d6 Binary files /dev/null and b/libraries/idbfs/images/icons/tools.png differ diff --git a/libraries/idbfs/images/icons/trash_empty.png b/libraries/idbfs/images/icons/trash_empty.png new file mode 100644 index 0000000..48dc2ec Binary files /dev/null and b/libraries/idbfs/images/icons/trash_empty.png differ diff --git a/libraries/idbfs/index.html b/libraries/idbfs/index.html new file mode 100755 index 0000000..9b15283 --- /dev/null +++ b/libraries/idbfs/index.html @@ -0,0 +1,114 @@ + + + +
+ + +' + e.name + '
'); + errors.textContent = e.name; +} + +function refreshFolder(e) { + errors.textContent = ''; // Reset errors. + + // Open the FS, otherwise list the files. + if (filer && !filer.isOpen) { + openFS(); + } else { + filer.ls('.', function(entries) { + renderEntries(entries); + }, onError); + } +} +function display(el, type) { + Util.toArray(document.querySelectorAll('[data-display-type]')).forEach(function(el, i) { + el.classList.remove('active'); + }); + + if (type == 'list') { + filesContainer.classList.remove('large'); + document.querySelector('[data-display-type="list"]').classList.add('active'); + } else { + filesContainer.classList.add('large'); + document.querySelector('[data-display-type="icons"]').classList.add('active'); + } +} + +function toggleContentEditable(el) { + if (el.isContentEditable) { + el.removeAttribute('contenteditable'); + } else { + el.setAttribute('contenteditable', ''); + el.focus(); + } +} + +function toggleLog(opt_hide) { + if (opt_hide) { + document.querySelector('#log').classList.remove('active'); + document.querySelector('#toggle-log').checked = false; + } else { + document.querySelector('#log').classList.toggle('active'); + } +} + + +function click(el) { + // Simulate link click on an element. + var evt = document.createEvent('Event'); + evt.initEvent('click', false, false); + el.dispatchEvent(evt); +} + + +function constructEntryHTML(entry, i) { + var img = entry.isDirectory ? + '' : + ''; + + var html = [img, 'Opened: ' + fs.name, + '
'); + + setCwd('/'); // Display current path as root. + refreshFolder(); + openFsButton.innerHTML = ''; + openFsButton.classList.add('fakebutton'); + importButton.disabled = true; // mozdirectory doesn't work in FF. + createButton.disabled = false; + }, function(e) { + if (e.name == 'SECURITY_ERR') { + errors.textContent = 'SECURITY_ERR: Are you running in incognito mode?'; + openFsButton.innerHTML = ''; + openFsButton.classList.add('fakebutton'); + return; + } + onError(e); + }); + } catch(e) { + if (e.code == FileError.BROWSER_NOT_SUPPORTED) { + fileList.innerHTML = 'BROWSER_NOT_SUPPORTED'; + } + } +} + +function setCwd(path) { + var cwd = document.querySelector('#cwd').value; + var rootPath = filer.pathToFilesystemURL('/'); + + if (path == '/' || (path == '..' && (rootPath == cwd))) { + document.querySelector('#cwd').value = filer.pathToFilesystemURL('/'); + return; + } else if (path == '..') { + var parts = cwd.split('/'); + parts.pop(); + path = parts.join('/'); + if (path == rootPath.substring(0, rootPath.length - 1)) { + path += '/'; + } + } + + document.querySelector('#cwd').value = filer.pathToFilesystemURL(path); +} + +function mkdir(name, opt_callback) { + if (!name) return; + + errors.textContent = ''; // Reset errors. + + try { + if (opt_callback) { + filer.mkdir(name, false, opt_callback, onError); + } else { + filer.mkdir(name, true, addEntryToList, onError); + } + } catch(e) { + logger.log('' + e + '
'); + } +} + +function cd(i, opt_callback) { + errors.textContent = ''; // Reset errors. + + if (i == -1) { + var path = '..'; + } else { + var path = entries[i].fullPath; + } + + setCwd(path); + + if (opt_callback) { + filer.ls(path, opt_callback, onError); + } else { + filer.ls(path, renderEntries, onError); + } +} + +function openFile(i) { + errors.textContent = ''; // Reset errors. + var fileWin = self.open(toURL(entries[i]), 'fileWin'); +} + +function newFile(name) { + if (!name) return; + + errors.textContent = ''; // Reset errors. + + try { + filer.create(name, true, addEntryToList, onError); + } catch(e) { + onError(e); + } +} + +function writeFile(fileName, file, opt_rerender) { + if (!file) return; + + var rerender = opt_rerender == undefined ? true : false; + + errors.textContent = ''; // Reset errors. + + filer.write(fileName, {data: file, type: file.type}, + function(fileEntry, fileWriter) { + if (rerender) { + addEntryToList(fileEntry); + filer.ls('.', renderEntries, onError); // Just re-read this dir. + } + }, + onError + ); +} + +function rename(el, i) { + errors.textContent = ''; // Reset errors. + + filer.mv(entries[i].fullPath, '.', el.textContent, function(entry) { + logger.log('' + entries[i].name + ' renamed to ' + entry.name + '
'); + entries[i] = entry; + + // Fill download link with updated filsystem URL. + var downloadLink = el.parentElement.querySelector('[download]'); + if (downloadLink) { + downloadLink.href = toURL(entry); + } + }); + toggleContentEditable(el); +} + +function remove(link, i) { + errors.textContent = ''; // Reset errors. + + var entry = entries[i]; + + if (!confirm('Delete ' + entry.name + '?')) { + return; + } + + filer.rm(entry, function() { + var li = link.parentNode; + li.classList.add('fadeout'); + li.addEventListener('webkitTransitionEnd', function(e) { + this.parentNode.removeChild(this); + filer.ls('.', renderEntries, onError); // Just re-read this dir. + }, false); + li.addEventListener('mozTransitionEnd', function(e) { + this.parentNode.removeChild(this); + filer.ls('.', renderEntries, onError); // Just re-read this dir. + }, false); + }, onError); +} + +function copy(el, i) { + errors.textContent = ''; // Reset errors. + + filer.cp(entries[i], el.textContent, function(entry) { + logger.log('' + entries[i].name + ' renamed to ' + entry.name + '
'); + entries[i] = entry; + }); + toggleContentEditable(el); +} + +function readFile(i) { + errors.textContent = ''; // Reset errors. + + var entry = entries[i]; + + try { + filer.open(entry.name, function(file) { + + filePreview.classList.toggle('show'); + + filePreview.innerHTML = [ + '' + e + '
'); + } +} + +function onKeydown(e) { + var target = e.target; + + // Prevent enter key from inserting carriage return in the contenteditable + // file/folder renaming. + if (target.isContentEditable && 'filename' in target.dataset) { + if (e.keyCode == 13) { // Enter + e.preventDefault(); + e.stopPropagation(); + target.blur(); + } + return; + } + + var active = document.querySelector('#files li.on'); + + if (e.keyCode == 27) { // ESC + filePreview.classList.remove('show'); + filePreview.innerHTML = ''; + + if (active) { + active.classList.remove('on'); + } + + toggleLog(true); + + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (target.nodeName != 'INPUT') { + if (e.keyCode == 8) { // Backspace. + if (active) { + click(active.querySelector('a[data-remove-link]')); + } + e.preventDefault(); + return; + } else if (e.keyCode == 13) { // Enter + if (active) { + if (active.querySelector('.folder')) { + currentLi = 1; // reset current active to first item in current folder. + } + click(active.querySelector('a[data-preview-link]') || active.querySelector('img')); + e.preventDefault(); + return; + } + } + } + + if (active) { + active.classList.remove('on'); + } + + var count = entries.length + 1; + + if (e.keyCode == 39 || e.keyCode == 40) { // Right/down arrow. + currentLi = currentLi == count ? 1 : ++currentLi; + document.querySelector('#files li:nth-of-type(' + currentLi + ')').classList.toggle('on'); + e.preventDefault(); + } else if (e.keyCode == 37 || e.keyCode == 38) { // Left/up arrow. + currentLi = currentLi == 1 ? count : --currentLi; + document.querySelector('#files li:nth-of-type(' + currentLi + ')').classList.toggle('on'); + e.preventDefault(); + } +} + +function onImport(e) { + var files = e.target.files; + if (files.length) { + var count = 0; + Util.toArray(files).forEach(function(file, i) { + + var folders = file.webkitRelativePath.split('/'); + folders = folders.slice(0, folders.length - 1); + + // Add each directory. If it already exists, then a noop. + mkdir(folders.join('/'), function(dirEntry) { + var path = file.webkitRelativePath; + + ++count; + + // Write each file by it's path. Skipt '/.' (which is a directory). + if (path.lastIndexOf('/.') != path.length - 2) { + writeFile(path, file, false); + if (count == files.length) { + filer.ls('.', renderEntries, onError); // Rerender view on final file. + } + } + }); + }); + } +} + +function addListeners() { + importButton.addEventListener('click', function(e) { + fileDirInput.click(); + }, false); + fileDirInput.addEventListener('change', onImport, false); + document.addEventListener('keydown', onKeydown, false); + + var dnd = new DnDFileController('body', function(files) { + Util.toArray(files).forEach(function(file, i) { + writeFile(file.name, file); + }); + }); +} + +// DOMContentLoaded seems to be an issue with FF. +window.addEventListener('load', function(e) { + var count = 0; + setInterval(function() { + ticker.innerHTML = + 'Tip: ' + TICKER_LIST[count++ % TICKER_LIST.length]; + ticker.classList.add('fadedIn'); + }, 7000); +}, false); + +window.addEventListener('load', function(e) { + addListeners(); + document.querySelector('.offscreen').classList.remove('offscreen'); +}, false); diff --git a/libraries/idbfs/js/dnd.js b/libraries/idbfs/js/dnd.js new file mode 100644 index 0000000..e2ebf71 --- /dev/null +++ b/libraries/idbfs/js/dnd.js @@ -0,0 +1,34 @@ +function DnDFileController(selector, onDropCallback) { + var el_ = document.querySelector(selector); + + this.dragenter = function(e) { + e.stopPropagation(); + e.preventDefault(); + el_.classList.add('dropping'); + }; + + this.dragover = function(e) { + e.stopPropagation(); + e.preventDefault(); + }; + + this.dragleave = function(e) { + e.stopPropagation(); + e.preventDefault(); + //el_.classList.remove('dropping'); + }; + + this.drop = function(e) { + e.stopPropagation(); + e.preventDefault(); + + el_.classList.remove('dropping'); + + onDropCallback(e.dataTransfer.files) + }; + + el_.addEventListener('dragenter', this.dragenter, false); + el_.addEventListener('dragover', this.dragover, false); + el_.addEventListener('dragleave', this.dragleave, false); + el_.addEventListener('drop', this.drop, false); +}; diff --git a/libraries/idbfs/js/filer.js b/libraries/idbfs/js/filer.js new file mode 100755 index 0000000..ca8b95e --- /dev/null +++ b/libraries/idbfs/js/filer.js @@ -0,0 +1,16 @@ +var self=this;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; +if(void 0===self.FileError){var FileError=function(){};FileError.prototype.prototype=Error.prototype} +var Util={toArray:function(a){return Array.prototype.slice.call(a||[],0)},strToDataURL:function(a,b,c){return(void 0!=c?c:1)?"data:"+b+";base64,"+self.btoa(a):"data:"+b+","+a},strToObjectURL:function(a,b){for(var c=new Uint8Array(a.length),e=0;e