Browse Source

feat: sync to dropbox

Gerald 9 years ago
parent
commit
4e20fe00e6

+ 1 - 0
src/background/db.js

@@ -552,6 +552,7 @@ VMDB.prototype.parseScript = function (data) {
       script.custom.homepageURL = data.from;
     if (scriptUtils.isRemote(data.url))
       script.custom.lastInstallURL = data.url;
+    script.custom.modified = data.modified || Date.now();
     return _this.saveScript(script, tx);
   }).then(function (script) {
     _.assign(res.data, scriptUtils.getScriptInfo(script));

+ 28 - 0
src/background/events.js

@@ -0,0 +1,28 @@
+function getEventEmitter() {
+  var events = {};
+  return {
+    on: on,
+    off: off,
+    fire: fire,
+  };
+  function on(type, func) {
+    var list = events[type];
+    if (!list) list = events[type] = [];
+    list.push(func);
+  }
+  function off(type, func) {
+    var list = events[type];
+    if (list) {
+      var i = list.indexOf(func);
+      if (~i) list.splice(i, 1);
+    }
+  }
+  function fire(type, data) {
+    var list = events[type];
+    list && list.forEach(function (func) {
+      func(data, type);
+    });
+  }
+}
+
+var events = getEventEmitter();

+ 3 - 0
src/background/main.js

@@ -5,6 +5,7 @@ var commands = {
     return Promise.resolve(scriptUtils.newScript());
   },
   RemoveScript: function (id, src) {
+    setTimeout(sync.start);
     return vmdb.removeScript(id);
   },
   GetData: function (data, src) {
@@ -69,6 +70,7 @@ var commands = {
           isClickable: true,
         });
       _.messenger.post(res);
+      setTimeout(sync.start);
       return res.data;
     });
   },
@@ -135,6 +137,7 @@ vmdb.initialized.then(function () {
     }
   });
   setTimeout(autoUpdate, 2e4);
+  sync.init();
 });
 
 // Common functions

+ 139 - 0
src/background/sync/dropbox.js

