| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546 |
- var _ = require('src/common');
- var events = require('../utils/events');
- var app = require('../app');
- var options = require('../options');
- var inited;
- var serviceNames = [];
- var services = {};
- var autoSync = _.debounce(function () {
- sync();
- }, 60 * 60 * 1000);
- var working = Promise.resolve();
- var syncConfig = initConfig();
- function getFilename(uri) {
- return 'vm-' + encodeURIComponent(uri);
- }
- function isScriptFile(name) {
- return /^vm-/.test(name);
- }
- function getURI(name) {
- return decodeURIComponent(name.slice(3));
- }
- function initConfig() {
- function get(key, def) {
- var keys = _.normalizeKeys(key);
- keys.unshift('sync');
- return options.get(keys, def);
- }
- function set(key, value) {
- var keys = _.normalizeKeys(key);
- keys.unshift('sync');
- options.set(keys, value);
- }
- function init() {
- var sync = options.get('sync');
- if (!sync || !sync.services) {
- sync = {
- services: {},
- };
- // XXX Migrate from old data
- ['dropbox', 'onedrive']
- .forEach(function (key) {
- sync.services[key] = options.get(key);
- });
- set([], sync);
- }
- }
- init();
- return {get: get, set: set};
- }
- function ServiceConfig(name) {
- this.name = name;
- }
- ServiceConfig.prototype.normalizeKeys = function (key) {
- var keys = _.normalizeKeys(key);
- keys.unshift('services', this.name);
- return keys;
- };
- ServiceConfig.prototype.get = function (key, def) {
- var keys = this.normalizeKeys(key);
- return syncConfig.get(keys, def);
- };
- ServiceConfig.prototype.set = function (key, val) {
- var _this = this;
- if (typeof key === 'object') {
- var data = key;
- Object.keys(data).forEach(function (key) {
- var keys = _this.normalizeKeys(key);
- syncConfig.set(keys, data[key]);
- });
- } else {
- var keys = _this.normalizeKeys(key);
- syncConfig.set(keys, val);
- }
- };
- ServiceConfig.prototype.clear = function () {
- syncConfig.set(this.normalizeKeys(), {});
- };
- function serviceState(validStates, initialState, onChange) {
- var state = initialState || validStates[0];
- return {
- get: function () {return state;},
- set: function (_state) {
- if (~validStates.indexOf(_state)) {
- state = _state;
- onChange && onChange();
- } else {
- console.warn('Invalid state:', _state);
- }
- return state;
- },
- is: function (states) {
- if (!Array.isArray(states)) states = [states];
- return ~states.indexOf(state);
- },
- };
- }
- function getStates() {
- return serviceNames.map(function (name) {
- var service = services[name];
- return {
- name: service.name,
- displayName: service.displayName,
- authState: service.authState.get(),
- syncState: service.syncState.get(),
- lastSync: service.config.get('meta', {}).lastSync,
- progress: service.progress,
- };
- });
- }
- function serviceFactory(base, options) {
- var Service = function () {
- this.initialize.apply(this, arguments);
- };
- Service.prototype = Object.assign(Object.create(base), options);
- Service.extend = extendService;
- return Service;
- }
- function extendService(options) {
- return serviceFactory(this.prototype, options);
- }
- var BaseService = serviceFactory({
- name: 'base',
- displayName: 'BaseService',
- delayTime: 1000,
- urlPrefix: '',
- metaFile: 'Violentmonkey',
- delay: function (time) {
- if (time == null) time = this.delayTime;
- return new Promise(function (resolve, _reject) {
- setTimeout(resolve, time);
- });
- },
- initialize: function (name) {
- var _this = this;
- _this.onStateChange = _.debounce(_this.onStateChange.bind(_this));
- if (name) _this.name = name;
- _this.progress = {
- finished: 0,
- total: 0,
- };
- _this.config = new ServiceConfig(_this.name);
- _this.authState = serviceState([
- 'idle',
- 'initializing',
- 'authorizing', // in case some services require asynchronous requests to get access_tokens
- 'authorized',
- 'unauthorized',
- 'error',
- ], null, _this.onStateChange);
- _this.syncState = serviceState([
- 'idle',
- 'ready',
- 'syncing',
- 'error',
- ], null, _this.onStateChange);
- // _this.initToken();
- _this.events = events.getEventEmitter();
- _this.lastFetch = Promise.resolve();
- _this.startSync = _this.syncFactory();
- },
- on: function () {
- return this.events.on.apply(null, arguments);
- },
- off: function () {
- return this.events.off.apply(null, arguments);
- },
- fire: function () {
- return this.events.fire.apply(null, arguments);
- },
- onStateChange: function () {
- _.messenger.post({
- cmd: 'UpdateSync',
- data: getStates(),
- });
- },
- syncFactory: function () {
- var _this = this;
- var promise, debouncedResolve;
- function shouldSync() {
- return _this.authState.is('authorized') && getCurrent() === _this.name;
- }
- function init() {
- if (!shouldSync()) return Promise.resolve();
- console.log('Ready to sync:', _this.displayName);
- _this.syncState.set('ready');
- promise = working = working.then(function () {
- return new Promise(function (resolve, _reject) {
- debouncedResolve = _.debounce(resolve, 10 * 1000);
- debouncedResolve();
- });
- })
- .then(function () {
- if (shouldSync()) {
- return _this.sync();
- }
- _this.syncState.set('idle');
- })
- .catch(function (err) {
- console.error(err);
- })
- .then(function () {
- promise = debouncedResolve = null;
- });
- }
- return function () {
- if (!promise) init();
- debouncedResolve && debouncedResolve();
- return promise;
- };
- },
- prepareHeaders: function () {
- this.headers = {};
- },
- prepare: function () {
- var _this = this;
- _this.authState.set('initializing');
- return (_this.initToken() ? Promise.resolve(_this.user()) : Promise.reject({
- type: 'unauthorized',
- }))
- .then(function () {
- _this.authState.set('authorized');
- }, function (err) {
- if (err.type === 'unauthorized') {
- // _this.config.clear();
- _this.authState.set('unauthorized');
- } else {
- console.error(err);
- _this.authState.set('error');
- }
- _this.syncState.set('idle');
- throw err;
- });
- },
- checkSync: function () {
- var _this = this;
- return _this.prepare()
- .then(function () {
- return _this.startSync();
- });
- },
- user: _.noop,
- getMeta: function () {
- var _this = this;
- return _this.get(_this.metaFile)
- .then(function (data) {
- return JSON.parse(data);
- });
- },
- initToken: function () {
- var _this = this;
- _this.prepareHeaders();
- var token = _this.config.get('token');
- if (token) {
- _this.headers.Authorization = 'Bearer ' + token;
- return true;
- }
- },
- request: function (options) {
- var _this = this;
- var progress = _this.progress;
- var lastFetch;
- if (options.noDelay) {
- lastFetch = Promise.resolve();
- } else {
- lastFetch = _this.lastFetch;
- _this.lastFetch = lastFetch.then(function () {
- return _this.delay();
- });
- }
- progress.total ++;
- _this.onStateChange();
- return lastFetch.then(function () {
- return new Promise(function (resolve, reject) {
- var xhr = new XMLHttpRequest;
- var prefix = options.prefix;
- if (prefix == null) prefix = _this.urlPrefix;
- xhr.open(options.method || 'GET', prefix + options.url, true);
- var headers = Object.assign({}, _this.headers, options.headers);
- if (options.body && typeof options.body === 'object') {
- headers['Content-Type'] = 'application/json';
- options.body = JSON.stringify(options.body);
- }
- Object.keys(headers).forEach(function (key) {
- var v = headers[key];
- v && xhr.setRequestHeader(key, v);
- });
- xhr.onloadend = function () {
- progress.finished ++;
- var data = xhr.responseText;
- if (options.responseType === 'json') {
- try {
- data = JSON.parse(data);
- } catch (e) {
- // Invalid JSON data
- }
- }
- _this.onStateChange();
- if (xhr.status === 503) {
- // TODO Too Many Requests
- }
- // net error: xhr.status === 0
- if (xhr.status >= 200 && xhr.status < 300) {
- resolve(data);
- } else {
- requestError(data);
- }
- };
- xhr.send(options.body);
- function requestError(data) {
- reject({
- url: options.url,
- status: xhr.status,
- xhr: xhr,
- data: data,
- });
- }
- });
- });
- },
- sync: function () {
- var _this = this;
- _this.progress = {
- finished: 0,
- total: 0,
- };
- _this.syncState.set('syncing');
- return _this.getMeta()
- .then(function (meta) {
- return Promise.all([
- meta,
- _this.list(),
- app.vmdb.getScriptsByIndex('position'),
- ]);
- }).then(function (res) {
- var remote = {
- meta: res[0],
- data: res[1],
- };
- var local = {
- meta: _this.config.get('meta', {}),
- data: res[2],
- };
- 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, '(', 'local:', local.meta.timestamp, 'remote:', remote.meta.timestamp, ')');
- 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 || !item.custom.modified || remoteItem.modified > item.custom.modified) {
- getRemote.push(remoteItem);
- } else if (remoteItem.modified < item.custom.modified) {
- putRemote.push(item);
- }
- delete map[item.uri];
- } else if (firstSync || !outdated) {
- putRemote.push(item);
- } else {
- delLocal.push(item);
- }
- });
- Object.keys(map).forEach(function (uri) {
- var item = map[uri];
- if (outdated) {
- getRemote.push(item);
- } else {
- delRemote.push(item);
- }
- });
- var promises = [].concat(
- getRemote.map(function (item) {
- console.log('Download script:', item.uri);
- return _this.get(getFilename(item.uri)).then(function (raw) {
- var data = {};
- try {
- var obj = JSON.parse(raw);
- if (obj.version === 1) {
- data.code = obj.code;
- data.more = obj.more;
- }
- } catch (e) {
- data.code = raw;
- }
- data.modified = item.modified;
- if (!options.get('syncScriptStatus') && data.more) {
- delete data.more.enabled;
- }
- return app.vmdb.parseScript(data)
- .then(function (res) {
- _.messenger.post(res);
- });
- });
- }),
- putRemote.map(function (item) {
- console.log('Upload script:', item.uri);
- var data = JSON.stringify({
- version: 1,
- code: item.code,
- more: {
- custom: item.custom,
- enabled: item.enabled,
- update: item.update,
- },
- });
- return _this.put(getFilename(item.uri), data)
- .then(function (data) {
- if (item.custom.modified !== data.modified) {
- item.custom.modified = data.modified;
- return app.vmdb.saveScript(item);
- }
- });
- }),
- delRemote.map(function (item) {
- console.log('Remove remote script:', item.uri);
- return _this.remove(getFilename(item.uri));
- }),
- delLocal.map(function (item) {
- console.log('Remove local script:', item.uri);
- return app.vmdb.removeScript(item.id);
- })
- );
- promises.push(Promise.all(promises).then(function () {
- var promises = [];
- var remoteChanged;
- if (!remote.meta.timestamp || putRemote.length || delRemote.length) {
- remoteChanged = true;
- remote.meta.timestamp = Date.now();
- promises.push(_this.put(_this.metaFile, JSON.stringify(remote.meta)));
- }
- if (!local.meta.timestamp || getRemote.length || delLocal.length || remoteChanged || outdated) {
- local.meta.timestamp = remote.meta.timestamp;
- }
- local.meta.lastSync = Date.now();
- _this.config.set('meta', local.meta);
- return Promise.all(promises);
- }));
- return Promise.all(promises.map(function (promise) {
- // ignore errors to ensure all promises are fulfilled
- return promise.then(_.noop, function (err) {
- return err || true;
- });
- }))
- .then(function (errors) {
- errors = errors.filter(function (err) {return err;});
- if (errors.length) throw errors;
- });
- })
- .then(function () {
- _this.syncState.set('idle');
- }, function (err) {
- _this.syncState.set('error');
- console.log('Failed syncing:', _this.name);
- console.log(err);
- });
- },
- });
- function register(Service) {
- var name = Service.prototype.name || Service.name;
- var service = new Service(name);
- serviceNames.push(name);
- services[name] = service;
- // setTimeout(function () {
- // inited && service.checkSync();
- // });
- return service;
- }
- function getCurrent() {
- return syncConfig.get('current');
- }
- function getService(name) {
- name = name || getCurrent();
- return services[name];
- }
- function initialize() {
- inited = true;
- // serviceNames.forEach(function (name) {
- // var service = services[name];
- // service.checkSync();
- // });
- var service = getService();
- service && service.checkSync();
- // sync();
- }
- function syncOne(service) {
- if (service.syncState.is(['ready', 'syncing'])) return;
- if (service.authState.is(['idle', 'error'])) return service.checkSync();
- if (service.authState.is('authorized')) return service.startSync();
- }
- function sync() {
- var service = getService();
- return service && syncOne(service).then(autoSync);
- }
- function checkAuthUrl(url) {
- return serviceNames.some(function (name) {
- var service = services[name];
- return service.checkAuth && service.checkAuth(url);
- });
- }
- function authorize() {
- var service = getService();
- service && service.authorize();
- }
- function revoke() {
- var service = getService();
- service && service.revoke();
- }
- options.hook(function (data) {
- ('sync.current' in data) && initialize();
- });
- exports.utils = {
- getFilename: getFilename,
- isScriptFile: isScriptFile,
- getURI: getURI,
- };
- exports.initialize = initialize;
- exports.sync = sync;
- exports.getStates = getStates;
- exports.checkAuthUrl = checkAuthUrl;
- exports.BaseService = BaseService;
- exports.register = register;
- exports.service = getService;
- exports.authorize = authorize;
- exports.revoke = revoke;
|