 Issue 29796555:
  Issue 6621  (Closed)
    
  
    Issue 29796555:
  Issue 6621  (Closed) 
  | Index: lib/ioIndexedDB.js | 
| diff --git a/lib/ioIndexedDB.js b/lib/ioIndexedDB.js | 
| new file mode 100644 | 
| index 0000000000000000000000000000000000000000..d4ed7ffa009bd53006245a860854736e621b1974 | 
| --- /dev/null | 
| +++ b/lib/ioIndexedDB.js | 
| @@ -0,0 +1,300 @@ | 
| +/* | 
| + * This file is part of Adblock Plus <https://adblockplus.org/>, | 
| + * Copyright (C) 2006-present eyeo GmbH | 
| + * | 
| + * Adblock Plus is free software: you can redistribute it and/or modify | 
| + * it under the terms of the GNU General Public License version 3 as | 
| + * published by the Free Software Foundation. | 
| + * | 
| + * Adblock Plus 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 Adblock Plus. If not, see <http://www.gnu.org/licenses/>. | 
| + */ | 
| + | 
| +"use strict"; | 
| + | 
| + | 
| +// values from the DefaultConfig | 
| +// https://github.com/localForage/localForage/blob/2cdbd74/src/localforage.js#L42-L51 | 
| +const localForageDbConfig = { | 
| + dbName: "localforage", | 
| + storeName: "keyvaluepairs", | 
| + version: 2 | 
| +}; | 
| + | 
| +const dbConfig = { | 
| + dbName: "adblockplus", | 
| + storeName: "file", | 
| + keyPath: "fileName", | 
| + version: 1 | 
| +}; | 
| + | 
| +let db = openDB(dbConfig); | 
| +let migrationDone = migrateFiles(); | 
| + | 
| +const keyPrefix = "file:"; | 
| + | 
| +/** | 
| + * Handles migrating all files from localforage db | 
| + * used in the previous implementation by the localForage library | 
| + * to the new adblockplus db that we use as a replacement | 
| + * @return {Promise} | 
| + * Promise to be resolved or rejected once the operation is completed | 
| + */ | 
| +function migrateFiles() | 
| +{ | 
| + return openDB(localForageDbConfig) | 
| + .then(localForageDb => | 
| + { | 
| + return getAllFiles(localForageDb, localForageDbConfig.storeName) | 
| 
kzar
2018/06/12 10:46:17
Nit: I guess we can omit the curly braces and the
 
piscoi.georgiana
2018/06/14 13:41:26
Done.
 | 
| + .then(files => | 
| + db.then(dbInstance => files.map(file => | 
| + Promise.all(saveFile(file, dbInstance, dbConfig.storeName))))) | 
| 
Sebastian Noack
2018/06/13 00:55:12
Please apologize if I'm wrong, but to me this seem
 
piscoi.georgiana
2018/06/14 13:41:27
Spot on, thank you! I've fixed it.
 | 
| + .then(() => | 
| + clearObjectStore(localForageDb, localForageDbConfig.storeName)); | 
| + }); | 
| +} | 
| + | 
| +function getAllFiles(dbInstance, storeName) | 
| +{ | 
| + return new Promise((resolve, reject) => | 
| + { | 
| + // edge doesn't currently support getAll method on IDBObjectStore interface | 
| + // so a cursor is used to iterate over all objects from the store | 
| + let transaction = dbInstance | 
| + .transaction([storeName], IDBTransaction.READ_ONLY); | 
| + | 
| + let store = transaction.objectStore(storeName); | 
| + let cursorReq = store.openCursor(); | 
| + let filesData = []; | 
| + | 
| + transaction.oncomplete = event => | 
| + { | 
| + resolve(filesData); | 
| + }; | 
| + | 
| + cursorReq.onsuccess = event => | 
| + { | 
| + let cursor = event.currentTarget.result; | 
| + if (cursor) | 
| + { | 
| + let value = cursor.value; | 
| + | 
| + filesData.push({ | 
| + fileName: cursor.key, | 
| + content: value.content, | 
| + lastModified: value.lastModified | 
| + }); | 
| + cursor.continue(); | 
| + } | 
| + }; | 
| + | 
| + cursorReq.onerror = reject; | 
| + }); | 
| +} | 
| + | 
| +function clearObjectStore(dbInstance, storeName) | 
| +{ | 
| + return new Promise((resolve, reject) => | 
| + { | 
| + let store = getObjectStore(dbInstance, storeName); | 
| + let req = store.clear(); | 
| + | 
| + req.onsuccess = resolve; | 
| + req.onerror = reject; | 
| + }); | 
| +} | 
| + | 
| +function fileToKey(fileName) | 
| +{ | 
| + return keyPrefix + fileName; | 
| +} | 
| + | 
| +function formatFile(name, data) | 
| +{ | 
| + return { | 
| + fileName: fileToKey(name), | 
| + content: Array.from(data), | 
| + lastModified: Date.now() | 
| + }; | 
| +} | 
| + | 
| +function openDB({dbName, storeName, version, keyPath}) | 
| +{ | 
| + return new Promise((resolve, reject) => | 
| + { | 
| + let req = indexedDB.open(dbName, version); | 
| + | 
| + req.onsuccess = event => | 
| + { | 
| + return resolve(event.currentTarget.result); | 
| + }; | 
| + | 
| + req.onerror = reject; | 
| + | 
| + req.onupgradeneeded = event => | 
| + { | 
| + event | 
| + .currentTarget | 
| + .result | 
| + .createObjectStore(storeName, | 
| + { | 
| + keyPath, | 
| + autoIncrement: true | 
| + }); | 
| + }; | 
| + }); | 
| +} | 
| + | 
| +function getObjectStore(dbInstance, storeName) | 
| +{ | 
| + return dbInstance | 
| + .transaction([storeName], IDBTransaction.READ_WRITE) | 
| + .objectStore(storeName); | 
| +} | 
| + | 
| +function getFile(fileName, dbInstance, storeName) | 
| +{ | 
| + return new Promise((resolve, reject) => | 
| + { | 
| + let store = getObjectStore(dbInstance, storeName); | 
| + let req = store.get(fileToKey(fileName)); | 
| + | 
| + req.onsuccess = event => | 
| + { | 
| + let result = event.currentTarget.result; | 
| + | 
| + if (result) | 
| + resolve(result); | 
| + else | 
| + reject({type: "NoSuchFile"}); | 
| + }; | 
| + req.onerror = reject; | 
| + }); | 
| +} | 
| + | 
| +function saveFile(data, dbInstance, storeName) | 
| +{ | 
| + return new Promise((resolve, reject) => | 
| + { | 
| + let store = getObjectStore(dbInstance, storeName); | 
| + let req = store.put(data); | 
| + | 
| + req.onsuccess = resolve; | 
| + req.onerror = reject; | 
| + }); | 
| +} | 
| + | 
| +function deleteFile(fileName, dbInstance, storeName) | 
| +{ | 
| + return new Promise((resolve, reject) => | 
| + { | 
| + let store = getObjectStore(dbInstance, storeName); | 
| + let req = store.delete(fileToKey(fileName)); | 
| + | 
| + req.onsuccess = resolve; | 
| + req.onerror = reject; | 
| + }); | 
| +} | 
| + | 
| +exports.IO = | 
| +{ | 
| + /** | 
| + * Writes text lines to a file. | 
| + * @param {string} fileName | 
| + * Name of the file to be written | 
| + * @param {Iterable.<string>} data | 
| + * An array-like or iterable object containing the lines (without line | 
| + * endings) | 
| + * @return {Promise} | 
| + * Promise to be resolved or rejected once the operation is completed | 
| + */ | 
| + writeToFile(fileName, data) | 
| + { | 
| + return migrationDone | 
| + .then(() => db) | 
| + .then(dbInstance => | 
| + saveFile(formatFile(fileName, data), dbInstance, dbConfig.storeName)); | 
| + }, | 
| + | 
| + /** | 
| + * Reads text lines from a file. | 
| + * @param {string} fileName | 
| + * Name of the file to be read | 
| + * @param {TextSink} listener | 
| + * Function that will be called for each line in the file | 
| + * @return {Promise} | 
| + * Promise to be resolved or rejected once the operation is completed | 
| + */ | 
| + readFromFile(fileName, listener) | 
| + { | 
| + return migrationDone | 
| + .then(() => db) | 
| + .then(dbInstance => getFile(fileName, dbInstance, dbConfig.storeName)) | 
| + .then(entry => | 
| + { | 
| + for (let line of entry.content) | 
| + listener(line); | 
| + }); | 
| + }, | 
| + | 
| + /** | 
| + * Retrieves file metadata. | 
| + * @param {string} fileName | 
| + * Name of the file to be looked up | 
| + * @return {Promise.<StatData>} | 
| + * Promise to be resolved with file metadata once the operation is | 
| + * completed | 
| + */ | 
| + statFile(fileName) | 
| + { | 
| + return migrationDone | 
| + .then(() => db) | 
| 
kzar
2018/06/12 10:46:17
Couldn't we do `db.then(dbInstance => ...` instead
 
piscoi.georgiana
2018/06/14 13:41:27
Yes, it looks better your way. Thank you!
 | 
| + .then(dbInstance => getFile(fileName, dbInstance, dbConfig.storeName)) | 
| + .then(entry => | 
| + { | 
| + return { | 
| + exists: true, | 
| + lastModified: entry.lastModified | 
| + }; | 
| + }) | 
| + .catch(error => | 
| + { | 
| + if (error.type == "NoSuchFile") | 
| + return {exists: false}; | 
| + throw error; | 
| + }); | 
| + }, | 
| + | 
| + /** | 
| + * Renames a file. | 
| + * @param {string} fromFile | 
| + * Name of the file to be renamed | 
| + * @param {string} newName | 
| + * New file name, will be overwritten if exists | 
| + * @return {Promise} | 
| + * Promise to be resolved or rejected once the operation is completed | 
| + */ | 
| + renameFile(fromFile, newName) | 
| + { | 
| + return migrationDone | 
| + .then(() => db) | 
| + .then(dbInstance => | 
| + getFile(fromFile, dbInstance, dbConfig.storeName) | 
| + .then(fileData => | 
| + saveFile( | 
| + { | 
| + fileName: fileToKey(newName), | 
| + content: fileData.content, | 
| + lastModified: fileData.lastModified | 
| + }, | 
| + dbInstance, | 
| + dbConfig.storeName)) | 
| + .then(() => deleteFile(fromFile, dbInstance, dbConfig.storeName))); | 
| + } | 
| +}; | 
| + |