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

Subscribers

People subscribed via source and target branches

to all changes: