1/** 2 * Copyright 2013 - Eric Bidelman 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 16 * @fileoverview 17 * A polyfill implementation of the HTML5 Filesystem API which sits on top of 18 * IndexedDB as storage layer. Files and folders are stored as FileEntry and 19 * FolderEntry objects in a single object store. IDBKeyRanges are used to query 20 * into a folder. A single object store is sufficient because we can utilize the 21 * properties of ASCII. Namely, ASCII / is followed by ASCII 0. Thus, 22 * "/one/two/" comes before "/one/two/ANYTHING" comes before "/one/two/0". 23 * 24 * @author Eric Bidelman ([email protected]) 25 */ 26 27'use strict'; 28 29(function(exports) { 30 31// Bomb out if the Filesystem API is available natively. 32if (exports.requestFileSystem || exports.webkitRequestFileSystem) { 33 return; 34} 35 36// Bomb out if no indexedDB available 37const indexedDB = exports.indexedDB || exports.mozIndexedDB || 38 exports.msIndexedDB; 39if (!indexedDB) { 40 return; 41} 42 43let IDB_SUPPORTS_BLOB = true; 44 45// Check to see if IndexedDB support blobs 46const support = function() { 47 var dbName = "blob-support"; 48 indexedDB.deleteDatabase(dbName).onsuccess = function() { 49 var request = indexedDB.open(dbName, 1); 50 request.onerror = function() { 51 IDB_SUPPORTS_BLOB = false; 52 }; 53 request.onsuccess = function() { 54 var db = request.result; 55 try { 56 var blob = new Blob(["test"], {type: "text/plain"}); 57 var transaction = db.transaction("store", "readwrite"); 58 transaction.objectStore("store").put(blob, "key"); 59 IDB_SUPPORTS_BLOB = true; 60 } catch (err) { 61 IDB_SUPPORTS_BLOB = false; 62 } finally { 63 db.close(); 64 indexedDB.deleteDatabase(dbName); 65 } 66 }; 67 request.onupgradeneeded = function() { 68 request.result.createObjectStore("store"); 69 }; 70 }; 71}; 72 73const Base64ToBlob = function(dataURL) { 74 var BASE64_MARKER = ';base64,'; 75 if (dataURL.indexOf(BASE64_MARKER) == -1) { 76 var parts = dataURL.split(','); 77 var contentType = parts[0].split(':')[1]; 78 var raw = decodeURIComponent(parts[1]); 79 80 return new Blob([raw], {type: contentType}); 81 } 82 83 var parts = dataURL.split(BASE64_MARKER); 84 var contentType = parts[0].split(':')[1]; 85 var raw = window.atob(parts[1]); 86 var rawLength = raw.length; 87 88 var uInt8Array = new Uint8Array(rawLength); 89 90 for (var i = 0; i < rawLength; ++i) { 91 uInt8Array[i] = raw.charCodeAt(i); 92 } 93 94 return new Blob([uInt8Array], {type: contentType}); 95}; 96 97const BlobToBase64 = function(blob, onload) { 98 var reader = new FileReader(); 99 reader.readAsDataURL(blob); 100 reader.onloadend = function() { 101 onload(reader.result); 102 }; 103}; 104 105if (!exports.PERSISTENT) { 106 exports.TEMPORARY = 0; 107 exports.PERSISTENT = 1; 108} 109 110// Prevent errors in browsers that don't support FileError. 111// TODO: FF 13+ supports DOM4 Events (DOMError). Use them instead? 112if (exports.FileError === undefined) { 113 window.FileError = function() {}; 114 FileError.prototype.prototype = Error.prototype; 115} 116 117if (!FileError.INVALID_MODIFICATION_ERR) { 118 FileError.INVALID_MODIFICATION_ERR = 9; 119 FileError.NOT_FOUND_ERR = 1; 120} 121 122function MyFileError(obj) { 123 var code_ = obj.code; 124 var name_ = obj.name; 125 126 // Required for FF 11. 127 Object.defineProperty(this, 'code', { 128 set: function(code) { 129 code_ = code; 130 }, 131 get: function() { 132 return code_; 133 } 134 }); 135 136 Object.defineProperty(this, 'name', { 137 set: function(name) { 138 name_ = name; 139 }, 140 get: function() { 141 return name_; 142 } 143 }); 144} 145 146MyFileError.prototype = FileError.prototype; 147MyFileError.prototype.toString = Error.prototype.toString; 148 149const INVALID_MODIFICATION_ERR = new MyFileError({ 150 code: FileError.INVALID_MODIFICATION_ERR, 151 name: 'INVALID_MODIFICATION_ERR'});
152const NOT_IMPLEMENTED_ERR = new MyFileError({code: 1000, 153 name: 'Not implemented'}); 154const NOT_FOUND_ERR = new MyFileError({code: FileError.NOT_FOUND_ERR, 155 name: 'Not found'}); 156 157let fs_ = null; 158 159// Browsers other than Chrome don't implement persistent vs. temporary storage. 160// but default to temporary anyway. 161let storageType_ = 'temporary'; 162const idb_ = {db: null}; 163const FILE_STORE_ = 'entries'; 164 165const DIR_SEPARATOR = '/'; 166const DIR_OPEN_BOUND = String.fromCharCode(DIR_SEPARATOR.charCodeAt(0) + 1); 167 168// When saving an entry, the fullPath should always lead with a slash and never 169// end with one (e.g. a directory). Also, resolve '.' and '..' to an absolute 170// one. This method ensures path is legit! 171function resolveToFullPath_(cwdFullPath, path) { 172 var fullPath = path; 173 174 var relativePath = path[0] != DIR_SEPARATOR; 175 if (relativePath) { 176 fullPath = cwdFullPath + DIR_SEPARATOR + path; 177 } 178 179 // Normalize '.'s, '..'s and '//'s. 180 var parts = fullPath.split(DIR_SEPARATOR); 181 var finalParts = []; 182 for (var i = 0; i < parts.length; ++i) { 183 var part = parts[i]; 184 if (part === '..') { 185 // Go up one level. 186 if (!finalParts.length) { 187 throw Error('Invalid path'); 188 } 189 finalParts.pop(); 190 } else if (part === '.') { 191 // Skip over the current directory. 192 } else if (part !== '') { 193 // Eliminate sequences of '/'s as well as possible leading/trailing '/'s. 194 finalParts.push(part); 195 } 196 } 197 198 fullPath = DIR_SEPARATOR + finalParts.join(DIR_SEPARATOR); 199 200 // fullPath is guaranteed to be normalized by construction at this point: 201 // '.'s, '..'s, '//'s will never appear in it. 202 203 return fullPath; 204} 205 206// // Path can be relative or absolute. If relative, it's taken from the cwd_. 207// // If a filesystem URL is passed it, it is simple returned 208// function pathToFsURL_(path) { 209// path = resolveToFullPath_(cwdFullPath, path); 210// path = fs_.root.toURL() + path.substring(1); 211// return path; 212// }; 213 214/** 215 * Interface to wrap the native File interface. 216 * 217 * This interface is necessary for creating zero-length (empty) files, 218 * something the Filesystem API allows you to do. Unfortunately, File's 219 * constructor cannot be called directly, making it impossible to instantiate 220 * an empty File in JS. 221 * 222 * @param {Object} opts Initial values. 223 * @constructor 224 */ 225function MyFile(opts) { 226 var blob_ = null; 227 228 this.size = opts.size || 0; 229 this.name = opts.name || ''; 230 this.type = opts.type || ''; 231 this.lastModifiedDate = opts.lastModifiedDate || null; 232 //this.slice = Blob.prototype.slice; // Doesn't work with structured clones. 233 234 // Need some black magic to correct the object's size/name/type based on the 235 // blob that is saved. 236 Object.defineProperty(this, 'blob_', { 237 enumerable: true, 238 get: function() { 239 return blob_; 240 }, 241 set: function (val) { 242 blob_ = val; 243 this.size = blob_.size; 244 this.name = blob_.name; 245 this.type = blob_.type; 246 this.lastModifiedDate = blob_.lastModifiedDate; 247 }.bind(this) 248 }); 249} 250MyFile.prototype.constructor = MyFile; 251//MyFile.prototype.slice = Blob.prototype.slice; 252 253/** 254 * Interface to writing a Blob/File. 255 * 256 * Modeled from: 257 * dev.w3.org/2009/dap/file-system/file-writer.html#the-filewriter-interface 258 * 259 * @param {FileEntry} fileEntry The FileEntry associated with this writer. 260 * @constructor 261 */ 262function FileWriter(fileEntry) { 263 if (!fileEntry) { 264 throw Error('Expected fileEntry argument to write.'); 265 } 266 267 var position_ = 0; 268 var blob_ = fileEntry.file_ ? fileEntry.file_.blob_ : null; 269 270 Object.defineProperty(this, 'position', { 271 get: function() { 272 return position_; 273 } 274 }); 275 276 Object.defineProperty(this, 'length', { 277 get: function() { 278 return blob_ ? blob_.size : 0; 279 } 280 }); 281 282 this.seek = function(offset) { 283 position_ = offset; 284 285 if (position_ > this.length) { 286 position_ = this.length; 287 } 288 if (position_ < 0) { 289 position_ += this.length; 290 } 291 if (position_ < 0) { 292 position_ = 0; 293 } 294 }; 295 296 this.truncate = function(size) { 297 if (blob_) { 298 if (size < this.length) { 299 blob_ = blob_.slice(0, size); 300 } else { 301 blob_ = new Blob([blob_, new Uint8Array(size - this.length)], 302 {type: blob_.type}); 303 } 304 } else { 305 blob_ = new Blob([]); 306 } 307 308 position_ = 0; // truncate from beginning of file. 309 310 this.write(blob_); // calls onwritestart and onwriteend. 311 }; 312 313 this.write = function(data) { 314 if (!data) { 315 throw Error('Expected blob argument to write.'); 316 } 317 318 // Call onwritestart if it was defined. 319 if (this.onwritestart) { 320 this.onwritestart(); 321 } 322 323 // TODO: not handling onprogress, onwrite, onabort. Throw an error if 324 // they're defined. 325 326 if (blob_) { 327 // Calc the head and tail fragments 328 var head = blob_.slice(0, position_); 329 var tail = blob_.slice(position_ + data.size); 330 331 // Calc the padding 332 var padding = position_ - head.size; 333 if (padding < 0) { 334 padding = 0; 335 } 336 337 // Do the "write". In fact, a full overwrite of the Blob.
338 // TODO: figure out if data.type should overwrite the exist blob's type. 339 blob_ = new Blob([head, new Uint8Array(padding), data, tail], 340 {type: blob_.type}); 341 } else { 342 blob_ = new Blob([data], {type: data.type}); 343 } 344 345 const writeFile = function(blob) { 346 // Blob might be a DataURI depending on browser support. 347 fileEntry.file_.blob_ = blob; 348 fileEntry.file_.lastModifiedDate = data.lastModifiedDate || new Date(); 349 idb_.put(fileEntry, function(entry) { 350 if (!IDB_SUPPORTS_BLOB) { 351 // Set the blob we're writing on this file entry so we can recall it later. 352 fileEntry.file_.blob_ = blob_; 353 fileEntry.file_.lastModifiedDate = data.lastModifiedDate || null; 354 } 355 356 // Add size of data written to writer.position. 357 position_ += data.size; 358 359 if (this.onwriteend) { 360 this.onwriteend(); 361 } 362 }.bind(this), this.onerror); 363 }.bind(this); 364 365 if (IDB_SUPPORTS_BLOB) { 366 writeFile(blob_); 367 } else { 368 BlobToBase64(blob_, writeFile); 369 } 370 }; 371} 372 373 374/** 375 * Interface for listing a directory's contents (files and folders). 376 * 377 * Modeled from: 378 * dev.w3.org/2009/dap/file-system/pub/FileSystem/#idl-def-DirectoryReader 379 * 380 * @constructor 381 */ 382function DirectoryReader(dirEntry) { 383 var dirEntry_ = dirEntry; 384 var used_ = false; 385 386 this.readEntries = function(successCallback, opt_errorCallback) { 387 if (!successCallback) { 388 throw Error('Expected successCallback argument.'); 389 } 390 391 // This is necessary to mimic the way DirectoryReader.readEntries() should 392 // normally behavior. According to spec, readEntries() needs to be called 393 // until the length of result array is 0. To handle someone implementing 394 // a recursive call to readEntries(), get everything from indexedDB on the 395 // first shot. Then (DirectoryReader has been used), return an empty 396 // result array. 397 if (!used_) { 398 idb_.getAllEntries(dirEntry_.fullPath, function(entries) { 399 used_= true; 400 successCallback(entries); 401 }, opt_errorCallback); 402 } else { 403 successCallback([]); 404 } 405 }; 406}; 407 408/** 409 * Interface supplies information about the state of a file or directory. 410 * 411 * Modeled from: 412 * dev.w3.org/2009/dap/file-system/file-dir-sys.html#idl-def-Metadata 413 * 414 * @constructor 415 */ 416function Metadata(modificationTime, size) { 417 this.modificationTime_ = modificationTime || null; 418 this.size_ = size || 0; 419} 420 421Metadata.prototype = { 422 get modificationTime() { 423 return this.modificationTime_; 424 }, 425 get size() { 426 return this.size_; 427 } 428} 429 430/** 431 * Interface representing entries in a filesystem, each of which may be a File 432 * or DirectoryEntry. 433 * 434 * Modeled from: 435 * dev.w3.org/2009/dap/file-system/pub/FileSystem/#idl-def-Entry 436 * 437 * @constructor 438 */ 439function Entry() {} 440 441Entry.prototype = { 442 name: null, 443 fullPath: null, 444 filesystem: null, 445 copyTo: function() { 446 throw NOT_IMPLEMENTED_ERR; 447 }, 448 getMetadata: function(successCallback, opt_errorCallback) { 449 if (!successCallback) { 450 throw Error('Expected successCallback argument.'); 451 } 452 453 try { 454 if (this.isFile) { 455 successCallback( 456 new Metadata(this.file_.lastModifiedDate, this.file_.size)); 457 } else { 458 opt_errorCallback(new MyFileError({code: 1001, 459 name: 'getMetadata() not implemented for DirectoryEntry'})); 460 } 461 } catch(e) { 462 opt_errorCallback && opt_errorCallback(e); 463 } 464 }, 465 getParent: function() { 466 throw NOT_IMPLEMENTED_ERR; 467 }, 468 moveTo: function() { 469 throw NOT_IMPLEMENTED_ERR; 470 }, 471 remove: function(successCallback, opt_errorCallback) { 472 if (!successCallback) { 473 throw Error('Expected successCallback argument.'); 474 } 475 // TODO: This doesn't protect against directories that have content in it. 476 // Should throw an error instead if the dirEntry is not empty. 477 idb_['delete'](this.fullPath, function() { 478 successCallback(); 479 }, opt_errorCallback); 480 }, 481 toURL: function() { 482 var origin = location.protocol + '//' + location.host; 483 return 'filesystem:' + origin + DIR_SEPARATOR + storageType_.toLowerCase() + 484 this.fullPath; 485 }, 486}; 487 488/** 489 * Interface representing a file in the filesystem. 490 * 491 * Modeled from: 492 * dev.w3.org/2009/dap/file-system/pub/FileSystem/#the-fileentry-interface 493 * 494 * @param {FileEntry} opt_fileEntry Optional FileEntry to initialize this 495 * object from. 496 * @constructor 497 * @extends {Entry} 498 */ 499function FileEntry(opt_fileEntry) { 500 this.file_ = null; 501 502 Object.defineProperty(this, 'isFile', { 503 enumerable: true, 504 get: function() { 505 return true; 506 } 507 }); 508 Object.defineProperty(this, 'isDirectory', { 509 enumerable: true, 510 get: function() { 511 return false;
512 } 513 }); 514 515 // Create this entry from properties from an existing FileEntry. 516 if (opt_fileEntry) { 517 this.file_ = opt_fileEntry.file_; 518 this.name = opt_fileEntry.name; 519 this.fullPath = opt_fileEntry.fullPath; 520 this.filesystem = opt_fileEntry.filesystem; 521 if (typeof(this.file_.blob_) === "string") { 522 this.file_.blob_ = Base64ToBlob(this.file_.blob_); 523 } 524 } 525} 526FileEntry.prototype = new Entry(); 527FileEntry.prototype.constructor = FileEntry; 528FileEntry.prototype.createWriter = function(callback) { 529 // TODO: figure out if there's a way to dispatch onwrite event as we're writing 530 // data to IDB. Right now, we're only calling onwritend/onerror 531 // FileEntry.write(). 532 callback(new FileWriter(this)); 533}; 534FileEntry.prototype.file = function(successCallback, opt_errorCallback) { 535 if (!successCallback) { 536 throw Error('Expected successCallback argument.'); 537 } 538 539 if (this.file_ == null) { 540 if (opt_errorCallback) { 541 opt_errorCallback(NOT_FOUND_ERR); 542 } else { 543 throw NOT_FOUND_ERR; 544 } 545 return; 546 } 547 548 // If we're returning a zero-length (empty) file, return the fake file obj. 549 // Otherwise, return the native File object that we've stashed. 550 var file = this.file_.blob_ == null ? this.file_ : this.file_.blob_; 551 file.lastModifiedDate = this.file_.lastModifiedDate; 552 553 // Add Blob.slice() to this wrapped object. Currently won't work :( 554 /*if (!val.slice) { 555 val.slice = Blob.prototype.slice; // Hack to add back in .slice(). 556 }*/ 557 successCallback(file); 558}; 559 560/** 561 * Interface representing a directory in the filesystem. 562 * 563 * Modeled from: 564 * dev.w3.org/2009/dap/file-system/pub/FileSystem/#the-directoryentry-interface 565 * 566 * @param {DirectoryEntry} opt_folderEntry Optional DirectoryEntry to 567 * initialize this object from. 568 * @constructor 569 * @extends {Entry} 570 */ 571function DirectoryEntry(opt_folderEntry) { 572 Object.defineProperty(this, 'isFile', { 573 enumerable: true, 574 get: function() { 575 return false; 576 } 577 }); 578 Object.defineProperty(this, 'isDirectory', { 579 enumerable: true, 580 get: function() { 581 return true; 582 } 583 }); 584 585 // Create this entry from properties from an existing DirectoryEntry. 586 if (opt_folderEntry) { 587 this.name = opt_folderEntry.name; 588 this.fullPath = opt_folderEntry.fullPath; 589 this.filesystem = opt_folderEntry.filesystem; 590 } 591} 592DirectoryEntry.prototype = new Entry(); 593DirectoryEntry.prototype.constructor = DirectoryEntry; 594DirectoryEntry.prototype.createReader = function() { 595 return new DirectoryReader(this); 596}; 597DirectoryEntry.prototype.getDirectory = function(path, options, successCallback, 598 opt_errorCallback) { 599 600 // Create an absolute path if we were handed a relative one. 601 path = resolveToFullPath_(this.fullPath, path); 602 603 idb_.get(path, function(folderEntry) { 604 if (!options) { 605 options = {}; 606 } 607 608 if (options.create === true && options.exclusive === true && folderEntry) { 609 // If create and exclusive are both true, and the path already exists, 610 // getDirectory must fail. 611 if (opt_errorCallback) { 612 opt_errorCallback(INVALID_MODIFICATION_ERR); 613 return; 614 } 615 } else if (options.create === true && !folderEntry) { 616 // If create is true, the path doesn't exist, and no other error occurs, 617 // getDirectory must create it as a zero-length file and return a corresponding 618 // DirectoryEntry. 619 var dirEntry = new DirectoryEntry(); 620 dirEntry.name = path.split(DIR_SEPARATOR).pop(); // Just need filename. 621 dirEntry.fullPath = path; 622 dirEntry.filesystem = fs_; 623 624 idb_.put(dirEntry, successCallback, opt_errorCallback); 625 } else if (options.create === true && folderEntry) { 626 627 if (folderEntry.isDirectory) { 628 // IDB won't save methods, so we need re-create the DirectoryEntry. 629 successCallback(new DirectoryEntry(folderEntry)); 630 } else { 631 if (opt_errorCallback) { 632 opt_errorCallback(INVALID_MODIFICATION_ERR); 633 return; 634 } 635 } 636 } else if ((!options.create || options.create === false) && !folderEntry) { 637 // Handle root special. It should always exist. 638 if (path == DIR_SEPARATOR) { 639 folderEntry = new DirectoryEntry(); 640 folderEntry.name = ''; 641 folderEntry.fullPath = DIR_SEPARATOR; 642 folderEntry.filesystem = fs_; 643 successCallback(folderEntry); 644 return; 645 } 646 647 // If create is not true and the path doesn't exist, getDirectory must fail. 648 if (opt_errorCallback) { 649 opt_errorCallback(NOT_FOUND_ERR); 650 return; 651 } 652 } else if ((!options.create || options.create === false) && folderEntry && 653 folderEntry.isFile) { 654 // If create is not true and the path exists, but is a file, getDirectory 655 // must fail. 656 if (opt_errorCallback) { 657 opt_errorCallback(INVALID_MODIFICATION_ERR); 658 return; 659 } 660 } else { 661 // Otherwise, if no other error occurs, getDirectory must return a 662 // DirectoryEntry corresponding to path. 663 664 // IDB won't' save methods, so we need re-create DirectoryEntry. 665 successCallback(new DirectoryEntry(folderEntry)); 666 } 667 }, opt_errorCallback); 668}; 669 670DirectoryEntry.prototype.getFile = function(path, options, successCallback, 671 opt_errorCallback) { 672 673 // Create an absolute path if we were handed a relative one. 674 path = resolveToFullPath_(this.fullPath, path); 675 676 idb_.get(path, function(fileEntry) { 677 if (!options) { 678 options = {}; 679 } 680 681 if (options.create === true && options.exclusive === true && fileEntry) { 682 // If create and exclusive are both true, and the path already exists, 683 // getFile must fail. 684 685 if (opt_errorCallback) { 686 opt_errorCallback(INVALID_MODIFICATION_ERR); 687 return; 688 } 689 } else if (options.create === true && !fileEntry) { 690 // If create is true, the path doesn't exist, and no other error occurs, 691 // getFile must create it as a zero-length file and return a corresponding 692 // FileEntry. 693 var fileEntry = new FileEntry(); 694 fileEntry.name = path.split(DIR_SEPARATOR).pop(); // Just need filename. 695 fileEntry.fullPath = path; 696 fileEntry.filesystem = fs_; 697 fileEntry.file_ = new MyFile({size: 0, name: fileEntry.name, 698 lastModifiedDate: new Date()}); 699 700 idb_.put(fileEntry, successCallback, opt_errorCallback); 701 702 } else if (options.create === true && fileEntry) { 703 if (fileEntry.isFile) { 704 // IDB won't save methods, so we need re-create the FileEntry. 705 successCallback(new FileEntry(fileEntry)); 706 } else { 707 if (opt_errorCallback) { 708 opt_errorCallback(INVALID_MODIFICATION_ERR); 709 return; 710 } 711 } 712 } else if ((!options.create || options.create === false) && !fileEntry) { 713 // If create is not true and the path doesn't exist, getFile must fail. 714 if (opt_errorCallback) { 715 opt_errorCallback(NOT_FOUND_ERR); 716 return; 717 } 718 } else if ((!options.create || options.create === false) && fileEntry && 719 fileEntry.isDirectory) { 720 // If create is not true and the path exists, but is a directory, getFile 721 // must fail. 722 if (opt_errorCallback) { 723 opt_errorCallback(INVALID_MODIFICATION_ERR); 724 return; 725 } 726 } else { 727 // Otherwise, if no other error occurs, getFile must return a FileEntry 728 // corresponding to path. 729 730 // IDB won't' save methods, so we need re-create the FileEntry. 731 successCallback(new FileEntry(fileEntry)); 732 } 733 }, opt_errorCallback); 734}; 735 736DirectoryEntry.prototype.removeRecursively = function(successCallback, 737 opt_errorCallback) { 738 if (!successCallback) { 739 throw Error('Expected successCallback argument.'); 740 } 741 742 this.remove(successCallback, opt_errorCallback); 743}; 744 745/** 746 * Interface representing a filesystem. 747 * 748 * Modeled from: 749 * dev.w3.org/2009/dap/file-system/pub/FileSystem/#idl-def-LocalFileSystem 750 * 751 * @param {number} type Kind of storage to use, either TEMPORARY or PERSISTENT. 752 * @param {number} size Storage space (bytes) the application expects to need. 753 * @constructor 754 */ 755function DOMFileSystem(type, size) { 756 storageType_ = type == exports.TEMPORARY ? 'Temporary' : 'Persistent'; 757 this.name = (location.protocol + location.host).replace(/:/g, '_') + 758 ':' + storageType_; 759 this.root = new DirectoryEntry(); 760 this.root.fullPath = DIR_SEPARATOR; 761 this.root.filesystem = this; 762 this.root.name = ''; 763} 764 765function requestFileSystem(type, size, successCallback, opt_errorCallback) { 766 if (type != exports.TEMPORARY && type != exports.PERSISTENT) { 767 if (opt_errorCallback) { 768 opt_errorCallback(INVALID_MODIFICATION_ERR); 769 return; 770 } 771 } 772 773 fs_ = new DOMFileSystem(type, size); 774 idb_.open(fs_.name, function(e) { 775 successCallback(fs_); 776 }, opt_errorCallback); 777} 778 779function resolveLocalFileSystemURL(url, successCallback, opt_errorCallback) { 780 var origin = location.protocol + '//' + location.host; 781 var base = 'filesystem:' + origin + DIR_SEPARATOR + storageType_.toLowerCase(); 782 url = url.replace(base, ''); 783 if (url.substr(-1) === '/') { 784 url = url.slice(0, -1); 785 } 786 if (url) { 787 idb_.get(url, function(entry) { 788 if (entry) { 789 if (entry.isFile) { 790 return successCallback(new FileEntry(entry)); 791 } else if (entry.isDirectory) { 792 return successCallback(new DirectoryEntry(entry)); 793 } 794 } else { 795 opt_errorCallback && opt_errorCallback(NOT_FOUND_ERR); 796 } 797 }, opt_errorCallback); 798 } else { 799 successCallback(fs_.root); 800 } 801} 802 803// Core logic to handle IDB operations ========================================= 804 805idb_.open = function(dbName, successCallback, opt_errorCallback) { 806 var self = this; 807 808 // TODO: FF 12.0a1 isn't liking a db name with : in it. 809 var request = indexedDB.open(dbName.replace(':', '_')/*, 1 /*version*/); 810 811 request.onerror = opt_errorCallback || onError; 812
813 request.onupgradeneeded = function(e) { 814 // First open was called or higher db version was used. 815 816 // console.log('onupgradeneeded: oldVersion:' + e.oldVersion, 817 // 'newVersion:' + e.newVersion); 818 819 self.db = e.target.result; 820 self.db.onerror = onError; 821 822 if (!self.db.objectStoreNames.contains(FILE_STORE_)) { 823 var store = self.db.createObjectStore(FILE_STORE_/*,{keyPath: 'id', autoIncrement: true}*/); 824 } 825 }; 826 827 request.onsuccess = function(e) { 828 self.db = e.target.result; 829 self.db.onerror = onError; 830 successCallback(e); 831 }; 832 833 request.onblocked = opt_errorCallback || onError; 834}; 835 836idb_.close = function() { 837 this.db.close(); 838 this.db = null; 839}; 840 841// TODO: figure out if we should ever call this method. The filesystem API 842// doesn't allow you to delete a filesystem once it is 'created'. Users should 843// use the public remove/removeRecursively API instead. 844idb_.drop = function(successCallback, opt_errorCallback) { 845 if (!this.db) { 846 return; 847 } 848 849 var dbName = this.db.name; 850 851 var request = indexedDB.deleteDatabase(dbName); 852 request.onsuccess = function(e) { 853 successCallback(e); 854 }; 855 request.onerror = opt_errorCallback || onError; 856 857 idb_.close(); 858}; 859 860idb_.get = function(fullPath, successCallback, opt_errorCallback) { 861 if (!this.db) { 862 return; 863 } 864 865 var tx = this.db.transaction([FILE_STORE_], 'readonly'); 866 867 //var request = tx.objectStore(FILE_STORE_).get(fullPath); 868 var range = IDBKeyRange.bound(fullPath, fullPath + DIR_OPEN_BOUND, 869 false, true); 870 var request = tx.objectStore(FILE_STORE_).get(range); 871 872 tx.onabort = opt_errorCallback || onError; 873 tx.oncomplete = function(e) { 874 successCallback(request.result); 875 }; 876}; 877 878idb_.getAllEntries = function(fullPath, successCallback, opt_errorCallback) { 879 if (!this.db) { 880 return; 881 } 882 883 var results = []; 884 885 //var range = IDBKeyRange.lowerBound(fullPath, true); 886 //var range = IDBKeyRange.upperBound(fullPath, true); 887 888 // Treat the root entry special. Querying it returns all entries because 889 // they match '/'. 890 var range = null; 891 if (fullPath != DIR_SEPARATOR) { 892 //console.log(fullPath + '/', fullPath + DIR_OPEN_BOUND) 893 range = IDBKeyRange.bound( 894 fullPath + DIR_SEPARATOR, fullPath + DIR_OPEN_BOUND, false, true); 895 } 896 897 var tx = this.db.transaction([FILE_STORE_], 'readonly'); 898 tx.onabort = opt_errorCallback || onError; 899 tx.oncomplete = function(e) {
900 // TODO: figure out how to do be range queries instead of filtering result 901 // in memory :( 902 results = results.filter(function(val) { 903 var valPartsLen = val.fullPath.split(DIR_SEPARATOR).length; 904 var fullPathPartsLen = fullPath.split(DIR_SEPARATOR).length; 905 906 if (fullPath == DIR_SEPARATOR && valPartsLen < fullPathPartsLen + 1) { 907 // Hack to filter out entries in the root folder. This is inefficient 908 // because reading the entires of fs.root (e.g. '/') returns ALL 909 // results in the database, then filters out the entries not in '/'. 910 return val; 911 } else if (fullPath != DIR_SEPARATOR && 912 valPartsLen == fullPathPartsLen + 1) { 913 // If this a subfolder and entry is a direct child, include it in 914 // the results. Otherwise, it's not an entry of this folder. 915 return val; 916 } 917 }); 918 919 successCallback(results); 920 }; 921 922 var request = tx.objectStore(FILE_STORE_).openCursor(range); 923 924 request.onsuccess = function(e) { 925 var cursor = e.target.result; 926 if (cursor) { 927 var val = cursor.value; 928 929 results.push(val.isFile ? new FileEntry(val) : new DirectoryEntry(val)); 930 cursor['continue'](); 931 } 932 }; 933}; 934 935idb_['delete'] = function(fullPath, successCallback, opt_errorCallback) { 936 if (!this.db) { 937 return; 938 } 939 940 var tx = this.db.transaction([FILE_STORE_], 'readwrite'); 941 tx.oncomplete = successCallback; 942 tx.onabort = opt_errorCallback || onError; 943 944 //var request = tx.objectStore(FILE_STORE_).delete(fullPath); 945 var range = IDBKeyRange.bound( 946 fullPath, fullPath + DIR_OPEN_BOUND, false, true); 947 var request = tx.objectStore(FILE_STORE_)['delete'](range); 948}; 949 950idb_.put = function(entry, successCallback, opt_errorCallback) { 951 if (!this.db) { 952 return; 953 } 954 955 var tx = this.db.transaction([FILE_STORE_], 'readwrite'); 956 tx.onabort = opt_errorCallback || onError; 957 tx.oncomplete = function(e) { 958 // TODO: Error is thrown if we pass the request event back instead. 959 successCallback(entry); 960 }; 961 962 var request = tx.objectStore(FILE_STORE_).put(entry, entry.fullPath); 963}; 964 965// Global error handler. Errors bubble from request, to transaction, to db. 966function onError(e) { 967 switch (e.target.errorCode) { 968 case 12: 969 console.log('Error - Attempt to open db with a lower version than the ' + 970 'current one.'); 971 break; 972 default: 973 console.log('errorCode: ' + e.target.errorCode); 974 } 975 976 console.log(e, e.code, e.message); 977} 978 979// Clean up. 980// TODO: decide if this is the best place for this. 981exports.addEventListener('beforeunload', function(e) { 982 idb_.db && idb_.db.close(); 983}, false); 984 985//exports.idb = idb_; 986exports.requestFileSystem = requestFileSystem; 987exports.resolveLocalFileSystemURL = resolveLocalFileSystemURL; 988 989// Export more stuff (to window) for unit tests to do their thing. 990if (exports === window && exports.RUNNING_TESTS) { 991 exports['Entry'] = Entry; 992 exports['FileEntry'] = FileEntry; 993 exports['DirectoryEntry'] = DirectoryEntry; 994 exports['resolveToFullPath_'] = resolveToFullPath_; 995 exports['Metadata'] = Metadata; 996 exports['Base64ToBlob'] = Base64ToBlob; 997} 998 999})(self); // Don't use window because we want to run in workers.
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.