@@ -0,0 +1,139 @@
+var dropbox = function () {
+  var config = {
+    client_id: 'f0q12zup2uys5w8',
+    redirect_uri: 'https://violentmonkey.github.io/auth_dropbox.html',
+    options: _.options.get('dropbox', {}),
+  };
+  var events = getEventEmitter();
+  var dropbox = {
+    inst: null,
+    init: init,
+    dump: dump,
+    authenticate: authenticate,
+    on: events.on,
+    meta: {},
+  };
+
+  chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
+    var redirect_uri = config.redirect_uri + '#';
+    var url = changeInfo.url;
+    if (url && url.slice(0, redirect_uri.length) === redirect_uri) {
+      authorized(url.slice(redirect_uri.length));
+    }
+  });
+
+  function init() {
+    dropbox.inst = null;
+    var ret;
+    if (config.options.token) {
+      dropbox.inst = new Dropbox(config.options.token);
+      dropbox.meta = config.options.meta = config.options.meta || {};
+      ret = dropbox.inst.fetch('https://api.dropboxapi.com/1/account/info')
+      .then(function (res) {
+        events.fire('init');
+        //return res.json();
+      }, function (res) {
+        if (res.status > 300) {
+          dropbox.inst = null;
+          _.options.set('dropbox', config.options = {});
+        }
+      });
+    } else {
+      ret = Promise.reject();
+    }
+    return ret;
+  }
+  function authenticate() {
+    var params = {
+      response_type: 'token',
+      client_id: config.client_id,
+      redirect_uri: config.redirect_uri,
+    };
+    var url = 'https://www.dropbox.com/1/oauth2/authorize';
+    var qs = searchParams.dump(params);
+    url += '?' + qs;
+    chrome.tabs.create({url: url});
+  }
+  function authorized(raw) {
+    var data = searchParams.load(raw);
+    if (data.access_token) {
+      _.assign(config.options, {
+        uid: data.uid,
+        token: data.access_token,
+      });
+      dump();
+      init();
+    }
+  }
+  function dump() {
+    _.options.set('dropbox', config.options);
+  }
+  function normalize(item) {
+    return {
+      bytes: item.bytes,
+      uri: decodeURIComponent(item.path.slice(1, -8)),
+      modified: new Date(item.modified).getTime(),
+      //is_deleted: item.is_deleted,
+    };
+  }
+
+  function Dropbox(token) {
+    this.token = token;
+    this.headers = {
+      Authorization: 'Bearer ' + token,
+    };
+  }
+  Dropbox.prototype.fetch = function (input, init) {
+    init = init || {};
+    init.headers = _.assign(init.headers || {}, this.headers);
+    return fetch(input, init)
+    .then(function (res) {
+      return new Promise(function (resolve, reject) {
+        res.status > 300 ? reject(res) : resolve(res);
+      });
+    });
+  };
+  Dropbox.prototype.put = function (path, data) {
+    return this.fetch('https://content.dropboxapi.com/1/files_put/auto/' + path, {
+      method: 'PUT',
+      body: data,
+    }).then(function (res) {
+      return res.json()
+    }).then(normalize);
+  };
+  Dropbox.prototype.get = function (path) {
+    return this.fetch('https://content.dropboxapi.com/1/files/auto/' + path)
+    .then(function (res) {
+      return res.text();
+    });
+  };
+  Dropbox.prototype.remove = function (path) {
+    return this.fetch('https://api.dropboxapi.com/1/fileops/delete', {
+      method: 'POST',
+      headers: {
+        'Content-type': 'application/x-www-form-urlencoded',
+      },
+      body: searchParams.dump({
+        root: 'auto',
+        path: path,
+      }),
+    }).then(function (res) {
+      return res.json();
+    }).then(normalize);
+  };
+  Dropbox.prototype.list = function () {
+    var _this = this;
+    //return _this.fetch('https://api.dropboxapi.com/1/metadata/auto/?include_deleted=true')
+    return _this.fetch('https://api.dropboxapi.com/1/metadata/auto/')
+    .then(function (res) {
+      return res.json();
+    })
+    .then(function (data) {
+      return data.contents.filter(function (item) {
+        return !item.is_dir && /\.user\.js$/.test(item.path);
+      }).map(normalize);
+    });
+  };
+
+  return dropbox;
+}();

+ 151 - 0
src/background/sync/index.js

