~urbanape/bindwood/batch-pushing

« back to all changes in this revision

Viewing changes to chrome/content/sync.js

  • Committer: Zachery Bir
  • Date: 2009-07-10 13:12:57 UTC
  • mfrom: (1.2.12 bindwood)
  • Revision ID: zbir@howler.local-20090710131257-zovw7w2y6abzo66l
Merged in dbus-and-local-uuids branch

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/* Lots and lots of debugging information */
 
2
var Bindwood = {
 
3
    bookmarksService: Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"]
 
4
        .getService(Components.interfaces.nsINavBookmarksService),
 
5
    uuidService: Components.classes["@mozilla.org/uuid-generator;1"]
 
6
        .getService(Components.interfaces.nsIUUIDGenerator),
 
7
    annotationService: Components.classes["@mozilla.org/browser/annotation-service;1"]
 
8
        .getService(Components.interfaces.nsIAnnotationService),
 
9
    consoleService: Components.classes["@mozilla.org/consoleservice;1"]
 
10
        .getService(Components.interfaces.nsIConsoleService),
 
11
    historyService: Components.classes["@mozilla.org/browser/nav-history-service;1"]
 
12
        .getService(Components.interfaces.nsINavHistoryService),
 
13
    ioService: Components.classes["@mozilla.org/network/io-service;1"]
 
14
        .getService(Components.interfaces.nsIIOService),
 
15
 
 
16
    annotationKey: "bindwood/uuid",
 
17
    uuidItemIdMap: {},
 
18
 
 
19
    generateUUIDString: function() {
 
20
        return Bindwood.uuidService.generateUUID().toString();
 
21
    },
 
22
 
 
23
    annotateItemWithUUID: function(itemId) {
 
24
        var uuid = Bindwood.generateUUIDString();
 
25
        Bindwood.annotationService.setItemAnnotation(itemId, Bindwood.annotationKey, uuid, 0, Bindwood.annotationService.EXPIRE_NEVER);
 
26
        // Whenever we create a new UUID, stash it and the itemId in
 
27
        // our local cache.
 
28
        Bindwood.uuidItemIdMap[uuid] = itemId;
 
29
        return uuid;
 
30
    },
 
31
 
 
32
    itemIdForUUID: function(uuid) {
 
33
        // First, try to look it up in our local cache, barring that
 
34
        // (which shouldn't happen), look it up slowly.
 
35
        var itemId = Bindwood.uuidItemIdMap[uuid];
 
36
 
 
37
        if (!itemId) {
 
38
            var items = Bindwood.annotationService.getItemsWithAnnotation(Bindwood.annotationKey, {});
 
39
            for (var i = 0; i < items.length; i++) {
 
40
                if (Bindwood.annotationService.getItemAnnotation(items[i], Bindwood.annotationKey) == uuid) {
 
41
                    Bindwood.uuidItemIdMap[uuid] = itemId = items[i];
 
42
                    break;
 
43
                }
 
44
            }
 
45
        }
 
46
        return itemId;
 
47
    },
 
48
 
 
49
    uuidForItemId: function(itemId) {
 
50
        // Try to look up the uuid, and failing that, assign a new one
 
51
        // and return it.
 
52
        var uuid;
 
53
        var found = false;
 
54
        try {
 
55
            uuid = Bindwood.annotationService.getItemAnnotation(itemId, Bindwood.annotationKey);
 
56
            found = true;
 
57
        } catch(e) {
 
58
            uuid = Bindwood.annotateItemWithUUID(itemId);
 
59
        }
 
60
 
 
61
        return uuid;
 
62
    },
 
63
 
 
64
    writeMessage: function(aMessage) {
 
65
        // convenience method for logging. Way better than alert()s.
 
66
        Bindwood.consoleService.logStringMessage("Bindwood: " + aMessage);
 
67
    },
 
68
 
 
69
    writeError: function(aMessage, e) {
 
70
        Bindwood.writeMessage(aMessage + "message: '" + e.message + "', reason: '" + e.reason + "', description: '" + e.description + "', error: '" + e.error + "'");
 
71
    },
 
72
 
 
73
 
 
74
 
 
75
    init: function() {
 
76
        // Start the process and de-register ourself
 
77
        // http://forums.mozillazine.org/viewtopic.php?f=19&t=657911&start=0
 
78
        // It ensures that we're only running that code on the first window.
 
79
 
 
80
        if(Components.classes["@mozilla.org/appshell/window-mediator;1"]
 
81
           .getService(Components.interfaces.nsIWindowMediator)
 
82
           .getEnumerator("").getNext() == window) {
 
83
            Bindwood.getCouchPortNumber(Bindwood.startProcess);
 
84
        }
 
85
    },
 
86
 
 
87
    getCouchPortNumber: function(continueFunction) {
 
88
        // find the desktop Couch port number by making a D-Bus call
 
89
        // we call D-Bus by shelling out to a bash script which calls
 
90
        // it for us, and writes the port number into a temp file
 
91
 
 
92
        // find OS temp dir to put the tempfile in
 
93
        // https://developer.mozilla.org/index.php?title=File_I%2F%2FO#Getting_special_files
 
94
        var tmpdir = Components.classes["@mozilla.org/file/directory_service;1"]
 
95
                     .getService(Components.interfaces.nsIProperties)
 
96
                     .get("TmpD", Components.interfaces.nsIFile);
 
97
        // create a randomly named tempfile in the tempdir
 
98
        var tmpfile = Components.classes["@mozilla.org/file/local;1"]
 
99
                     .createInstance(Components.interfaces.nsILocalFile);
 
100
        tmpfile.initWithPath(tmpdir.path + "/desktopcouch." + Math.random());
 
101
        tmpfile.createUnique(tmpfile.NORMAL_FILE_TYPE, 0600);
 
102
        Bindwood.writeMessage("Tempfile for Couch port number: " + tmpfile.path);
 
103
 
 
104
        // find the D-Bus bash script, which is in our extension folder
 
105
        var MY_ID = "bindwood@ubuntu.com";
 
106
        var em = Components.classes["@mozilla.org/extensions/manager;1"].
 
107
            getService(Components.interfaces.nsIExtensionManager);
 
108
        var dbus_script = em.getInstallLocation(MY_ID).getItemFile(MY_ID,
 
109
            "chrome/content/dbus.sh");
 
110
        // create an nsILocalFile for the executable
 
111
        var nsifile = Components.classes["@mozilla.org/file/local;1"]
 
112
                     .createInstance(Components.interfaces.nsILocalFile);
 
113
        nsifile.initWithPath(dbus_script.path);
 
114
 
 
115
        // create an nsIProcess2 to execute this bash script
 
116
        var process = Components.classes["@mozilla.org/process/util;1"]
 
117
                        .createInstance(Components.interfaces.nsIProcess2);
 
118
        process.init(nsifile);
 
119
 
 
120
        // Run the process, passing the tmpfile path
 
121
        var args = [tmpfile.path];
 
122
        process.runAsync(args, args.length, {
 
123
            observe: function(process, finishState, data) {
 
124
                var port = 5984;
 
125
                if (finishState == "process-finished") {
 
126
                    // read temp file to find port number
 
127
                    // https://developer.mozilla.org/en/Code_snippets/File_I%2f%2fO#Reading_from_a_file
 
128
                    var data = "";
 
129
                    var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].
 
130
                        createInstance(Components.interfaces.nsIFileInputStream);
 
131
                    var cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
 
132
                        createInstance(Components.interfaces.nsIConverterInputStream);
 
133
                    fstream.init(tmpfile, -1, 0, 0);
 
134
                    cstream.init(fstream, "UTF-8", 0, 0);
 
135
                    let (str = {}) {
 
136
                      cstream.readString(-1, str); // read the whole file and put it in str.value
 
137
                      data = str.value;
 
138
                    }
 
139
                    cstream.close(); // this closes fstream
 
140
                    data = data.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
 
141
                    if (/^[0-9]+$/.test(data)) {
 
142
                        port = data;
 
143
                    } else {
 
144
                        Bindwood.writeMessage("D-Bus port data is not a number (" + data + ")");
 
145
                    }
 
146
                } else {
 
147
                    // fall back to system CouchDB
 
148
                    Bindwood.writeMessage("D-Bus port find failed");
 
149
                }
 
150
                tmpfile.remove(false);
 
151
                continueFunction(port);
 
152
        }
 
153
        });
 
154
    },
 
