Bug 1007409 - Cache reading list articles in files, not indexedDB. r=rnewman

This commit is contained in:
Margaret Leibovic 2014-11-04 13:34:45 -08:00
parent 2683d1a61a
commit c1da8b2813
2 changed files with 146 additions and 164 deletions

View File

@ -4,9 +4,12 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */ * You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict"; "use strict";
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
"resource://services-common/utils.js");
let Reader = { let Reader = {
// Version of the cache database schema // Version of the cache schema.
DB_VERSION: 1, CACHE_VERSION: 1,
DEBUG: 0, DEBUG: 0,
@ -76,7 +79,8 @@ let Reader = {
observe: function(aMessage, aTopic, aData) { observe: function(aMessage, aTopic, aData) {
switch(aTopic) { switch(aTopic) {
case "Reader:Removed": { case "Reader:Removed": {
this.removeArticleFromCache(aData); let uri = Services.io.newURI(aData, null, null);
this.removeArticleFromCache(uri).catch(e => Cu.reportError("Error removing article from cache: " + e));
break; break;
} }
@ -91,10 +95,13 @@ let Reader = {
_addTabToReadingList: function(tabID) { _addTabToReadingList: function(tabID) {
let tab = BrowserApp.getTabForId(tabID); let tab = BrowserApp.getTabForId(tabID);
let currentURI = tab.browser.currentURI; if (!tab) {
let urlWithoutRef = currentURI.specIgnoringRef; Cu.reportError("Can't add tab to reading list because no tab found for ID: " + tabID);
return;
}
let uri = tab.browser.currentURI;
this.getArticleFromCache(urlWithoutRef, (article) => { this.getArticleFromCache(uri).then(article => {
// If the article is already in the cache, just use that. // If the article is already in the cache, just use that.
if (article) { if (article) {
this.addArticleToReadingList(article); this.addArticleToReadingList(article);
@ -102,7 +109,7 @@ let Reader = {
} }
// Otherwise, get the article data from the tab. // Otherwise, get the article data from the tab.
this.getArticleForTab(tabID, urlWithoutRef, (article) => { this.getArticleForTab(tabID, uri.specIgnoringRef, article => {
if (article) { if (article) {
this.addArticleToReadingList(article); this.addArticleToReadingList(article);
} else { } else {
@ -114,7 +121,7 @@ let Reader = {
}); });
} }
}); });
}); }, e => Cu.reportError("Error trying to get article from cache: " + e));
}, },
addArticleToReadingList: function(article) { addArticleToReadingList: function(article) {
@ -131,7 +138,7 @@ let Reader = {
excerpt: article.excerpt || "", excerpt: article.excerpt || "",
}); });
this.storeArticleInCache(article); this.storeArticleInCache(article).catch(e => Cu.reportError("Error storing article in cache: " + e));
}, },
getStateForParseOnLoad: function Reader_getStateForParseOnLoad() { getStateForParseOnLoad: function Reader_getStateForParseOnLoad() {
@ -154,30 +161,28 @@ let Reader = {
let request = { url: url, callbacks: [callback] }; let request = { url: url, callbacks: [callback] };
this._requests[url] = request; this._requests[url] = request;
try { let uri = Services.io.newURI(url, null, null);
this.log("parseDocumentFromURL: " + url);
// First, try to find a cached parsed article in the DB // First, try to find a parsed article in the cache.
this.getArticleFromCache(url, function(article) { this.getArticleFromCache(uri).then(article => {
if (article) { if (article) {
this.log("Page found in cache, return article immediately"); this.log("Page found in cache, return article immediately");
this._runCallbacksAndFinish(request, article); this._runCallbacksAndFinish(request, article);
return; return;
} }
if (!this._requests) { if (!this._requests) {
this.log("Reader has been destroyed, abort"); this.log("Reader has been destroyed, abort");
return; return;
} }
// Article hasn't been found in the cache DB, we need to // Article hasn't been found in the cache, we need to
// download the page and parse the article out of it. // download the page and parse the article out of it.
this._downloadAndParseDocument(url, request); this._downloadAndParseDocument(url, request);
}.bind(this)); }, e => {
} catch (e) { Cu.reportError("Error trying to get article from cache: " + e);
this.log("Error parsing document from URL: " + e);
this._runCallbacksAndFinish(request, null); this._runCallbacksAndFinish(request, null);
} });
}, },
getArticleForTab: function Reader_getArticleForTab(tabId, url, callback) { getArticleForTab: function Reader_getArticleForTab(tabId, url, callback) {
@ -194,109 +199,81 @@ let Reader = {
this.parseDocumentFromURL(url, callback); this.parseDocumentFromURL(url, callback);
}, },
parseDocumentFromTab: function(tabId, callback) { parseDocumentFromTab: function (tab, callback) {
try { let uri = tab.browser.currentURI;
this.log("parseDocumentFromTab: " + tabId); if (!this._shouldCheckUri(uri)) {
callback(null);
return;
}
let tab = BrowserApp.getTabForId(tabId); // First, try to find a parsed article in the cache.
let url = tab.browser.contentWindow.location.href; this.getArticleFromCache(uri).then(article => {
let uri = Services.io.newURI(url, null, null); if (article) {
this.log("Page found in cache, return article immediately");
if (!this._shouldCheckUri(uri)) { callback(article);
callback(null);
return; return;
} }
// First, try to find a cached parsed article in the DB let doc = tab.browser.contentWindow.document;
this.getArticleFromCache(url, function(article) { this._readerParse(uri, doc, article => {
if (article) { if (!article) {
this.log("Page found in cache, return article immediately"); this.log("Failed to parse page");
callback(article); callback(null);
return; return;
} }
callback(article);
let doc = tab.browser.contentWindow.document; });
this._readerParse(uri, doc, function (article) { }, e => {
if (!article) { Cu.reportError("Error trying to get article from cache: " + e);
this.log("Failed to parse page");
callback(null);
return;
}
callback(article);
}.bind(this));
}.bind(this));
} catch (e) {
this.log("Error parsing document from tab: " + e);
callback(null); callback(null);
});
},
/**
* Retrieves an article from the cache given an article URI.
*
* @param uri The article URI.
* @return Promise
* @resolve JS object representing the article, or null if no article is found.
* @rejects OS.File.Error
*/
getArticleFromCache: Task.async(function* (uri) {
let path = this._toHashedPath(uri.specIgnoringRef);
try {
let array = yield OS.File.read(path);
return JSON.parse(new TextDecoder().decode(array));
} catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
return null;
} }
}, }),
getArticleFromCache: function Reader_getArticleFromCache(url, callback) { /**
this._getCacheDB(function(cacheDB) { * Stores an article in the cache.
if (!cacheDB) { *
callback(false); * @param article JS object representing article.
return; * @return Promise
} * @resolve When the article is stored.
* @rejects OS.File.Error
*/
storeArticleInCache: Task.async(function* (article) {
let array = new TextEncoder().encode(JSON.stringify(article));
let path = this._toHashedPath(article.url);
yield this._ensureCacheDir();
yield OS.File.writeAtomic(path, array, { tmpPath: path + ".tmp" });
}),
let transaction = cacheDB.transaction(cacheDB.objectStoreNames); /**
let articles = transaction.objectStore(cacheDB.objectStoreNames[0]); * Removes an article from the cache given an article URI.
*
let request = articles.get(url); * @param uri The article URI.
* @return Promise
request.onerror = function(event) { * @resolve When the article is removed.
this.log("Error getting article from the cache DB: " + url); * @rejects OS.File.Error
callback(null); */
}.bind(this); removeArticleFromCache: Task.async(function* (uri) {
let path = this._toHashedPath(uri.specIgnoringRef);
request.onsuccess = function(event) { yield OS.File.remove(path);
this.log("Got article from the cache DB: " + event.target.result); }),
callback(event.target.result);
}.bind(this);
}.bind(this));
},
storeArticleInCache: function Reader_storeArticleInCache(article) {
this._getCacheDB(function(cacheDB) {
if (!cacheDB) {
return;
}
let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite");
let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
let request = articles.add(article);
request.onerror = function(event) {
this.log("Error storing article in the cache DB: " + article.url);
}.bind(this);
request.onsuccess = function(event) {
this.log("Stored article in the cache DB: " + article.url);
}.bind(this);
}.bind(this));
},
removeArticleFromCache: function Reader_removeArticleFromCache(url) {
this._getCacheDB(function(cacheDB) {
if (!cacheDB) {
return;
}
let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite");
let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
let request = articles.delete(url);
request.onerror = function(event) {
this.log("Error removing article from the cache DB: " + url);
}.bind(this);
request.onsuccess = function(event) {
this.log("Removed article from the cache DB: " + url);
}.bind(this);
}.bind(this));
},
uninit: function Reader_uninit() { uninit: function Reader_uninit() {
Services.prefs.removeObserver("reader.parse-on-load.", this); Services.prefs.removeObserver("reader.parse-on-load.", this);
@ -312,11 +289,6 @@ let Reader = {
} }
} }
delete this._requests; delete this._requests;
if (this._cacheDB) {
this._cacheDB.close();
delete this._cacheDB;
}
}, },
log: function(msg) { log: function(msg) {
@ -374,7 +346,7 @@ let Reader = {
doc: new XMLSerializer().serializeToString(doc) doc: new XMLSerializer().serializeToString(doc)
}); });
} catch (e) { } catch (e) {
dump("Reader: could not build Readability arguments: " + e); Cu.reportError("Reader: could not build Readability arguments: " + e);
callback(null); callback(null);
} }
}, },
@ -406,7 +378,7 @@ let Reader = {
browser.webNavigation.allowMetaRedirects = true; browser.webNavigation.allowMetaRedirects = true;
browser.webNavigation.allowPlugins = false; browser.webNavigation.allowPlugins = false;
browser.addEventListener("DOMContentLoaded", function (event) { browser.addEventListener("DOMContentLoaded", event => {
let doc = event.originalTarget; let doc = event.originalTarget;
// ignore on frames and other documents // ignore on frames and other documents
@ -423,7 +395,7 @@ let Reader = {
} }
callback(doc); callback(doc);
}.bind(this)); });
browser.loadURIWithFlags(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, browser.loadURIWithFlags(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
null, null, null); null, null, null);
@ -435,7 +407,7 @@ let Reader = {
try { try {
this.log("Needs to fetch page, creating request: " + url); this.log("Needs to fetch page, creating request: " + url);
request.browser = this._downloadDocument(url, function(doc) { request.browser = this._downloadDocument(url, doc => {
this.log("Finished loading page: " + doc); this.log("Finished loading page: " + doc);
if (!doc) { if (!doc) {
@ -447,7 +419,7 @@ let Reader = {
this.log("Parsing response with Readability"); this.log("Parsing response with Readability");
let uri = Services.io.newURI(url, null, null); let uri = Services.io.newURI(url, null, null);
this._readerParse(uri, doc, function (article) { this._readerParse(uri, doc, article => {
// Delete reference to the browser element as we've finished parsing. // Delete reference to the browser element as we've finished parsing.
let browser = request.browser; let browser = request.browser;
if (browser) { if (browser) {
@ -462,47 +434,57 @@ let Reader = {
} }
this.log("Parsing has been successful"); this.log("Parsing has been successful");
this._runCallbacksAndFinish(request, article); this._runCallbacksAndFinish(request, article);
}.bind(this)); });
}.bind(this)); });
} catch (e) { } catch (e) {
this.log("Error downloading and parsing document: " + e); this.log("Error downloading and parsing document: " + e);
this._runCallbacksAndFinish(request, null); this._runCallbacksAndFinish(request, null);
} }
}, },
_getCacheDB: function Reader_getCacheDB(callback) { get _cryptoHash() {
if (this._cacheDB) { delete this._cryptoHash;
callback(this._cacheDB); return this._cryptoHash = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
return; },
}
let request = window.indexedDB.open("about:reader", this.DB_VERSION); get _unicodeConverter() {
delete this._unicodeConverter;
this._unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
this._unicodeConverter.charset = "utf8";
return this._unicodeConverter;
},
request.onerror = function(event) { /**
this.log("Error connecting to the cache DB"); * Calculate the hashed path for a stripped article URL.
this._cacheDB = null; *
callback(null); * @param url The article URL. This should have referrers removed.
}.bind(this); * @return The file path to the cached article.
*/
_toHashedPath: function (url) {
let value = this._unicodeConverter.convertToByteArray(url);
this._cryptoHash.init(this._cryptoHash.MD5);
this._cryptoHash.update(value, value.length);
request.onsuccess = function(event) { let hash = CommonUtils.encodeBase32(this._cryptoHash.finish(false));
this.log("Successfully connected to the cache DB"); let fileName = hash.substring(0, hash.indexOf("=")) + ".json";
this._cacheDB = event.target.result; return OS.Path.join(OS.Constants.Path.profileDir, "readercache", fileName);
callback(this._cacheDB); },
}.bind(this);
request.onupgradeneeded = function(event) { /**
this.log("Database schema upgrade from " + * Ensures the cache directory exists.
event.oldVersion + " to " + event.newVersion); *
* @return Promise
let cacheDB = event.target.result; * @resolves When the cache directory exists.
* @rejects OS.File.Error
// Create the articles object store */
this.log("Creating articles object store"); _ensureCacheDir: function () {
cacheDB.createObjectStore("articles", { keyPath: "url" }); let dir = OS.Path.join(OS.Constants.Path.profileDir, "readercache");
return OS.File.exists(dir).then(exists => {
this.log("Database upgrade done: " + this.DB_VERSION); if (!exists) {
}.bind(this); return OS.File.makeDir(dir);
}
});
} }
}; };

View File

@ -4225,7 +4225,7 @@ Tab.prototype = {
return; return;
// Once document is fully loaded, parse it // Once document is fully loaded, parse it
Reader.parseDocumentFromTab(this.id, function (article) { Reader.parseDocumentFromTab(this, function (article) {
// The loaded page may have changed while we were parsing the document. // The loaded page may have changed while we were parsing the document.
// Make sure we've got the current one. // Make sure we've got the current one.
let uri = this.browser.currentURI; let uri = this.browser.currentURI;