@@ -0,0 +1,151 @@
+var sync = function () {
+  var METAFILE = 'Violentmonkey';
+  var services = [dropbox];
+  var servicesReady = [];
+  var queue = [];
+  var syncing;
+  var timer;
+
+  return {
+    init: init,
+    start: start,
+  };
+
+  function start(service) {
+    if (service) queue.push(service);
+    else if (!syncing && queue.length < servicesReady.length) queue = servicesReady.slice();
+    if (syncing) return;
+    debouncedSync();
+  }
+  function debouncedSync() {
+    console.log('Ready to sync');
+    timer && clearTimeout(timer);
+    timer = setTimeout(function () {
+      timer = null;
+      console.log('Start to sync');
+      sync();
+    }, 10000);
+  }
+  function stopSync() {
+    console.log('Sync ended');
+    syncing = false;
+  }
+  function sync() {
+    var service = queue.shift();
+    if (!service) return stopSync();
+    syncing = true;
+    syncOne(service).then(sync, stopSync);
+  }
+  function init() {
+    services.forEach(function (service) {
+      service.on('init', function () {
+        servicesReady.push(service);
+        start(service);
+      });
+      service.init();
+    });
+  }
+  function getFilename(uri) {
+    // encodeURIComponent twice to ensure the decoded filename has no slashes
+    return encodeURIComponent(encodeURIComponent(uri) + '.user.js');
+  }
+  function syncOne(service) {
+    if (!service.inst) return;
+    return Promise.all([
+      service.inst.list(),
+      service.inst.get(METAFILE)
+      .then(function (data) {
+        return JSON.parse(data);
+      }, function (res) {
+        if (res.status === 404) {
+          return {};
+        }
+        throw res;
+      }),
+      vmdb.getScriptsByIndex('position'),
+    ]).then(function (res) {
+      var remote = {
+        data: res[0],
+        meta: res[1],
+      };
+      var local = {
+        data: res[2],
+        meta: service.meta,
+      };
+      var firstSync = !local.meta.timestamp;
+      var outdated = !local.meta.timestamp || remote.meta.timestamp > local.meta.timestamp;
+      console.log('First sync:', firstSync);
+      console.log('Outdated:', outdated);
+      var map = {};
+      var getRemote = [];
+      var putRemote = [];
+      var delRemote = [];
+      var delLocal = [];
+      remote.data.forEach(function (item) {
+        map[item.uri] = item;
+      });
+      local.data.forEach(function (item) {
+        var remoteItem = map[item.uri];
+        if (remoteItem) {
+          if (firstSync || remoteItem.modified <= item.custom.modified) {
+            // up to date
+            delete map[item.uri];
+          }
+        } else if (firstSync || !outdated) {
+          putRemote.push(item);
+        } else {
+          delLocal.push(item);
+        }
+        return map;
+      });
+      for (var uri in map) {
+        var item = map[uri];
+        if (firstSync || outdated) {
+          getRemote.push(item);
+        } else {
+          delRemote.push(item);
+        }
+      }
+      var promises = [].concat(
+        getRemote.map(function (item) {
+          console.log('Download script:', item.uri);
+          return service.inst.get(getFilename(item.uri)).then(function (code) {
+            return vmdb.parseScript({
+              code: code,
+              modified: item.modified,
+            });
+          });
+        }),
+        putRemote.map(function (item) {
+          console.log('Upload script:', item.uri);
+          return service.inst.put(getFilename(item.uri), item.code).then(function (data) {
+            if (item.custom.modified !== data.modified) {
+              item.custom.modified = data.modified;
+              return vmdb.saveScript(item);
+            }
+          });
+        }),
+        delRemote.map(function (item) {
+          console.log('Remove remote script:', item.uri);
+          return service.inst.remove(getFilename(item.uri));
+        }),
+        delLocal.map(function (item) {
+          console.log('Remove local script:', item.uri);
+          return vmdb.removeScript(item.id);
+        })
+      );
+      return Promise.all(promises).then(function () {
+        var promises = [];
+        if (!remote.meta.timestamp || putRemote.length || delRemote.length) {
+          remote.meta.timestamp = Date.now();
+          promises.push(service.inst.put(METAFILE, JSON.stringify(remote.meta)));
+        }
+        if (!local.meta.timestamp || getRemote.length || delLocal.length) {
+          local.meta.timestamp = remote.meta.timestamp;
+          service.dump();
+        }
+        return Promise.all(promises);
+      });
+    }).catch(err => console.log(err));
+  }
+}();

+ 17 - 0
src/background/utils.js

@@ -165,6 +165,23 @@ var tester = function () {
   };
 }();
 
+var searchParams = {
+  load: function (string) {
+    return string.split('&').reduce(function (data, piece) {
+      parts = piece.split('=');
+      data[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
+      return data;
+    }, {});
+  },
+  dump: function (dict) {
+    var qs = [];
+    for (var k in dict) {
+      qs.push(encodeURIComponent(k) + '=' + encodeURIComponent(dict[k]));
+    }
+    return qs.join('&');
+  },
+};
+
 _.broadcast = function (data) {
   chrome.tabs.query({}, function (tabs) {
     _.forEach(tabs, function (tab) {

+ 1 - 0
src/common.js

@@ -12,6 +12,7 @@ _.options = function () {
     trackLocalFile: false,
     injectMode: 0,
     autoReload: false,
+    dropbox: {},
   };
 
   function getOption(key, def) {