155
 
 
156
    startProcess: function(couchPortNumber) {
 
157
        Bindwood.writeMessage("Starting process with Couch on port " + couchPortNumber);
 
158
        CouchDB.PORT_NUMBER = couchPortNumber;
 
159
        try {
 
160
            Bindwood.pushBookmarks();
 
161
        } catch(e) {
 
162
            Bindwood.writeError("Error when calling pushBookmarks: ", e);
 
163
        }
 
164
        Bindwood.createView();
 
165
        Bindwood.pullBookmarks();
 
166
    },
 
167
 
 
168
    createView: function() {
 
169
        var DDID = "_design/views";
 
170
        var couch = new CouchDB('bookmarks');
 
171
        var current_doc;
 
172
        try {
 
173
            current_doc = couch.open(DDID);
 
174
            if (current_doc !== null) {
 
175
                Bindwood.writeMessage("View already exists; leaving it alone");
 
176
            } else {
 
177
                new_doc = {
 
178
                    _id: DDID,
 
179
                    views: {
 
180
                        display: {
 
181
                         map: "function(doc) { " +
 
182
                           "var scheme = doc.uri.split(':',1)[0]; " +
 
183
                           "var uri; " +
 
184
                           "if (scheme == 'http' || scheme == 'https') {" +
 
185
                             "uri = doc.uri.split('/')[2];" +
 
186
                             "if (uri.length < 30) {" +
 
187
                               " uri += '/' + " +
 
188
                               "doc.uri.split('/',4)[3].substr(0,30-uri.length) + '...';" +
 
189
                             "}" +
 
190
                           "} else {" +
 
191
                           "uri = scheme + ' URL';" +
 
192
                           "}" +
 
193
                           "emit(doc.title, uri);" +
 
194
                         "}"
 
195
                       }
 
196
                    }
 
197
                };
 
198
                try {
 
199
                    couch.save(new_doc);
 
200
                } catch(e) {
 
201
                    Bindwood.writeError("Problem saving view: ", e);
 
202
                }
 
203
            }
 
204
        } catch(e) {
 
205
            // some kind of error fetching the existing design doc
 
206
            Bindwood.writeError("Problem checking for view: ", e);
 
207
        }
 
208
    },
 
