Merge lp:~urbanape/bindwood/with-couch-libraries into lp:~urbanape/bindwood/trunk

Proposed by Zachery Bir
Status: Merged
Merge reported by: Zachery Bir
Merged at revision: not available
Proposed branch: lp:~urbanape/bindwood/with-couch-libraries
Merge into: lp:~urbanape/bindwood/trunk
Diff against target: None lines
To merge this branch: bzr merge lp:~urbanape/bindwood/with-couch-libraries
Reviewer Review Type Date Requested Status
Joshua Blount (community) Approve
Stuart Langridge (community) Approve
Review via email: mp+8150@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Zachery Bir (urbanape) wrote :

We now have an extension that will propagate additions to CouchDB as well as deletions and modifications to bookmarks currently in CouchDB.

To test:

1) Ensure you have a local CouchDB running on port 5984 with a database called 'bookmarks'.

2) (Re)start Firefox with the extension in place

3) Add a bookmark

4) Verify it exists in CouchDB

5) Change the bookmark in Firefox (title or URI)

6) Verify the change was recorded (new rev #)

7) Delete the bookmark in Firefox

8) Verify the record no longer exists in CouchDB

Revision history for this message
Stuart Langridge (sil) wrote :

1. it's an extension to save your bookmarks to your desktop CouchDB, not to Ubuntu One
2. it would be useful to add some comments explaining it a bit, perhaps?
3. localhost:5984 is hardcoded (this will obviously be changed later, so it's probably OK now)

review: Approve
Revision history for this message
Joshua Blount (jblount) wrote :

This looks fine, noting that sil's comments are valid.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'chrome.manifest'
2--- chrome.manifest 2009-06-30 13:47:50 +0000
3+++ chrome.manifest 2009-07-02 13:41:07 +0000
4@@ -1,2 +1,3 @@
5-content ubuntuone-firefox-bookmark-sync chrome/content/
6-overlay chrome://browser/content/browser.xul chrome://ubuntuone-firefox-bookmark-sync/content/browserOverlay.xul
7+content bindwood chrome/content/
8+locale bindwood en chrome/locale/en/
9+overlay chrome://browser/content/browser.xul chrome://bindwood/content/browserOverlay.xul
10
11=== modified file 'chrome/content/browserOverlay.xul'
12--- chrome/content/browserOverlay.xul 2009-06-30 13:47:50 +0000
13+++ chrome/content/browserOverlay.xul 2009-07-02 15:11:14 +0000
14@@ -1,15 +1,25 @@
15 <?xml version="1.0"?>
16
17-<!DOCTYPE overlay SYSTEM "chrome://ubuntuone-firefox-bookmark-sync/locale/ubuntuone-firefox-bookmark-sync.dtd">
18+<!DOCTYPE overlay SYSTEM "chrome://bindwood/locale/en/bindwood.dtd">
19
20-<overlay id="ubuntuone-firefox-bookmark-sync"
21+<overlay id="bindwood"
22 xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
23
24+ <script type="application/x-javascript" src="chrome://bindwood/content/couch.js" />
25+ <script type="application/x-javascript" src="chrome://bindwood/content/sync.js" />
26+
27+ <window id="main-window">
28+ <script type="application/x-javascript">
29+ var bmsvc = Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"].getService(Components.interfaces.nsINavBookmarksService);
30+ window.addEventListener("load", function() { bmsvc.addObserver(BindWoodListener, false); }, false);
31+ </script>
32+ </window>
33+
34 <menupopup id="menu_ToolsPopup">
35- <menuitem id="UbuntuOneFirefoxBookmarkSync"
36+ <menuitem id="BindWoodSync"
37 label="Sync Bookmarks (well, alert)"
38 insertbefore="javascriptConsole"
39- oncommand="alert('syncing...');" />
40+ oncommand="syncBookmarks();" />
41 </menupopup>
42
43 </overlay>
44
45=== added file 'chrome/content/couch.js'
46--- chrome/content/couch.js 1970-01-01 00:00:00 +0000
47+++ chrome/content/couch.js 2009-07-02 15:11:14 +0000
48@@ -0,0 +1,395 @@
49+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
50+// use this file except in compliance with the License. You may obtain a copy
51+// of the License at
52+//
53+// http://www.apache.org/licenses/LICENSE-2.0
54+//
55+// Unless required by applicable law or agreed to in writing, software
56+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
57+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
58+// License for the specific language governing permissions and limitations under
59+// the License.
60+
61+// A simple class to represent a database. Uses XMLHttpRequest to interface with
62+// the CouchDB server.
63+
64+function CouchDB(name, httpHeaders) {
65+ this.name = name;
66+ this.uri = "/" + encodeURIComponent(name) + "/";
67+
68+ // The XMLHttpRequest object from the most recent request. Callers can
69+ // use this to check result http status and headers.
70+ this.last_req = null;
71+
72+ this.request = function(method, uri, requestOptions) {
73+ requestOptions = requestOptions || {}
74+ requestOptions.headers = combine(requestOptions.headers, httpHeaders)
75+ return CouchDB.request(method, uri, requestOptions);
76+ }
77+
78+ // Creates the database on the server
79+ this.createDb = function() {
80+ this.last_req = this.request("PUT", this.uri);
81+ CouchDB.maybeThrowError(this.last_req);
82+ return JSON.parse(this.last_req.responseText);
83+ }
84+
85+ // Deletes the database on the server
86+ this.deleteDb = function() {
87+ this.last_req = this.request("DELETE", this.uri);
88+ if (this.last_req.status == 404)
89+ return false;
90+ CouchDB.maybeThrowError(this.last_req);
91+ return JSON.parse(this.last_req.responseText);
92+ }
93+
94+ // Save a document to the database
95+ this.save = function(doc, options) {
96+ if (doc._id == undefined)
97+ doc._id = CouchDB.newUuids(1)[0];
98+
99+ this.last_req = this.request("PUT", this.uri +
100+ encodeURIComponent(doc._id) + encodeOptions(options),
101+ {body: JSON.stringify(doc)});
102+ CouchDB.maybeThrowError(this.last_req);
103+ var result = JSON.parse(this.last_req.responseText);
104+ doc._rev = result.rev;
105+ return result;
106+ }
107+
108+ // Open a document from the database
109+ this.open = function(docId, options) {
110+ this.last_req = this.request("GET", this.uri + encodeURIComponent(docId) + encodeOptions(options));
111+ if (this.last_req.status == 404)
112+ return null;
113+ CouchDB.maybeThrowError(this.last_req);
114+ return JSON.parse(this.last_req.responseText);
115+ }
116+
117+ // Deletes a document from the database
118+ this.deleteDoc = function(doc) {
119+ this.last_req = this.request("DELETE", this.uri + encodeURIComponent(doc._id) + "?rev=" + doc._rev);
120+ CouchDB.maybeThrowError(this.last_req);
121+ var result = JSON.parse(this.last_req.responseText);
122+ doc._rev = result.rev; //record rev in input document
123+ doc._deleted = true;
124+ return result;
125+ }
126+
127+ // Deletes an attachment from a document
128+ this.deleteDocAttachment = function(doc, attachment_name) {
129+ this.last_req = this.request("DELETE", this.uri + encodeURIComponent(doc._id) + "/" + attachment_name + "?rev=" + doc._rev);
130+ CouchDB.maybeThrowError(this.last_req);
131+ var result = JSON.parse(this.last_req.responseText);
132+ doc._rev = result.rev; //record rev in input document
133+ return result;
134+ }
135+
136+ this.bulkSave = function(docs, options) {
137+ // first prepoulate the UUIDs for new documents
138+ var newCount = 0
139+ for (var i=0; i<docs.length; i++) {
140+ if (docs[i]._id == undefined)
141+ newCount++;
142+ }
143+ var newUuids = CouchDB.newUuids(docs.length);
144+ var newCount = 0
145+ for (var i=0; i<docs.length; i++) {
146+ if (docs[i]._id == undefined)
147+ docs[i]._id = newUuids.pop();
148+ }
149+ var json = {"docs": docs};
150+ // put any options in the json
151+ for (var option in options) {
152+ json[option] = options[option];
153+ }
154+ this.last_req = this.request("POST", this.uri + "_bulk_docs", {
155+ body: JSON.stringify(json)
156+ });
157+ if (this.last_req.status == 417) {
158+ return {errors: JSON.parse(this.last_req.responseText)};
159+ }
160+ else {
161+ CouchDB.maybeThrowError(this.last_req);
162+ var results = JSON.parse(this.last_req.responseText);
163+ for (var i = 0; i < docs.length; i++) {
164+ if(results[i].rev)
165+ docs[i]._rev = results[i].rev;
166+ }
167+ return results;
168+ }
169+ }
170+
171+ this.ensureFullCommit = function() {
172+ this.last_req = this.request("POST", this.uri + "_ensure_full_commit");
173+ CouchDB.maybeThrowError(this.last_req);
174+ return JSON.parse(this.last_req.responseText);
175+ }
176+
177+ // Applies the map function to the contents of database and returns the results.
178+ this.query = function(mapFun, reduceFun, options, keys) {
179+ var body = {language: "javascript"};
180+ if(keys) {
181+ body.keys = keys ;
182+ }
183+ if (typeof(mapFun) != "string")
184+ mapFun = mapFun.toSource ? mapFun.toSource() : "(" + mapFun.toString() + ")";
185+ body.map = mapFun;
186+ if (reduceFun != null) {
187+ if (typeof(reduceFun) != "string")
188+ reduceFun = reduceFun.toSource ? reduceFun.toSource() : "(" + reduceFun.toString() + ")";
189+ body.reduce = reduceFun;
190+ }
191+ if (options && options.options != undefined) {
192+ body.options = options.options;
193+ delete options.options;
194+ }
195+ this.last_req = this.request("POST", this.uri + "_temp_view" + encodeOptions(options), {
196+ headers: {"Content-Type": "application/json"},
197+ body: JSON.stringify(body)
198+ });
199+ CouchDB.maybeThrowError(this.last_req);
200+ return JSON.parse(this.last_req.responseText);
201+ }
202+
203+ this.view = function(viewname, options, keys) {
204+ var viewParts = viewname.split('/');
205+ var viewPath = this.uri + "_design/" + viewParts[0] + "/_view/"
206+ + viewParts[1] + encodeOptions(options);
207+ if(!keys) {
208+ this.last_req = this.request("GET", viewPath);
209+ } else {
210+ this.last_req = this.request("POST", viewPath, {
211+ headers: {"Content-Type": "application/json"},
212+ body: JSON.stringify({keys:keys})
213+ });
214+ }
215+ if (this.last_req.status == 404)
216+ return null;
217+ CouchDB.maybeThrowError(this.last_req);
218+ return JSON.parse(this.last_req.responseText);
219+ }
220+
221+ // gets information about the database
222+ this.info = function() {
223+ this.last_req = this.request("GET", this.uri);
224+ CouchDB.maybeThrowError(this.last_req);
225+ return JSON.parse(this.last_req.responseText);
226+ }
227+
228+ this.allDocs = function(options,keys) {
229+ if(!keys) {
230+ this.last_req = this.request("GET", this.uri + "_all_docs" + encodeOptions(options));
231+ } else {
232+ this.last_req = this.request("POST", this.uri + "_all_docs" + encodeOptions(options), {
233+ headers: {"Content-Type": "application/json"},
234+ body: JSON.stringify({keys:keys})
235+ });
236+ }
237+ CouchDB.maybeThrowError(this.last_req);
238+ return JSON.parse(this.last_req.responseText);
239+ }
240+
241+ this.designDocs = function() {
242+ return this.allDocs({startkey:"_design", endkey:"_design0"});
243+ };
244+
245+ this.allDocsBySeq = function(options,keys) {
246+ var req = null;
247+ if(!keys) {
248+ req = this.request("GET", this.uri + "_all_docs_by_seq" + encodeOptions(options));
249+ } else {
250+ req = this.request("POST", this.uri + "_all_docs_by_seq" + encodeOptions(options), {
251+ headers: {"Content-Type": "application/json"},
252+ body: JSON.stringify({keys:keys})
253+ });
254+ }
255+ CouchDB.maybeThrowError(req);
256+ return JSON.parse(req.responseText);
257+ }
258+
259+ this.compact = function() {
260+ this.last_req = this.request("POST", this.uri + "_compact");
261+ CouchDB.maybeThrowError(this.last_req);
262+ return JSON.parse(this.last_req.responseText);
263+ }
264+
265+ this.setDbProperty = function(propId, propValue) {
266+ this.last_req = this.request("PUT", this.uri + propId,{
267+ body:JSON.stringify(propValue)
268+ });
269+ CouchDB.maybeThrowError(this.last_req);
270+ return JSON.parse(this.last_req.responseText);
271+ }
272+
273+ this.getDbProperty = function(propId) {
274+ this.last_req = this.request("GET", this.uri + propId);
275+ CouchDB.maybeThrowError(this.last_req);
276+ return JSON.parse(this.last_req.responseText);
277+ }
278+
279+ this.setAdmins = function(adminsArray) {
280+ this.last_req = this.request("PUT", this.uri + "_admins",{
281+ body:JSON.stringify(adminsArray)
282+ });
283+ CouchDB.maybeThrowError(this.last_req);
284+ return JSON.parse(this.last_req.responseText);
285+ }
286+
287+ this.getAdmins = function() {
288+ this.last_req = this.request("GET", this.uri + "_admins");
289+ CouchDB.maybeThrowError(this.last_req);
290+ return JSON.parse(this.last_req.responseText);
291+ }
292+
293+ // Convert a options object to an url query string.
294+ // ex: {key:'value',key2:'value2'} becomes '?key="value"&key2="value2"'
295+ function encodeOptions(options) {
296+ var buf = []
297+ if (typeof(options) == "object" && options !== null) {
298+ for (var name in options) {
299+ if (!options.hasOwnProperty(name)) continue;
300+ var value = options[name];
301+ if (name == "key" || name == "startkey" || name == "endkey") {
302+ value = toJSON(value);
303+ }
304+ buf.push(encodeURIComponent(name) + "=" + encodeURIComponent(value));
305+ }
306+ }
307+ if (!buf.length) {
308+ return "";
309+ }
310+ return "?" + buf.join("&");
311+ }
312+
313+ function toJSON(obj) {
314+ return obj !== null ? JSON.stringify(obj) : null;
315+ }
316+
317+ function combine(object1, object2) {
318+ if (!object2)
319+ return object1;
320+ if (!object1)
321+ return object2;
322+
323+ for (var name in object2)
324+ object1[name] = object2[name];
325+
326+ return object1;
327+ }
328+
329+
330+}
331+
332+// this is the XMLHttpRequest object from last request made by the following
333+// CouchDB.* functions (except for calls to request itself).
334+// Use this from callers to check HTTP status or header values of requests.
335+CouchDB.last_req = null;
336+
337+
338+CouchDB.allDbs = function() {
339+ CouchDB.last_req = CouchDB.request("GET", "/_all_dbs");
340+ CouchDB.maybeThrowError(CouchDB.last_req);
341+ return JSON.parse(CouchDB.last_req.responseText);
342+}
343+
344+CouchDB.allDesignDocs = function() {
345+ var ddocs = {}, dbs = CouchDB.allDbs();
346+ for (var i=0; i < dbs.length; i++) {
347+ var db = new CouchDB(dbs[i]);
348+ ddocs[dbs[i]] = db.designDocs();
349+ };
350+ return ddocs;
351+};
352+
353+CouchDB.getVersion = function() {
354+ CouchDB.last_req = CouchDB.request("GET", "/");
355+ CouchDB.maybeThrowError(CouchDB.last_req);
356+ return JSON.parse(CouchDB.last_req.responseText).version;
357+}
358+
359+CouchDB.replicate = function(source, target, rep_options) {
360+ rep_options = rep_options || {};
361+ var headers = rep_options.headers || {};
362+ CouchDB.last_req = CouchDB.request("POST", "/_replicate", {
363+ headers: headers,
364+ body: JSON.stringify({source: source, target: target})
365+ });
366+ CouchDB.maybeThrowError(CouchDB.last_req);
367+ return JSON.parse(CouchDB.last_req.responseText);
368+}
369+
370+CouchDB.request = function(method, uri, options) {
371+ options = options || {};
372+ var req = null;
373+ if (typeof(XMLHttpRequest) != "undefined") {
374+ req = new XMLHttpRequest();
375+ } else if (typeof(ActiveXObject) != "undefined") {
376+ req = new ActiveXObject("Microsoft.XMLHTTP");
377+ } else {
378+ throw new Error("No XMLHTTPRequest support detected");
379+ }
380+ req.open(method, "http://localhost:5984" + uri, false);
381+ if (options.headers) {
382+ var headers = options.headers;
383+ for (var headerName in headers) {
384+ if (!headers.hasOwnProperty(headerName)) continue;
385+ req.setRequestHeader(headerName, headers[headerName]);
386+ }
387+ }
388+ req.send(options.body || "");
389+ return req;
390+}
391+
392+CouchDB.requestStats = function(module, key, test) {
393+ var query_arg = "";
394+ if(test !== null) {
395+ query_arg = "?flush=true";
396+ }
397+
398+ var stat = CouchDB.request("GET", "/_stats/" + module + "/" + key + query_arg).responseText;
399+ return JSON.parse(stat)[module][key];
400+}
401+
402+CouchDB.uuids_cache = [];
403+
404+CouchDB.newUuids = function(n) {
405+ if (CouchDB.uuids_cache.length >= n) {
406+ var uuids = CouchDB.uuids_cache.slice(CouchDB.uuids_cache.length - n);
407+ if(CouchDB.uuids_cache.length - n == 0) {
408+ CouchDB.uuids_cache = [];
409+ } else {
410+ CouchDB.uuids_cache =
411+ CouchDB.uuids_cache.slice(0, CouchDB.uuids_cache.length - n);
412+ }
413+ return uuids;
414+ } else {
415+ CouchDB.last_req = CouchDB.request("GET", "/_uuids?count=" + (100 + n));
416+ CouchDB.maybeThrowError(CouchDB.last_req);
417+ var result = JSON.parse(CouchDB.last_req.responseText);
418+ CouchDB.uuids_cache =
419+ CouchDB.uuids_cache.concat(result.uuids.slice(0, 100));
420+ return result.uuids.slice(100);
421+ }
422+}
423+
424+CouchDB.maybeThrowError = function(req) {
425+ if (req.status >= 400) {
426+ try {
427+ var result = JSON.parse(req.responseText);
428+ } catch (ParseError) {
429+ var result = {error:"unknown", reason:req.responseText};
430+ }
431+ throw result;
432+ }
433+}
434+
435+CouchDB.params = function(options) {
436+ options = options || {};
437+ var returnArray = [];
438+ for(var key in options) {
439+ var value = options[key];
440+ returnArray.push(key + "=" + value);
441+ }
442+ return returnArray.join("&");
443+}
444\ No newline at end of file
445
446=== added file 'chrome/content/sync.js'
447--- chrome/content/sync.js 1970-01-01 00:00:00 +0000
448+++ chrome/content/sync.js 2009-07-02 17:56:29 +0000
449@@ -0,0 +1,68 @@
450+function _writeMessage(aMessage) {
451+ var consoleService = Components.classes["@mozilla.org/consoleservice;1"]
452+ .getService(Components.interfaces.nsIConsoleService);
453+ consoleService.logStringMessage("BindWood: " + aMessage);
454+}
455+
456+function syncBookmarks() {
457+ alert('Syncing bookmarks...');
458+ var couch = new CouchDB('bookmarks');
459+ var total_documents = couch.allDocs().total_rows;
460+ var all_documents = new Array();
461+ if (total_documents > 0) {
462+ all_documents = dc.all('bookmarks').rows;
463+ }
464+
465+ for (var i = 0; i < all_documents.length; i++) {
466+ alert(all_documents[i].id);
467+ }
468+}
469+
470+// An nsINavBookmarkObserver
471+var BindWoodListener = {
472+ onBeginUpdateBatch: function() {},
473+ onEndUpdateBatch: function() {},
474+ onItemAdded: function(aItemId, aFolder, aIndex) {
475+ netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead UniversalBrowserWrite");
476+ var couch = new CouchDB('bookmarks');
477+
478+ var doc = {_id: aItemId.toString()};
479+ try {
480+ var result = couch.save(doc);
481+ _writeMessage("Saved record: " + result.rev);
482+ } catch(e) {
483+ _writeMessage("Error '" + e.error + "': " + e.reason);
484+ }
485+ return true;
486+ },
487+ onItemRemoved: function(aItemId, aFolder, aIndex) {
488+ netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead UniversalBrowserWrite");
489+ var couch = new CouchDB('bookmarks');
490+
491+ var doc = couch.open(aItemId.toString());
492+ try {
493+ var result = couch.deleteDoc(doc);
494+ _writeMessage("Deleted record: " + result.rev);
495+ } catch(e) {
496+ _writeMessage("Error '" + e.error + "': " + e.reason);
497+ }
498+ return true;
499+ },
500+ onItemChanged: function(aBookmarkId, aProperty, aIsAnnotationProperty, aValue) {
501+ netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead UniversalBrowserWrite");
502+ var couch = new CouchDB('bookmarks');
503+
504+ var doc = couch.open(aBookmarkId.toString());
505+ doc[aProperty.toString()] = aValue.toString();
506+ try {
507+ var result = couch.save(doc);
508+ _writeMessage("Updated record: " + result.rev + ", setting (" + aProperty.toString() + ") to (" + aValue.toString() + ")");
509+ } catch(e) {
510+ _writeMessage("Error '" + e.error + "': " + e.reason);
511+ }
512+ return true;
513+ },
514+ onItemVisited: function(aBookmarkId, aVisitID, time) {},
515+ onItemMoved: function(aItemId, aOldParent, aOldIndex, aNewParent, aNewIndex) {},
516+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsINavBookmarkObserver])
517+};
518
519=== added file 'chrome/content/tutorial.html'
520--- chrome/content/tutorial.html 1970-01-01 00:00:00 +0000
521+++ chrome/content/tutorial.html 2009-07-02 13:41:07 +0000
522@@ -0,0 +1,8 @@
523+<html>
524+ <head>
525+ <title>BindWood Tutorial</title>
526+ </head>
527+ <body>
528+ This is BindWood.
529+ </body>
530+</html>
531
532=== added directory 'chrome/locale'
533=== added directory 'chrome/locale/en'
534=== added file 'chrome/locale/en/bindwood.dtd'
535=== modified file 'install.rdf'
536--- install.rdf 2009-06-30 13:47:50 +0000
537+++ install.rdf 2009-07-02 13:41:07 +0000
538@@ -3,7 +3,7 @@
539 xmlns:em="http://www.mozilla.org/2004/em-rdf#">
540
541 <Description about="urn:mozilla:install-manifest">
542- <em:id>ubuntuone-firefox-bookmark-sync@ubuntu.com</em:id>
543+ <em:id>bindwood@ubuntu.com</em:id>
544 <em:version>1.0</em:version>
545 <em:type>2</em:type>
546
547@@ -18,7 +18,7 @@
548 </em:targetApplication>
549
550 <!-- Front End MetaData -->
551- <em:name>UbuntuOne Bookmarks Sync</em:name>
552+ <em:name>BindWood</em:name>
553 <em:description>An extension to synchronize your bookmarks to UbuntuOne.</em:description>
554 <em:creator>Zachery Bir</em:creator>
555 <em:homepageURL>http://launchpad.net/ubuntuone-firefox-bookmark-sync</em:homepageURL>

Subscribers

People subscribed via source and target branches

to all changes: