diff options
Diffstat (limited to 'StoneIsland/plugins/cordova-plugin-file/src/browser/FileProxy.js')
| -rw-r--r-- | StoneIsland/plugins/cordova-plugin-file/src/browser/FileProxy.js | 1059 |
1 files changed, 1059 insertions, 0 deletions
diff --git a/StoneIsland/plugins/cordova-plugin-file/src/browser/FileProxy.js b/StoneIsland/plugins/cordova-plugin-file/src/browser/FileProxy.js new file mode 100644 index 00000000..17955b68 --- /dev/null +++ b/StoneIsland/plugins/cordova-plugin-file/src/browser/FileProxy.js @@ -0,0 +1,1059 @@ +/* + * + * 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<symbol> 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); +})(); |