209
 
 
210
    pushBookmarks: function() {
 
211
        // Prime the pump, so to speak, by uploading all our local
 
212
        // bookmarks to CouchDB (if they're not there already).
 
213
        var couch = new CouchDB('bookmarks');
 
214
        // Create the DB if it doesn't already exist
 
215
        try {
 
216
            couch.createDb();
 
217
        } catch (e) {
 
218
            Bindwood.writeError("Error when creating database in pushBookmarks (file_exists is OK here): ", e);
 
219
        }
 
220
 
 
221
        Bindwood.pushBookmarksFromList(Bindwood.bookmarksService.toolbarFolder,
 
222
            "toolbarFolder", couch);
 
223
        Bindwood.pushBookmarksFromList(Bindwood.bookmarksService.bookmarksMenuFolder,
 
224
            "bookmarksMenuFolder", couch);
 
225
    },
 
226
 
 
227
    getBookmarksFromList: function(bookmarksList) {
 
228
        var retval = [];
 
229
        var options = Bindwood.historyService.getNewQueryOptions();
 
230
        var query = Bindwood.historyService.getNewQuery();
 
231
        query.setFolders([bookmarksList], 1);
 
232
        var result = Bindwood.historyService.executeQuery(query, options);
 
233
        var rootNode = result.root;
 
234
        rootNode.containerOpen = true;
 
235
        for (var i=0; i<rootNode.childCount; i++) {
 
236
            var node = rootNode.getChild(i);
 
237
            if (Bindwood.bookmarksService.getItemType(node.itemId) !=
 
238
                Bindwood.bookmarksService.TYPE_BOOKMARK) {
 
239
                continue;
 
240
            }
 
241
 
 
242
            var title = Bindwood.bookmarksService.getItemTitle(node.itemId);
 
243
            try {
 
244
                var metadata = Bindwood.bookmarksService.getBookmarkURI(node.itemId);
 
245
            } catch(e) {
 
246
                Bindwood.writeError("problem fetching metadata for bookmark '" + title + "': ", e);
 
247
                continue;
 
248
            }
 
249
            var uuid = Bindwood.uuidForItemId(node.itemId);
 
250
            retval.push({
 
251
                title: title,
 
252
                metadata: metadata,
 
253
                uuid: uuid,
 
254
                id: node.itemId});
 
255
        }
 
256
        rootNode.containerOpen = false;
 
257
        return retval;
 
258
    },
 
