/* * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. * */ (function () { /* global require, exports, module */ /* global FILESYSTEM_PREFIX */ /* global IDBKeyRange */ /* global FileReader */ /* global atob, btoa, Blob */ /* Heavily based on https://github.com/ebidel/idb.filesystem.js */ // For chrome we don't need to implement proxy methods // All functionality can be accessed natively. if (require('./isChrome')()) { var pathsPrefix = { // Read-only directory where the application is installed. applicationDirectory: location.origin + '/', // eslint-disable-line no-undef // Where to put app-specific data files. dataDirectory: 'filesystem:file:///persistent/', // Cached files that should survive app restarts. // Apps should not rely on the OS to delete files in here. cacheDirectory: 'filesystem:file:///temporary/' }; exports.requestAllPaths = function (successCallback) { successCallback(pathsPrefix); }; require('cordova/exec/proxy').add('File', module.exports); return; } var LocalFileSystem = require('./LocalFileSystem'); var FileSystem = require('./FileSystem'); var FileEntry = require('./FileEntry'); var FileError = require('./FileError'); var DirectoryEntry = require('./DirectoryEntry'); var File = require('./File'); (function (exports, global) { var indexedDB = global.indexedDB || global.mozIndexedDB; if (!indexedDB) { throw 'Firefox OS File plugin: indexedDB not supported'; } var fs_ = null; var idb_ = {}; idb_.db = null; var FILE_STORE_ = 'entries'; var DIR_SEPARATOR = '/'; var pathsPrefix = { // Read-only directory where the application is installed. applicationDirectory: location.origin + '/', // eslint-disable-line no-undef // Where to put app-specific data files. dataDirectory: 'file:///persistent/', // Cached files that should survive app restarts. // Apps should not rely on the OS to delete files in here. cacheDirectory: 'file:///temporary/' }; var unicodeLastChar = 65535; /** * Exported functionality ***/ exports.requestFileSystem = function (successCallback, errorCallback, args) { var type = args[0]; // Size is ignored since IDB filesystem size depends // on browser implementation and can't be set up by user var size = args[1]; // eslint-disable-line no-unused-vars if (type !== LocalFileSystem.TEMPORARY && type !== LocalFileSystem.PERSISTENT) { if (errorCallback) { errorCallback(FileError.INVALID_MODIFICATION_ERR); } return; } var name = type === LocalFileSystem.TEMPORARY ? 'temporary' : 'persistent'; var storageName = (location.protocol + location.host).replace(/:/g, '_'); // eslint-disable-line no-undef var root = new DirectoryEntry('', DIR_SEPARATOR); fs_ = new FileSystem(name, root); idb_.open(storageName, function () { successCallback(fs_); }, errorCallback); }; // Overridden by Android, BlackBerry 10 and iOS to populate fsMap require('./fileSystems').getFs = function (name, callback) { callback(new FileSystem(name, fs_.root)); }; // list a directory's contents (files and folders). exports.readEntries = function (successCallback, errorCallback, args) { var fullPath = args[0]; if (typeof successCallback !== 'function') { throw Error('Expected successCallback argument.'); } var path = resolveToFullPath_(fullPath); exports.getDirectory(function () { idb_.getAllEntries(path.fullPath + DIR_SEPARATOR, path.storagePath, function (entries) { successCallback(entries); }, errorCallback); }, function () { if (errorCallback) { errorCallback(FileError.NOT_FOUND_ERR); } }, [path.storagePath, path.fullPath, {create: false}]); }; exports.getFile = function (successCallback, errorCallback, args) { var fullPath = args[0]; var path = args[1]; var options = args[2] || {}; // Create an absolute path if we were handed a relative one. path = resolveToFullPath_(fullPath, path); idb_.get(path.storagePath, function (fileEntry) { if (options.create === true && options.exclusive === true && fileEntry) { // If create and exclusive are both true, and the path already exists, // getFile must fail. if (errorCallback) { errorCallback(FileError.PATH_EXISTS_ERR); } } 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 newFileEntry = new FileEntry(path.fileName, path.fullPath, new FileSystem(path.fsName, fs_.root)); newFileEntry.file_ = new MyFile({ size: 0, name: newFileEntry.name, lastModifiedDate: new Date(), storagePath: path.storagePath }); idb_.put(newFileEntry, path.storagePath, successCallback, errorCallback); } else if (options.create === true && fileEntry) { if (fileEntry.isFile) { // Overwrite file, delete then create new. idb_['delete'](path.storagePath, function () { var newFileEntry = new FileEntry(path.fileName, path.fullPath, new FileSystem(path.fsName, fs_.root)); newFileEntry.file_ = new MyFile({ size: 0, name: newFileEntry.name, lastModifiedDate: new Date(), storagePath: path.storagePath }); idb_.put(newFileEntry, path.storagePath, successCallback, errorCallback); }, errorCallback); } else { if (errorCallback) { errorCallback(FileError.INVALID_MODIFICATION_ERR); } } } else if ((!options.create || options.create === false) && !fileEntry) { // If create is not true and the path doesn't exist, getFile must fail. if (errorCallback) { errorCallback(FileError.NOT_FOUND_ERR); } } 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 (errorCallback) { errorCallback(FileError.TYPE_MISMATCH_ERR); } } else { // Otherwise, if no other error occurs, getFile must return a FileEntry // corresponding to path. successCallback(fileEntryFromIdbEntry(fileEntry)); } }, errorCallback); }; exports.getFileMetadata = function (successCallback, errorCallback, args) { var fullPath = args[0]; exports.getFile(function (fileEntry) { successCallback(new File(fileEntry.file_.name, fileEntry.fullPath, '', fileEntry.file_.lastModifiedDate, fileEntry.file_.size)); }, errorCallback, [fullPath, null]); }; exports.getMetadata = function (successCallback, errorCallback, args) { exports.getFile(function (fileEntry) { successCallback( { modificationTime: fileEntry.file_.lastModifiedDate, size: fileEntry.file_.lastModifiedDate }); }, errorCallback, args); }; exports.setMetadata = function (successCallback, errorCallback, args) { var fullPath = args[0]; var metadataObject = args[1]; exports.getFile(function (fileEntry) { fileEntry.file_.lastModifiedDate = metadataObject.modificationTime; idb_.put(fileEntry, fileEntry.file_.storagePath, successCallback, errorCallback); }, errorCallback, [fullPath, null]); }; exports.write = function (successCallback, errorCallback, args) { var fileName = args[0]; var data = args[1]; var position = args[2]; var isBinary = args[3]; // eslint-disable-line no-unused-vars if (!data) { if (errorCallback) { errorCallback(FileError.INVALID_MODIFICATION_ERR); } return; } if (typeof data === 'string' || data instanceof String) { data = new Blob([data]); // eslint-disable-line no-undef } exports.getFile(function (fileEntry) { var blob_ = fileEntry.file_.blob_; if (!blob_) { blob_ = new Blob([data], {type: data.type}); // eslint-disable-line no-undef } else { // Calc the head and tail fragments var head = blob_.slice(0, position); var tail = blob_.slice(position + (data.size || data.byteLength)); // Calc the padding var padding = position - head.size; if (padding < 0) { padding = 0; } // Do the "write". In fact, a full overwrite of the Blob. blob_ = new Blob([head, new Uint8Array(padding), data, tail], // eslint-disable-line no-undef {type: data.type}); } // Set the blob we're writing on this file entry so we can recall it later. fileEntry.file_.blob_ = blob_; fileEntry.file_.lastModifiedDate = new Date() || null; fileEntry.file_.size = blob_.size; fileEntry.file_.name = blob_.name; fileEntry.file_.type = blob_.type; idb_.put(fileEntry, fileEntry.file_.storagePath, function () { successCallback(data.size || data.byteLength); }, errorCallback); }, errorCallback, [fileName, null]); }; exports.readAsText = function (successCallback, errorCallback, args) { var fileName = args[0]; var enc = args[1]; var startPos = args[2]; var endPos = args[3]; readAs('text', fileName, enc, startPos, endPos, successCallback, errorCallback); }; exports.readAsDataURL = function (successCallback, errorCallback, args) { var fileName = args[0]; var startPos = args[1]; var endPos = args[2]; readAs('dataURL', fileName, null, startPos, endPos, successCallback, errorCallback); }; exports.readAsBinaryString = function (successCallback, errorCallback, args) { var fileName = args[0]; var startPos = args[1]; var endPos = args[2]; readAs('binaryString', fileName, null, startPos, endPos, successCallback, errorCallback); }; exports.readAsArrayBuffer = function (successCallback, errorCallback, args) { var fileName = args[0]; var startPos = args[1]; var endPos = args[2]; readAs('arrayBuffer', fileName, null, startPos, endPos, successCallback, errorCallback); }; exports.removeRecursively = exports.remove = function (successCallback, errorCallback, args) { if (typeof successCallback !== 'function') { throw Error('Expected successCallback argument.'); } var fullPath = resolveToFullPath_(args[0]).storagePath; if (fullPath === pathsPrefix.cacheDirectory || fullPath === pathsPrefix.dataDirectory) { errorCallback(FileError.NO_MODIFICATION_ALLOWED_ERR); return; } function deleteEntry (isDirectory) { // 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'](fullPath, function () { successCallback(); }, function () { if (errorCallback) { errorCallback(); } }, isDirectory); } // We need to to understand what we are deleting: exports.getDirectory(function (entry) { deleteEntry(entry.isDirectory); }, function () { // DirectoryEntry was already deleted or entry is FileEntry deleteEntry(false); }, [fullPath, null, {create: false}]); }; exports.getDirectory = function (successCallback, errorCallback, args) { var fullPath = args[0]; var path = args[1]; var options = args[2]; // Create an absolute path if we were handed a relative one. path = resolveToFullPath_(fullPath, path); idb_.get(path.storagePath, 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 (errorCallback) { errorCallback(FileError.PATH_EXISTS_ERR); } // There is a strange bug in mobilespec + FF, which results in coming to multiple else-if's // so we are shielding from it with returns. return; } 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 // MyDirectoryEntry. var dirEntry = new DirectoryEntry(path.fileName, path.fullPath, new FileSystem(path.fsName, fs_.root)); idb_.put(dirEntry, path.storagePath, successCallback, errorCallback); return; } if (options.create === true && folderEntry) { if (folderEntry.isDirectory) { // IDB won't save methods, so we need re-create the MyDirectoryEntry. successCallback(new DirectoryEntry(folderEntry.name, folderEntry.fullPath, folderEntry.filesystem)); } else { if (errorCallback) { errorCallback(FileError.INVALID_MODIFICATION_ERR); } } return; } if ((!options.create || options.create === false) && !folderEntry) { // Handle root special. It should always exist. if (path.fullPath === DIR_SEPARATOR) { successCallback(fs_.root); return; } // If create is not true and the path doesn't exist, getDirectory must fail. if (errorCallback) { errorCallback(FileError.NOT_FOUND_ERR); } return; } 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 (errorCallback) { errorCallback(FileError.TYPE_MISMATCH_ERR); } return; } // Otherwise, if no other error occurs, getDirectory must return a // MyDirectoryEntry corresponding to path. // IDB won't' save methods, so we need re-create MyDirectoryEntry. successCallback(new DirectoryEntry(folderEntry.name, folderEntry.fullPath, folderEntry.filesystem)); }, errorCallback); }; exports.getParent = function (successCallback, errorCallback, args) { if (typeof successCallback !== 'function') { throw Error('Expected successCallback argument.'); } var fullPath = args[0]; // fullPath is like this: // file:///persistent/path/to/file or // file:///persistent/path/to/directory/ if (fullPath === DIR_SEPARATOR || fullPath === pathsPrefix.cacheDirectory || fullPath === pathsPrefix.dataDirectory) { successCallback(fs_.root); return; } // To delete all slashes at the end while (fullPath[fullPath.length - 1] === '/') { fullPath = fullPath.substr(0, fullPath.length - 1); } var pathArr = fullPath.split(DIR_SEPARATOR); pathArr.pop(); var parentName = pathArr.pop(); var path = pathArr.join(DIR_SEPARATOR) + DIR_SEPARATOR; // To get parent of root files var joined = path + parentName + DIR_SEPARATOR;// is like this: file:///persistent/ if (joined === pathsPrefix.cacheDirectory || joined === pathsPrefix.dataDirectory) { exports.getDirectory(successCallback, errorCallback, [joined, DIR_SEPARATOR, {create: false}]); return; } exports.getDirectory(successCallback, errorCallback, [path, parentName, {create: false}]); }; exports.copyTo = function (successCallback, errorCallback, args) { var srcPath = args[0]; var parentFullPath = args[1]; var name = args[2]; if (name.indexOf('/') !== -1 || srcPath === parentFullPath + name) { if (errorCallback) { errorCallback(FileError.INVALID_MODIFICATION_ERR); } return; } // Read src file exports.getFile(function (srcFileEntry) { var path = resolveToFullPath_(parentFullPath); // Check directory exports.getDirectory(function () { // Create dest file exports.getFile(function (dstFileEntry) { exports.write(function () { successCallback(dstFileEntry); }, errorCallback, [dstFileEntry.file_.storagePath, srcFileEntry.file_.blob_, 0]); }, errorCallback, [parentFullPath, name, {create: true}]); }, function () { if (errorCallback) { errorCallback(FileError.NOT_FOUND_ERR); } }, [path.storagePath, null, {create: false}]); }, errorCallback, [srcPath, null]); }; exports.moveTo = function (successCallback, errorCallback, args) { var srcPath = args[0]; // parentFullPath and name parameters is ignored because // args is being passed downstream to exports.copyTo method var parentFullPath = args[1]; // eslint-disable-line var name = args[2]; // eslint-disable-line exports.copyTo(function (fileEntry) { exports.remove(function () { successCallback(fileEntry); }, errorCallback, [srcPath]); }, errorCallback, args); }; exports.resolveLocalFileSystemURI = function (successCallback, errorCallback, args) { var path = args[0]; // Ignore parameters if (path.indexOf('?') !== -1) { path = String(path).split('?')[0]; } // support for encodeURI if (/\%5/g.test(path) || /\%20/g.test(path)) { // eslint-disable-line no-useless-escape path = decodeURI(path); } if (path.trim()[0] === '/') { if (errorCallback) { errorCallback(FileError.ENCODING_ERR); } return; } // support for cdvfile if (path.trim().substr(0, 7) === 'cdvfile') { if (path.indexOf('cdvfile://localhost') === -1) { if (errorCallback) { errorCallback(FileError.ENCODING_ERR); } return; } var indexPersistent = path.indexOf('persistent'); var indexTemporary = path.indexOf('temporary'); // cdvfile://localhost/persistent/path/to/file if (indexPersistent !== -1) { path = 'file:///persistent' + path.substr(indexPersistent + 10); } else if (indexTemporary !== -1) { path = 'file:///temporary' + path.substr(indexTemporary + 9); } else { if (errorCallback) { errorCallback(FileError.ENCODING_ERR); } return; } } // to avoid path form of '///path/to/file' function handlePathSlashes (path) { var cutIndex = 0; for (var i = 0; i < path.length - 1; i++) { if (path[i] === DIR_SEPARATOR && path[i + 1] === DIR_SEPARATOR) { cutIndex = i + 1; } else break; } return path.substr(cutIndex); } // Handle localhost containing paths (see specs ) if (path.indexOf('file://localhost/') === 0) { path = path.replace('file://localhost/', 'file:///'); } if (path.indexOf(pathsPrefix.dataDirectory) === 0) { path = path.substring(pathsPrefix.dataDirectory.length - 1); path = handlePathSlashes(path); exports.requestFileSystem(function () { exports.getFile(successCallback, function () { exports.getDirectory(successCallback, errorCallback, [pathsPrefix.dataDirectory, path, {create: false}]); }, [pathsPrefix.dataDirectory, path, {create: false}]); }, errorCallback, [LocalFileSystem.PERSISTENT]); } else if (path.indexOf(pathsPrefix.cacheDirectory) === 0) { path = path.substring(pathsPrefix.cacheDirectory.length - 1); path = handlePathSlashes(path); exports.requestFileSystem(function () { exports.getFile(successCallback, function () { exports.getDirectory(successCallback, errorCallback, [pathsPrefix.cacheDirectory, path, {create: false}]); }, [pathsPrefix.cacheDirectory, path, {create: false}]); }, errorCallback, [LocalFileSystem.TEMPORARY]); } else if (path.indexOf(pathsPrefix.applicationDirectory) === 0) { path = path.substring(pathsPrefix.applicationDirectory.length); // TODO: need to cut out redundant slashes? var xhr = new XMLHttpRequest(); // eslint-disable-line no-undef xhr.open('GET', path, true); xhr.onreadystatechange = function () { if (xhr.status === 200 && xhr.readyState === 4) { exports.requestFileSystem(function (fs) { fs.name = location.hostname; // eslint-disable-line no-undef // TODO: need to call exports.getFile(...) to handle errors correct fs.root.getFile(path, {create: true}, writeFile, errorCallback); }, errorCallback, [LocalFileSystem.PERSISTENT]); } }; xhr.onerror = function () { if (errorCallback) { errorCallback(FileError.NOT_READABLE_ERR); } }; xhr.send(); } else { if (errorCallback) { errorCallback(FileError.NOT_FOUND_ERR); } } function writeFile (entry) { entry.createWriter(function (fileWriter) { fileWriter.onwriteend = function (evt) { if (!evt.target.error) { entry.filesystemName = location.hostname; // eslint-disable-line no-undef successCallback(entry); } }; fileWriter.onerror = function () { if (errorCallback) { errorCallback(FileError.NOT_READABLE_ERR); } }; fileWriter.write(new Blob([xhr.response])); // eslint-disable-line no-undef }, errorCallback); // eslint-disable-line no-undef } }; exports.requestAllPaths = function (successCallback) { successCallback(pathsPrefix); }; /** * Helpers ***/ /** * 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_ = new Blob(); // eslint-disable-line no-undef this.size = opts.size || 0; this.name = opts.name || ''; this.type = opts.type || ''; this.lastModifiedDate = opts.lastModifiedDate || null; this.storagePath = opts.storagePath || ''; // 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; var MyFileHelper = { toJson: function (myFile, success) { /* Safari private browse mode cannot store Blob object to indexeddb. Then use pure json object instead of Blob object. */ var fr = new FileReader(); fr.onload = function (ev) { var base64 = btoa(String.fromCharCode.apply(null, new Uint8Array(fr.result))); success({ opt: { size: myFile.size, name: myFile.name, type: myFile.type, lastModifiedDate: myFile.lastModifiedDate, storagePath: myFile.storagePath }, base64: base64 }); }; fr.readAsArrayBuffer(myFile.blob_); }, setBase64: function (myFile, base64) { if (base64) { var arrayBuffer = (new Uint8Array( [].map.call(atob(base64), function (c) { return c.charCodeAt(0); }) )).buffer; myFile.blob_ = new Blob([arrayBuffer], { type: myFile.type }); } else { myFile.blob_ = new Blob(); } } }; // 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) { path = path || ''; var fullPath = path; var prefix = ''; cwdFullPath = cwdFullPath || DIR_SEPARATOR; if (cwdFullPath.indexOf(FILESYSTEM_PREFIX) === 0) { prefix = cwdFullPath.substring(0, cwdFullPath.indexOf(DIR_SEPARATOR, FILESYSTEM_PREFIX.length)); cwdFullPath = cwdFullPath.substring(cwdFullPath.indexOf(DIR_SEPARATOR, FILESYSTEM_PREFIX.length)); } var relativePath = path[0] !== DIR_SEPARATOR; if (relativePath) { fullPath = cwdFullPath; if (cwdFullPath !== DIR_SEPARATOR) { fullPath += DIR_SEPARATOR + path; } else { fullPath += path; } } // Remove doubled separator substrings var re = new RegExp(DIR_SEPARATOR + DIR_SEPARATOR, 'g'); fullPath = fullPath.replace(re, DIR_SEPARATOR); // Adjust '..'s by removing parent directories when '..' flows in path. var parts = fullPath.split(DIR_SEPARATOR); for (var i = 0; i < parts.length; ++i) { var part = parts[i]; if (part === '..') { parts[i - 1] = ''; parts[i] = ''; } } fullPath = parts.filter(function (el) { return el; }).join(DIR_SEPARATOR); // Add back in leading slash. if (fullPath[0] !== DIR_SEPARATOR) { fullPath = DIR_SEPARATOR + fullPath; } // Replace './' by current dir. ('./one/./two' -> one/two) fullPath = fullPath.replace(/\.\//g, DIR_SEPARATOR); // Replace '//' with '/'. fullPath = fullPath.replace(/\/\//g, DIR_SEPARATOR); // Replace '/.' with '/'. fullPath = fullPath.replace(/\/\./g, DIR_SEPARATOR); // Remove '/' if it appears on the end. if (fullPath[fullPath.length - 1] === DIR_SEPARATOR && fullPath !== DIR_SEPARATOR) { fullPath = fullPath.substring(0, fullPath.length - 1); } var storagePath = prefix + fullPath; storagePath = decodeURI(storagePath); fullPath = decodeURI(fullPath); return { storagePath: storagePath, fullPath: fullPath, fileName: fullPath.split(DIR_SEPARATOR).pop(), fsName: prefix.split(DIR_SEPARATOR).pop() }; } function fileEntryFromIdbEntry (fileEntry) { // IDB won't save methods, so we need re-create the FileEntry. var clonedFileEntry = new FileEntry(fileEntry.name, fileEntry.fullPath, fileEntry.filesystem); clonedFileEntry.file_ = fileEntry.file_; return clonedFileEntry; } function readAs (what, fullPath, encoding, startPos, endPos, successCallback, errorCallback) { exports.getFile(function (fileEntry) { var fileReader = new FileReader(); // eslint-disable-line no-undef var blob = fileEntry.file_.blob_.slice(startPos, endPos); fileReader.onload = function (e) { successCallback(e.target.result); }; fileReader.onerror = errorCallback; switch (what) { case 'text': fileReader.readAsText(blob, encoding); break; case 'dataURL': fileReader.readAsDataURL(blob); break; case 'arrayBuffer': fileReader.readAsArrayBuffer(blob); break; case 'binaryString': fileReader.readAsBinaryString(blob); break; } }, errorCallback, [fullPath, null]); } /** * Core logic to handle IDB operations ***/ idb_.open = function (dbName, successCallback, 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 = 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_)) { 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 = errorCallback || onError; }; idb_.close = function () { this.db.close(); this.db = null; }; idb_.get = function (fullPath, successCallback, errorCallback) { if (!this.db) { if (errorCallback) { errorCallback(FileError.INVALID_MODIFICATION_ERR); } return; } var tx = this.db.transaction([FILE_STORE_], 'readonly'); var request = tx.objectStore(FILE_STORE_).get(fullPath); tx.onabort = errorCallback || onError; tx.oncomplete = function () { var entry = request.result; if (entry && entry.file_json) { /* Safari private browse mode cannot store Blob object to indexeddb. Then use pure json object instead of Blob object. */ entry.file_ = new MyFile(entry.file_json.opt); MyFileHelper.setBase64(entry.file_, entry.file_json.base64); delete entry.file_json; } successCallback(entry); }; }; idb_.getAllEntries = function (fullPath, storagePath, successCallback, errorCallback) { if (!this.db) { if (errorCallback) { errorCallback(FileError.INVALID_MODIFICATION_ERR); } return; } var results = []; if (storagePath[storagePath.length - 1] === DIR_SEPARATOR) { storagePath = storagePath.substring(0, storagePath.length - 1); } var range = IDBKeyRange.bound(storagePath + DIR_SEPARATOR + ' ', storagePath + DIR_SEPARATOR + String.fromCharCode(unicodeLastChar)); var tx = this.db.transaction([FILE_STORE_], 'readonly'); tx.onabort = errorCallback || onError; tx.oncomplete = function () { results = results.filter(function (val) { var pathWithoutSlash = val.fullPath; if (val.fullPath[val.fullPath.length - 1] === DIR_SEPARATOR) { pathWithoutSlash = pathWithoutSlash.substr(0, pathWithoutSlash.length - 1); } var valPartsLen = pathWithoutSlash.split(DIR_SEPARATOR).length; var fullPathPartsLen = fullPath.split(DIR_SEPARATOR).length; /* Input fullPath parameter equals '//' for root folder */ /* Entries in root folder has valPartsLen equals 2 (see below) */ if (fullPath[fullPath.length - 1] === DIR_SEPARATOR && fullPath.trim().length === 2) { fullPathPartsLen = 1; } else if (fullPath[fullPath.length - 1] === DIR_SEPARATOR) { fullPathPartsLen = fullPath.substr(0, fullPath.length - 1).split(DIR_SEPARATOR).length; } else { fullPathPartsLen = fullPath.split(DIR_SEPARATOR).length; } if (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; } else return false; }); 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 ? fileEntryFromIdbEntry(val) : new DirectoryEntry(val.name, val.fullPath, val.filesystem)); cursor['continue'](); } }; }; idb_['delete'] = function (fullPath, successCallback, errorCallback, isDirectory) { if (!idb_.db) { if (errorCallback) { errorCallback(FileError.INVALID_MODIFICATION_ERR); } return; } var tx = this.db.transaction([FILE_STORE_], 'readwrite'); tx.oncomplete = successCallback; tx.onabort = errorCallback || onError; tx.oncomplete = function () { if (isDirectory) { // We delete nested files and folders after deleting parent folder // We use ranges: https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange fullPath = fullPath + DIR_SEPARATOR; // Range contains all entries in the form fullPath where // symbol in the range from ' ' to symbol which has code `unicodeLastChar` var range = IDBKeyRange.bound(fullPath + ' ', fullPath + String.fromCharCode(unicodeLastChar)); var newTx = this.db.transaction([FILE_STORE_], 'readwrite'); newTx.oncomplete = successCallback; newTx.onabort = errorCallback || onError; newTx.objectStore(FILE_STORE_)['delete'](range); } else { successCallback(); } }; tx.objectStore(FILE_STORE_)['delete'](fullPath); }; idb_.put = function (entry, storagePath, successCallback, errorCallback, retry) { if (!this.db) { if (errorCallback) { errorCallback(FileError.INVALID_MODIFICATION_ERR); } return; } var tx = this.db.transaction([FILE_STORE_], 'readwrite'); tx.onabort = errorCallback || onError; tx.oncomplete = function () { // TODO: Error is thrown if we pass the request event back instead. successCallback(entry); }; try { tx.objectStore(FILE_STORE_).put(entry, storagePath); } catch (e) { if (e.name === 'DataCloneError') { tx.oncomplete = null; /* Safari private browse mode cannot store Blob object to indexeddb. Then use pure json object instead of Blob object. */ var successCallback2 = function (entry) { entry.file_ = new MyFile(entry.file_json.opt); delete entry.file_json; successCallback(entry); }; if (!retry) { if (entry.file_ && entry.file_ instanceof MyFile && entry.file_.blob_) { MyFileHelper.toJson(entry.file_, function (json) { entry.file_json = json; delete entry.file_; idb_.put(entry, storagePath, successCallback2, errorCallback, true); }); return; } } } throw e; } }; // 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); } })(module.exports, window); require('cordova/exec/proxy').add('File', module.exports); })();