From a4ea0c80480437198dbb3ccec306ef8beae4215f Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 11 Mar 2014 02:01:22 +0100 Subject: [PATCH] Storage abstraction. --- www/js/app.js | 8 +- www/js/lib/archive.js | 150 +++++++++++----------------- www/js/lib/osabstraction.js | 193 ++++++++++++++++++++++++++++++++++++ www/js/lib/util.js | 69 +------------ 4 files changed, 260 insertions(+), 160 deletions(-) create mode 100644 www/js/lib/osabstraction.js diff --git a/www/js/app.js b/www/js/app.js index 5469f4ae..ddddb4b8 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -34,6 +34,7 @@ define(function(require) { var util = require('util'); var cookies = require('cookies'); var geometry = require('geometry'); + var osabstraction = require('osabstraction'); // Maximum number of titles to display in a search var MAX_SEARCH_RESULT_SIZE = 50; @@ -155,12 +156,15 @@ define(function(require) { // We have to scan all the DeviceStorages, because getDeviceStorage // only returns the default Device Storage. // See https://bugzilla.mozilla.org/show_bug.cgi?id=885753 - storages = navigator.getDeviceStorages("sdcard"); + storages = $.map(navigator.getDeviceStorages("sdcard"), function(s) { + return new osabstraction.StorageFirefoxOS(s); + }); } else { // The method getDeviceStorages is not available (FxOS 1.0) // The fallback is to use getDeviceStorage - storages[0] = navigator.getDeviceStorage("sdcard"); + storages[0] = new osabstraction.StorageFirefoxOS( + navigator.getDeviceStorage("sdcard")); } } diff --git a/www/js/lib/archive.js b/www/js/lib/archive.js index e956dd71..a51b1d6e 100644 --- a/www/js/lib/archive.js +++ b/www/js/lib/archive.js @@ -28,6 +28,7 @@ define(function(require) { var evopediaTitle = require('title'); var util = require('util'); var geometry = require('geometry'); + var jQuery = require('jquery'); // Declare the webworker that can uncompress with bzip2 algorithm var webworkerBzip2 = new Worker("js/lib/webworker_bzip2.js"); @@ -69,20 +70,16 @@ define(function(require) { */ LocalArchive.prototype.readTitleFilesFromStorage = function(storage, directory) { var currentLocalArchiveInstance = this; - var filerequest = storage.get(directory + 'titles.idx'); - filerequest.onsuccess = function() { - currentLocalArchiveInstance.titleFile = filerequest.result; - }; - filerequest.onerror = function(event) { - alert("Error reading title file in directory " + directory + " : " + event.target.error.name); - }; - var filerequestSearch = storage.get(directory + 'titles_search.idx'); - filerequestSearch.onsuccess = function() { - currentLocalArchiveInstance.titleSearchFile = filerequest.result; - }; - filerequest.onerror = function(event) { + storage.get(directory + 'titles.idx').then(function(file) { + currentLocalArchiveInstance.titleFile = file; + }, function(error) { + alert("Error reading title file in directory " + directory + " : " + error); + }); + storage.get(directory + 'titles_search.idx').then(function(file) { + currentLocalArchiveInstance.titleSearchFile = file; + }, function(error) { // Do nothing : this file is not mandatory in an archive - }; + }); }; /** @@ -102,20 +99,18 @@ define(function(require) { } else { prefixedFileNumber = index; } - var filerequest = storage.get(directory + 'wikipedia_' + prefixedFileNumber - + '.dat'); - filerequest.onsuccess = function() { - currentLocalArchiveInstance.dataFiles[index] = filerequest.result; - currentLocalArchiveInstance.readDataFilesFromStorage(storage, directory, - index + 1); - }; - filerequest.onerror = function(event) { - // TODO there must be a better to way to detect a FileNotFound - if (event.target.error.name != "NotFoundError") { - alert("Error reading data file " + index + " in directory " - + directory + " : " + event.target.error.name); - } - }; + storage.get(directory + 'wikipedia_' + prefixedFileNumber + '.dat') + .then(function(file) { + currentLocalArchiveInstance.dataFiles[index] = file; + currentLocalArchiveInstance.readDataFilesFromStorage(storage, directory, + index + 1); + }, function(error) { + // TODO there must be a better to way to detect a FileNotFound + if (error != "NotFoundError") { + alert("Error reading data file " + index + " in directory " + + directory + " : " + error); + } + }); }; /** @@ -135,20 +130,18 @@ define(function(require) { } else { prefixedFileNumber = index; } - var filerequest = storage.get(directory + 'coordinates_' + prefixedFileNumber - + '.idx'); - filerequest.onsuccess = function() { - currentLocalArchiveInstance.coordinateFiles[index - 1] = filerequest.result; + storage.get(directory + 'coordinates_' + prefixedFileNumber + + '.idx').then(function(file) { + currentLocalArchiveInstance.coordinateFiles[index] = file; currentLocalArchiveInstance.readCoordinateFilesFromStorage(storage, directory, index + 1); - }; - filerequest.onerror = function(event) { + }, function(error) { // TODO there must be a better to way to detect a FileNotFound - if (event.target.error.name != "NotFoundError") { + if (error != "NotFoundError") { alert("Error reading coordinates file " + index + " in directory " - + directory + " : " + event.target.error.name); + + directory + " : " + error); } - }; + }); }; /** @@ -161,15 +154,13 @@ define(function(require) { LocalArchive.prototype.readMetadataFileFromStorage = function(storage, directory) { var currentLocalArchiveInstance = this; - var filerequest = storage.get(directory + 'metadata.txt'); - filerequest.onsuccess = function() { - var metadataFile = filerequest.result; + storage.get(directory + 'metadata.txt').then(function(file) { + var metadataFile = file; currentLocalArchiveInstance.readMetadataFile(metadataFile); - }; - filerequest.onerror = function(event) { + }, function(error) { alert("Error reading metadata.txt file in directory " - + directory + " : " + event.target.error.name); - }; + + directory + " : " + error); + }); }; /** @@ -300,20 +291,16 @@ define(function(require) { */ LocalArchive.prototype.readMathFilesFromStorage = function(storage, directory) { var currentLocalArchiveInstance = this; - var filerequest1 = storage.get(directory + 'math.idx'); - filerequest1.onsuccess = function() { - currentLocalArchiveInstance.mathIndexFile = filerequest1.result; - }; - filerequest1.onerror = function(event) { - alert("Error reading math index file in directory " + directory + " : " + event.target.error.name); - }; - var filerequest2 = storage.get(directory + 'math.dat'); - filerequest2.onsuccess = function() { - currentLocalArchiveInstance.mathDataFile = filerequest2.result; - }; - filerequest2.onerror = function(event) { - alert("Error reading math data file in directory " + directory + " : " + event.target.error.name); - }; + storage.get(directory + 'math.idx').then(function(file) { + currentLocalArchiveInstance.mathIndexFile = file; + }, function(error) { + alert("Error reading math index file in directory " + directory + " : " + error); + }); + storage.get(directory + 'math.dat').then(function(file) { + currentLocalArchiveInstance.mathDataFile = file; + }, function(error) { + alert("Error reading math data file in directory " + directory + " : " + error); + }); }; /** @@ -928,40 +915,23 @@ define(function(require) { */ LocalArchive.scanForArchives = function(storages, callbackFunction) { var directories = []; - var cursor = util.enumerateAll(storages); - cursor.onerror = function() { - alert("Error scanning your SD card : " + cursor.error - +". If you're using the Firefox OS Simulator, please put the archives in a 'fake-sdcard' directory inside your Firefox profile (ex : ~/.mozilla/firefox/xxxx.default/extensions/r2d2b2g@mozilla.org/profile/fake-sdcard/wikipedia_small_2010-08-14)"); + var promises = jQuery.map(storages, function(storage) { + return storage.scanForDirectoriesContainingFile('titles.idx') + .then(function(dirs) { + jQuery.merge(directories, dirs); + return true + }); + }); + jQuery.when.apply(null, promises).then(function() { + callbackFunction(directories); + }, function(error) { + alert("Error scanning your SD card : " + error + + ". If you're using the Firefox OS Simulator, please put the archives in " + + "a 'fake-sdcard' directory inside your Firefox profile " + + "(ex : ~/.mozilla/firefox/xxxx.default/extensions/r2d2b2g@mozilla.org/" + + "profile/fake-sdcard/wikipedia_small_2010-08-14)"); callbackFunction(null); - }; - cursor.onsuccess = function() { - if (cursor.result) { - var file = cursor.result; - var fileName = file.name; - - // We look for files "titles.idx" - if (!util.endsWith(fileName, "titles.idx")) { - cursor.continue(); - return; - } - - // Handle the case of archive files at the root of the sd-card - // (without a subdirectory) - var directory = "/"; - - if (fileName.lastIndexOf('/')!==-1) { - // We want to return the directory where titles.idx is stored - // We also keep the trailing slash - directory = fileName.substring(0, fileName.lastIndexOf('/') + 1); - } - - directories.push(directory); - cursor.continue(); - } - else { - callbackFunction(directories); - } - }; + }); }; /** diff --git a/www/js/lib/osabstraction.js b/www/js/lib/osabstraction.js new file mode 100644 index 00000000..52bca2d8 --- /dev/null +++ b/www/js/lib/osabstraction.js @@ -0,0 +1,193 @@ +/** + * osabstraction.js: Abstraction layer for file access + * + * Copyright 2014 Evopedia developers + * License GPL v3: + * + * This file is part of Evopedia. + * + * Evopedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Evopedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Evopedia (file LICENSE-GPLv3.txt). If not, see + */ +define(['util', 'jquery'], function(util, jQuery) { + /** + * Creates an abstraction layer around the FirefoxOS storage. + * @see StoragePhoneGap + * @param storage FirefoxOS DeviceStorage object + */ + function StorageFirefoxOS(storage) { + this._storage = storage; + this.storageName = storage.storageName; + } + /** + * Access the given file. + * @param path absolute path to the file + * @return jQuery promise which is resolved with a HTML5 file object and + * rejected with an error message. + */ + StorageFirefoxOS.prototype.get = function(path) { + var deferred = jQuery.Deferred(); + var request = this._storage.get(path); + request.onsuccess = function() { deferred.resolve(this.result); } + request.onerror = function() { deferred.reject(this.error.name); } + return deferred.promise(); + } + /** + * Searches for directories containing a file with the given name. + * @param fileName file name to search + * @return jQuery promise which is resolved with an array of directory + * paths and rejected with an error message. + */ + StorageFirefoxOS.prototype.scanForDirectoriesContainingFile + = function(fileName) { + var deferred = jQuery.Deferred(); + var directories = []; + var cursor = this._storage.enumerate(); + cursor.onerror = function() { + deferred.reject(cursor.error); + }; + cursor.onsuccess = function() { + if (!cursor.result) { + deferred.resolve(directories); + return; + } + var file = cursor.result; + + if (!util.endsWith(file.name, fileName)) { + cursor.continue(); + return; + } + + // Handle the case of archive files at the root of the sd-card + // (without a subdirectory) + var directory = "/"; + + if (file.name.lastIndexOf('/') !== -1) { + // We want to return the directory where the file is stored + // We also keep the trailing slash + directory = file.name.substring(0, + file.name.lastIndexOf('/') + 1); + } + directories.push(directory); + + cursor.continue(); + }; + return deferred.promise(); + } + + /** + * Creates an abstraction layour around the PhoneGap storage. + * @see StorageFirefoxOS + * @param storage PhoneGap FileSystem object + */ + function StoragePhoneGap(storage) { + this._storage = storage; + this.storageName = 'PhoneGapStorage'; // TODO + } + /** + * Access the given file. + * @param path absolute path to the file + * @return jQuery promise which is resolved with a HTML5 file object and + * rejected with an error message. + */ + StoragePhoneGap.prototype.get = function(path) { + console.log("Trying to access " + path); + var deferred = jQuery.Deferred(); + var that = this; + var onSuccess = function(file) { + deferred.resolve(file); + }; + var onError = function(error) { + console.log("Error code: " + error.code); + deferred.reject(that._errorCodeToString(error.code)); + }; + var onSuccessInt = function(fileEntry) { + fileEntry.file(onSuccess, onError); + }; + var options = {create: false, exclusive: false}; + if (path.substr(0, 7) == 'file://') + path = path.substr(7); + this._storage.root.getFile(path, options, onSuccessInt, onError); + return deferred.promise(); + } + /** + * Searches for directories containing a file with the given name. + * @param fileName file name to search + * @return jQuery promise which is resolved with an array of directory + * paths and rejected with an error message. + */ + StoragePhoneGap.prototype.scanForDirectoriesContainingFile + = function(fileName) { + var that = this; + var deferred = jQuery.Deferred(); + var directories = []; + var stack = [this._storage.root]; + + var dirReaderSuccess = function(entries) { + var dir = stack[stack.length - 1]; + stack.pop(); + for (var i = 0; i < entries.length; i ++) { + var entry = entries[i]; + if (entry.isDirectory) { + stack.push(entry); + } else if (util.endsWith(entry.name, fileName)) { + var path = dir.fullPath; + if (path.length == 0 || path[path.length - 1] != '/') + path += '/'; + directories.push(path); + } + } + iteration(); + } + var dirReaderFail = function(error) { + deferred.reject(that._errorCodeToString(error.code)); + } + var iteration = function() { + if (stack.length == 0) { + deferred.resolve(directories); + return; + } + var reader = stack[stack.length - 1].createReader(); + reader.readEntries(dirReaderSuccess, dirReaderFail); + } + iteration(); + return deferred.promise(); + } + + /** + * Convert HTML5 FileError codes to strings. + * @param code FileError code + * @return string message corresponding to the error code + */ + StoragePhoneGap.prototype._errorCodeToString = function(code) { + switch (code) { + case FileError.QUOTA_EXCEEDED_ERR: + return 'QUOTA_EXCEEDED_ERR'; + case FileError.NOT_FOUND_ERR: + return 'NOT_FOUND_ERR'; + case FileError.SECURITY_ERR: + return 'SECURITY_ERR'; + case FileError.INVALID_MODIFICATION_ERR: + return 'INVALID_MODIFICATION_ERR'; + case FileError.INVALID_STATE_ERR: + return 'INVALID_STATE_ERR'; + default: + return 'Unknown Error'; + } + } + + return { + StorageFirefoxOS: StorageFirefoxOS, + StoragePhoneGap: StoragePhoneGap + }; +}); diff --git a/www/js/lib/util.js b/www/js/lib/util.js index 8dcb27a8..2e819d29 100644 --- a/www/js/lib/util.js +++ b/www/js/lib/util.js @@ -113,72 +113,6 @@ define(function(require) { return (r > 0 ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3); } - - /** - * This function emulates a "composite" storage enumeration - * (i.e. returns files from all the storage areas) - * This is needed since the removal of composite storage : - * see https://bugzilla.mozilla.org/show_bug.cgi?id=885753 - * - * This code was copied (with only slight modifications) from Gaia source code : - * https://bug893282.bugzilla.mozilla.org/attachment.cgi?id=785076 - * - * @param {type} storages List of DeviceStorage instances - * @returns {_L22.enumerateAll.cursor} Cursor of files found in device storages - */ - function enumerateAll(storages) { - var storageIndex = 0; - var ds_cursor = null; - - var cursor = { - continue: function cursor_continue() { - ds_cursor.continue(); - } - }; - - function enumerateNextStorage() { - ds_cursor = storages[storageIndex].enumerate(); - ds_cursor.onsuccess = onsuccess; - ds_cursor.onerror = onerror; - }; - - function onsuccess(e) { - cursor.result = e.target.result; - if (!cursor.result) { - storageIndex++; - if (storageIndex < storages.length) { - enumerateNextStorage(); - return; - } - // If we've run out of storages, then we fall through and call - // onsuccess with the null result. - } - if (cursor.onsuccess) { - try { - cursor.onsuccess(e); - } catch (err) { - console.warn('enumerateAll onsuccess threw', err); - } - } - }; - - function onerror(e) { - cursor.error = e.target.error; - if (cursor.onerror) { - try { - cursor.onerror(e); - } catch (err) { - console.warn('enumerateAll onerror threw', err); - } - } - }; - - enumerateNextStorage(); - return cursor; - } - - - /** * Functions and classes exposed by this module */ @@ -188,7 +122,6 @@ define(function(require) { readIntegerFrom2Bytes : readIntegerFrom2Bytes, readFloatFrom4Bytes : readFloatFrom4Bytes, uint8ArrayToHex : uint8ArrayToHex, - uint8ArrayToBase64 : uint8ArrayToBase64, - enumerateAll : enumerateAll + uint8ArrayToBase64 : uint8ArrayToBase64 }; }); \ No newline at end of file