259
 
 
260
    pushBookmarksFromList: function(bookmarksList, bookmarksListName, db) {
 
261
        var bookmarkData = Bindwood.getBookmarksFromList(bookmarksList);
 
262
        for (var i = 0; i < bookmarkData.length; i++) {
 
263
            // find this bookmark in CouchDB
 
264
            var uuid = Bindwood.uuidForItemId(bookmarkData[i].id);
 
265
            var uri =  bookmarkData[i].metadata.spec;
 
266
            var title = bookmarkData[i].title;
 
267
 
 
268
            var results = db.query(function(doc) {
 
269
                if (doc.application_annotations &&
 
270
                    doc.application_annotations.Firefox &&
 
271
                    doc.application_annotations.Firefox.uuid) {
 
272
                    emit(doc.application_annotations.Firefox.uuid, doc);
 
273
                }
 
274
            }, null, {
 
275
                startkey: uuid, endkey: uuid
 
276
            });
 
277
 
 
278
            if (results.rows.length === 0) {
 
279
                // this bookmark is not in CouchDB, so write it
 
280
                var record = {
 
281
                    record_type: "http://example.com/bookmark",
 
282
                    uri: uri,
 
283
                    title: title,
 
284
                    application_annotations: {
 
285
                        Firefox: {
 
286
                            uuid: uuid,
 
287
                            list: bookmarksListName
 
288
                        }
 
289
                    }
 
290
                };
 
291
                try {
 
292
                    db.save(record);
 
293
                } catch(e) {
 
294
                    Bindwood.writeError("Problem saving bookmark to CouchDB; bookmark is " + JSON.stringify(record) + ": ", e);
 
295
                }
 
296
            } else {
 
297
                // bookmark is already in CouchDB, so do nothing
 
298
            }
 
299
        }
 
300
    },
 
301
 
 
302
    pullBookmarks: function() {
 
303
        var couch = new CouchDB('bookmarks');
 
304
        // Fetch all bookmark documents from the database
 
305
        // the query function is evaluated by Couch, which doesn't know
 
306
        // what Bindwood.RECORD_TYPE is, so we string-encode it first to
 
307
        // include the literal value
 
308
        try {
 
309
            var rows = couch.query(function (doc) {
 
310
                if (doc.record_type == "http://example.com/bookmark") {
 
311
                    emit(doc._id,doc);
 
312
                }
 
313
            });
 
314
        } catch(e) {
 
315
            Bindwood.writeError("Problem fetching all bookmarks from Couch:", e);
 
316
        }
 
317
        for (var i = 0; i < rows.rows.length; i++) {
 
318
            var recordid = rows.rows[i].id;
 
319
            var bm = rows.rows[i].value;
 
320
            if (bm.application_annotations &&
 
321
                bm.application_annotations.Firefox &&
 
322
                bm.application_annotations.Firefox.uuid) {
 
323
                // this bookmark has a uuid, so check its values haven't changed
 
324
                // find the bookmark with this uuid
 
325
                var itemId = Bindwood.itemIdForUUID(
 
326
                    bm.application_annotations.Firefox.uuid);
 
327
                if (!itemId) {
 
328
                    // This bookmark has a uuid, but it's not one of ours.
 
329
                    // We need to work out whether (a) it's the same as one
 
330
                    //   of ours but with a different uuid (so we need to
 
331
                    //   make the uuids the same), or (b) it's a new one
 
332
                    //   that happens to have been created on a different
 
333
                    //   machine.
 
334
                    // For the moment, punt on this.
 
335
                } else {
 
336
                    var title = Bindwood.bookmarksService.getItemTitle(itemId);
 
337
                    var metadata = Bindwood.bookmarksService.getBookmarkURI(itemId);
 
338
                    if (title != bm.title) {
 
339
                        Bindwood.bookmarksService.setItemTitle(itemId, bm.title);
 
340
                    }
 
341
                    if (metadata.spec != bm.uri) {
 
342
                        try {
 
343
                            metadata = Bindwood.ioService.newURI(bm.uri, null, null);
 
344
                        } catch(e) {
 
345
                            Bindwood.writeError("Problem creating a new URI for bookmark", e);
 
346
                        }
 
347
                        Bindwood.bookmarksService.changeBookmarkURI(itemId, metadata);
 
348
                    }
 
349
                }
 
350
            } else {
 
351
                // this bookmark has no uuid, so create it from fresh in Firefox
 
352
                var list;
 
353
                if (bm.application_annotations &&
 
354
                    bm.application_annotations.Firefox &&
 
355
                    bm.application_annotations.Firefox.list) {
 
356
                    switch (bm.application_annotations.Firefox.list) {
 
357
                        case "toolbarFolder":
 
358
                            list = Bindwood.bookmarksService.toolbarFolder;
 
359
                            break;
 
360
                        case "bookmarksMenuFolder":
 
361
                            list = Bindwood.bookmarksService.bookmarksMenuFolder;
 
362
                            break;
 
363
                        default:
 
364
                            list = Bindwood.bookmarksService.toolbarFolder;
 
365
                            break;
 
366
                    }
 
367
                } else {
 
368
                    list = Bindwood.bookmarksService.toolbarFolder;
 
369
                }
 
370
                var metadata = Bindwood.ioService.newURI(bm.uri, null, null);
 
371
 
 
372
                var itemId = Bindwood.bookmarksService.insertBookmark(list,
 
373
                    metadata, -1, bm.title);
 
374
                // and then write the new uuid back to the record
 
375
                var uuid = Bindwood.uuidForItemId(itemId);
 
376
                var doc = couch.open(recordid);
 
377
                if (!doc.application_annotations) {
 
378
                    doc.application_annotations = {};
 
379
                }
 
380
                if (!doc.application_annotations.Firefox) {
 
381
                    doc.application_annotations.Firefox = {};
 
382
                }
 
383
                doc.application_annotations.Firefox.uuid = uuid;
 
384
                try {
 
385
                    couch.save(doc);
 
386
                } catch(e) {
 
387
                    Bindwood.writeError("Problem writing record for new bookmark: ",e);
 
388
                }
 
389
            }
 
390
        }
 
391
        // reschedule ourself
 
392
        setTimeout(Bindwood.pullBookmarks, 30000);
 
393
    },
 
394
 
 
395
    Observer: {
 
396
        // An nsINavBookmarkObserver
 
397
        onItemAdded: function(aItemId, aFolder, aIndex) {
 
398
            // A bookmark has been added, so we create a blank entry
 
399
            // in Couch with our local itemId attached.
 
400
            netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead UniversalBrowserWrite");
 
401
 
 
402
            var couch = new CouchDB('bookmarks');
 
403
 
 
404
            var uuid = Bindwood.uuidForItemId(aItemId);
 
405
 
 
406
            var list;
 
407
            switch(aFolder) {
 
408
                case Bindwood.bookmarksService.toolbarFolder:
 
409
                    list = "toolbarFolder";
 
410
                    break;
 
411
                case Bindwood.bookmarksService.bookmarksMenuFolder:
 
412
                    list = "bookmarksMenuFolder";
 
413
                    break;
 
414
                default:
 
415
                    list = "toolbarFolder";
 
416
                    break;
 
417
            }
 
418
 
 
419
            var doc = {
 
420
                record_type: "http://example.com/bookmark",
 
421
                application_annotations: {
 
422
                    Firefox: {
 
423
                        uuid: uuid,
 
424
                        list: list
 
425
                    }
 
426
                }
 
427
            };
 
428
 
 
429
            try {
 
430
                var result = couch.save(doc);
 
431
            } catch(e) {
 
432
                Bindwood.writeError("Problem saving new bookmark to Couch", e);
 
433
            }
 
434
        },
 
435
        onBeforeItemRemoved: function(aItemId) {
 
436
            // A bookmark has been removed. This is called before it's
 
437
            // been removed locally, though we're passed the itemId,
 
438
            // which we use to delete from Couch.
 
439
            netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead UniversalBrowserWrite");
 
440
 
 
441
            var couch = new CouchDB('bookmarks');
 
442
 
 
443
            var uuid = Bindwood.uuidForItemId(aItemId);
 
444
 
 
445
            var results = couch.query(function(doc) {
 
446
                if (doc.application_annotations &&
 
447
                    doc.application_annotations.Firefox &&
 
448
                    doc.application_annotations.Firefox.uuid) {
 
449
                    emit(doc.application_annotations.Firefox.uuid, doc);
 
450
                }
 
451
            }, null, {
 
452
                startkey: uuid, endkey: uuid
 
453
            });
 
454
 
 
455
            if (results.rows.length === 0) {
 
456
                Bindwood.writeMessage("A bookmark was deleted, but this bookmark isn't in CouchDB. This isn't supposed to happen.");
 
457
                return;
 
458
            }
 
459
 
 
460
            var doc = couch.open(results.rows[0].id);
 
461
 
 
462
            try {
 
463
                var result = couch.deleteDoc(doc);
 
464
                // Also remove from our local cache and remove
 
465
                // annotation from service.
 
466
                delete Bindwood.uuidItemIdMap[uuid];
 
467
            } catch(e) {
 
468
                Bindwood.writeError("Problem deleting record from Couch", e);
 
469
            }
 
470
        },
 
471
        onItemRemoved: function(aItemId, aFolder, aIndex) {
 
472
            Bindwood.annotationService.removeItemAnnotation(aItemId, Bindwood.annotationKey);
 
473
        },
 
474
        onItemChanged: function(aBookmarkId, aProperty, aIsAnnotationProperty, aValue) {
 
475
            // A property of a bookmark has changed. On multiple
 
476
            // property updates, this will be called multiple times,
 
477
            // once per property (i.e., for title and URI)
 
478
            netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead UniversalBrowserWrite");
 
479
 
 
480
            var couch = new CouchDB('bookmarks');
 
481
 
 
482
            var uuid = Bindwood.uuidForItemId(aBookmarkId);
 
483
 
 
484
            var results = couch.query(function(doc) {
 
485
                if (doc.application_annotations &&
 
486
                    doc.application_annotations.Firefox &&
 
487
                    doc.application_annotations.Firefox.uuid) {
 
488
                    emit(doc.application_annotations.Firefox.uuid, doc);
 
489
                }
 
490
            }, null, {
 
491
                startkey: uuid, endkey: uuid
 
492
            });
 
493
 
 
494
            if (results.rows.length === 0) {
 
495
                Bindwood.writeMessage("a bookmark has changed, but this bookmark isn't in CouchDB. this isn't supposed to happen.");
 
496
                return;
 
497
            }
 
498
 
 
499
            var doc = couch.open(results.rows[0].id);
 
500
            doc[aProperty.toString()] = aValue.toString();
 
501
 
 
502
            try {
 
503
                var result = couch.save(doc);
 
504
            } catch(e) {
 
505
                Bindwood.writeError("Problem saving updated bookmark to Couch", e);
 
506
            }
 
507
        },
 
508
 
 
509
        onBeginUpdateBatch: function() {},
 
510
        onEndUpdateBatch: function() {},
 
511
        onItemVisited: function(aBookmarkId, aVisitID, time) {},
 
512
        onItemMoved: function(aItemId, aOldParent, aOldIndex, aNewParent, aNewIndex) {},
 
513
        QueryInterface: function(iid) {
 
514
            if (iid.equals(Components.interfaces.nsINavBookmarkObserver) ||
 
515
                iid.equals(Components.interfaces.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS) ||
 
516
                iid.equals(Components.interfaces.nsISupports)) {
 
517
                return this;
 
518
            }
 
519
            throw Cr.NS_ERROR_NO_INTERFACE;
 
520
        }
 
521
    }
 
522
};