base.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. var _ = require('src/common');
  2. var events = require('../utils/events');
  3. var app = require('../app');
  4. var options = require('../options');
  5. var inited;
  6. var serviceNames = [];
  7. var services = {};
  8. var autoSync = _.debounce(function () {
  9. sync();
  10. }, 60 * 60 * 1000);
  11. var working = Promise.resolve();
  12. var syncConfig = initConfig();
  13. function getFilename(uri) {
  14. return 'vm-' + encodeURIComponent(uri);
  15. }
  16. function isScriptFile(name) {
  17. return /^vm-/.test(name);
  18. }
  19. function getURI(name) {
  20. return decodeURIComponent(name.slice(3));
  21. }
  22. function initConfig() {
  23. function get(key, def) {
  24. var keys = _.normalizeKeys(key);
  25. keys.unshift('sync');
  26. return options.get(keys, def);
  27. }
  28. function set(key, value) {
  29. var keys = _.normalizeKeys(key);
  30. keys.unshift('sync');
  31. options.set(keys, value);
  32. }
  33. function init() {
  34. var sync = options.get('sync');
  35. if (!sync || !sync.services) {
  36. sync = {
  37. services: {},
  38. };
  39. // XXX Migrate from old data
  40. ['dropbox', 'onedrive']
  41. .forEach(function (key) {
  42. sync.services[key] = options.get(key);
  43. });
  44. set([], sync);
  45. }
  46. }
  47. init();
  48. return {get: get, set: set};
  49. }
  50. function ServiceConfig(name) {
  51. this.name = name;
  52. }
  53. ServiceConfig.prototype.normalizeKeys = function (key) {
  54. var keys = _.normalizeKeys(key);
  55. keys.unshift('services', this.name);
  56. return keys;
  57. };
  58. ServiceConfig.prototype.get = function (key, def) {
  59. var keys = this.normalizeKeys(key);
  60. return syncConfig.get(keys, def);
  61. };
  62. ServiceConfig.prototype.set = function (key, val) {
  63. var _this = this;
  64. if (typeof key === 'object') {
  65. var data = key;
  66. Object.keys(data).forEach(function (key) {
  67. var keys = _this.normalizeKeys(key);
  68. syncConfig.set(keys, data[key]);
  69. });
  70. } else {
  71. var keys = _this.normalizeKeys(key);
  72. syncConfig.set(keys, val);
  73. }
  74. };
  75. ServiceConfig.prototype.clear = function () {
  76. syncConfig.set(this.normalizeKeys(), {});
  77. };
  78. function serviceState(validStates, initialState, onChange) {
  79. var state = initialState || validStates[0];
  80. return {
  81. get: function () {return state;},
  82. set: function (_state) {
  83. if (~validStates.indexOf(_state)) {
  84. state = _state;
  85. onChange && onChange();
  86. } else {
  87. console.warn('Invalid state:', _state);
  88. }
  89. return state;
  90. },
  91. is: function (states) {
  92. if (!Array.isArray(states)) states = [states];
  93. return ~states.indexOf(state);
  94. },
  95. };
  96. }
  97. function getStates() {
  98. return serviceNames.map(function (name) {
  99. var service = services[name];
  100. return {
  101. name: service.name,
  102. displayName: service.displayName,
  103. authState: service.authState.get(),
  104. syncState: service.syncState.get(),
  105. lastSync: service.config.get('meta', {}).lastSync,
  106. progress: service.progress,
  107. };
  108. });
  109. }
  110. function serviceFactory(base, options) {
  111. var Service = function () {
  112. this.initialize.apply(this, arguments);
  113. };
  114. Service.prototype = Object.assign(Object.create(base), options);
  115. Service.extend = extendService;
  116. return Service;
  117. }
  118. function extendService(options) {
  119. return serviceFactory(this.prototype, options);
  120. }
  121. var BaseService = serviceFactory({
  122. name: 'base',
  123. displayName: 'BaseService',
  124. delayTime: 1000,
  125. urlPrefix: '',
  126. metaFile: 'Violentmonkey',
  127. delay: function (time) {
  128. if (time == null) time = this.delayTime;
  129. return new Promise(function (resolve, _reject) {
  130. setTimeout(resolve, time);
  131. });
  132. },
  133. initialize: function (name) {
  134. var _this = this;
  135. _this.onStateChange = _.debounce(_this.onStateChange.bind(_this));
  136. if (name) _this.name = name;
  137. _this.progress = {
  138. finished: 0,
  139. total: 0,
  140. };
  141. _this.config = new ServiceConfig(_this.name);
  142. _this.authState = serviceState([
  143. 'idle',
  144. 'initializing',
  145. 'authorizing', // in case some services require asynchronous requests to get access_tokens
  146. 'authorized',
  147. 'unauthorized',
  148. 'error',
  149. ], null, _this.onStateChange);
  150. _this.syncState = serviceState([
  151. 'idle',
  152. 'ready',
  153. 'syncing',
  154. 'error',
  155. ], null, _this.onStateChange);
  156. // _this.initToken();
  157. _this.events = events.getEventEmitter();
  158. _this.lastFetch = Promise.resolve();
  159. _this.startSync = _this.syncFactory();
  160. },
  161. on: function () {
  162. return this.events.on.apply(null, arguments);
  163. },
  164. off: function () {
  165. return this.events.off.apply(null, arguments);
  166. },
  167. fire: function () {
  168. return this.events.fire.apply(null, arguments);
  169. },
  170. onStateChange: function () {
  171. _.messenger.post({
  172. cmd: 'UpdateSync',
  173. data: getStates(),
  174. });
  175. },
  176. syncFactory: function () {
  177. var _this = this;
  178. var promise, debouncedResolve;
  179. function shouldSync() {
  180. return _this.authState.is('authorized') && getCurrent() === _this.name;
  181. }
  182. function init() {
  183. if (!shouldSync()) return Promise.resolve();
  184. console.log('Ready to sync:', _this.displayName);
  185. _this.syncState.set('ready');
  186. promise = working = working.then(function () {
  187. return new Promise(function (resolve, _reject) {
  188. debouncedResolve = _.debounce(resolve, 10 * 1000);
  189. debouncedResolve();
  190. });
  191. })
  192. .then(function () {
  193. if (shouldSync()) {
  194. return _this.sync();
  195. }
  196. _this.syncState.set('idle');
  197. })
  198. .catch(function (err) {
  199. console.error(err);
  200. })
  201. .then(function () {
  202. promise = debouncedResolve = null;
  203. });
  204. }
  205. return function () {
  206. if (!promise) init();
  207. debouncedResolve && debouncedResolve();
  208. return promise;
  209. };
  210. },
  211. prepareHeaders: function () {
  212. this.headers = {};
  213. },
  214. prepare: function () {
  215. var _this = this;
  216. _this.authState.set('initializing');
  217. return (_this.initToken() ? Promise.resolve(_this.user()) : Promise.reject({
  218. type: 'unauthorized',
  219. }))
  220. .then(function () {
  221. _this.authState.set('authorized');
  222. }, function (err) {
  223. if (err.type === 'unauthorized') {
  224. // _this.config.clear();
  225. _this.authState.set('unauthorized');
  226. } else {
  227. console.error(err);
  228. _this.authState.set('error');
  229. }
  230. _this.syncState.set('idle');
  231. throw err;
  232. });
  233. },
  234. checkSync: function () {
  235. var _this = this;
  236. return _this.prepare()
  237. .then(function () {
  238. return _this.startSync();
  239. });
  240. },
  241. user: _.noop,
  242. getMeta: function () {
  243. var _this = this;
  244. return _this.get(_this.metaFile)
  245. .then(function (data) {
  246. return JSON.parse(data);
  247. });
  248. },
  249. initToken: function () {
  250. var _this = this;
  251. _this.prepareHeaders();
  252. var token = _this.config.get('token');
  253. if (token) {
  254. _this.headers.Authorization = 'Bearer ' + token;
  255. return true;
  256. }
  257. },
  258. request: function (options) {
  259. var _this = this;
  260. var progress = _this.progress;
  261. var lastFetch;
  262. if (options.noDelay) {
  263. lastFetch = Promise.resolve();
  264. } else {
  265. lastFetch = _this.lastFetch;
  266. _this.lastFetch = lastFetch.then(function () {
  267. return _this.delay();
  268. });
  269. }
  270. progress.total ++;
  271. _this.onStateChange();
  272. return lastFetch.then(function () {
  273. return new Promise(function (resolve, reject) {
  274. var xhr = new XMLHttpRequest;
  275. var prefix = options.prefix;
  276. if (prefix == null) prefix = _this.urlPrefix;
  277. xhr.open(options.method || 'GET', prefix + options.url, true);
  278. var headers = Object.assign({}, _this.headers, options.headers);
  279. if (options.body && typeof options.body === 'object') {
  280. headers['Content-Type'] = 'application/json';
  281. options.body = JSON.stringify(options.body);
  282. }
  283. Object.keys(headers).forEach(function (key) {
  284. var v = headers[key];
  285. v && xhr.setRequestHeader(key, v);
  286. });
  287. xhr.onloadend = function () {
  288. progress.finished ++;
  289. var data = xhr.responseText;
  290. if (options.responseType === 'json') {
  291. try {
  292. data = JSON.parse(data);
  293. } catch (e) {
  294. // Invalid JSON data
  295. }
  296. }
  297. _this.onStateChange();
  298. if (xhr.status === 503) {
  299. // TODO Too Many Requests
  300. }
  301. // net error: xhr.status === 0
  302. if (xhr.status >= 200 && xhr.status < 300) {
  303. resolve(data);
  304. } else {
  305. requestError(data);
  306. }
  307. };
  308. xhr.send(options.body);
  309. function requestError(data) {
  310. reject({
  311. url: options.url,
  312. status: xhr.status,
  313. xhr: xhr,
  314. data: data,
  315. });
  316. }
  317. });
  318. });
  319. },
  320. sync: function () {
  321. var _this = this;
  322. _this.progress = {
  323. finished: 0,
  324. total: 0,
  325. };
  326. _this.syncState.set('syncing');
  327. return _this.getMeta()
  328. .then(function (meta) {
  329. return Promise.all([
  330. meta,
  331. _this.list(),
  332. app.vmdb.getScriptsByIndex('position'),
  333. ]);
  334. }).then(function (res) {
  335. var remote = {
  336. meta: res[0],
  337. data: res[1],
  338. };
  339. var local = {
  340. meta: _this.config.get('meta', {}),
  341. data: res[2],
  342. };
  343. var firstSync = !local.meta.timestamp;
  344. var outdated = !local.meta.timestamp || remote.meta.timestamp > local.meta.timestamp;
  345. console.log('First sync:', firstSync);
  346. console.log('Outdated:', outdated, '(', 'local:', local.meta.timestamp, 'remote:', remote.meta.timestamp, ')');
  347. var map = {};
  348. var getRemote = [];
  349. var putRemote = [];
  350. var delRemote = [];
  351. var delLocal = [];
  352. remote.data.forEach(function (item) {
  353. map[item.uri] = item;
  354. });
  355. local.data.forEach(function (item) {
  356. var remoteItem = map[item.uri];
  357. if (remoteItem) {
  358. if (firstSync || !item.custom.modified || remoteItem.modified > item.custom.modified) {
  359. getRemote.push(remoteItem);
  360. } else if (remoteItem.modified < item.custom.modified) {
  361. putRemote.push(item);
  362. }
  363. delete map[item.uri];
  364. } else if (firstSync || !outdated) {
  365. putRemote.push(item);
  366. } else {
  367. delLocal.push(item);
  368. }
  369. });
  370. Object.keys(map).forEach(function (uri) {
  371. var item = map[uri];
  372. if (outdated) {
  373. getRemote.push(item);
  374. } else {
  375. delRemote.push(item);
  376. }
  377. });
  378. var promises = [].concat(
  379. getRemote.map(function (item) {
  380. console.log('Download script:', item.uri);
  381. return _this.get(getFilename(item.uri)).then(function (raw) {
  382. var data = {};
  383. try {
  384. var obj = JSON.parse(raw);
  385. if (obj.version === 1) {
  386. data.code = obj.code;
  387. data.more = obj.more;
  388. }
  389. } catch (e) {
  390. data.code = raw;
  391. }
  392. data.modified = item.modified;
  393. if (!options.get('syncScriptStatus') && data.more) {
  394. delete data.more.enabled;
  395. }
  396. return app.vmdb.parseScript(data)
  397. .then(function (res) {
  398. _.messenger.post(res);
  399. });
  400. });
  401. }),
  402. putRemote.map(function (item) {
  403. console.log('Upload script:', item.uri);
  404. var data = JSON.stringify({
  405. version: 1,
  406. code: item.code,
  407. more: {
  408. custom: item.custom,
  409. enabled: item.enabled,
  410. update: item.update,
  411. },
  412. });
  413. return _this.put(getFilename(item.uri), data)
  414. .then(function (data) {
  415. if (item.custom.modified !== data.modified) {
  416. item.custom.modified = data.modified;
  417. return app.vmdb.saveScript(item);
  418. }
  419. });
  420. }),
  421. delRemote.map(function (item) {
  422. console.log('Remove remote script:', item.uri);
  423. return _this.remove(getFilename(item.uri));
  424. }),
  425. delLocal.map(function (item) {
  426. console.log('Remove local script:', item.uri);
  427. return app.vmdb.removeScript(item.id);
  428. })
  429. );
  430. promises.push(Promise.all(promises).then(function () {
  431. var promises = [];
  432. var remoteChanged;
  433. if (!remote.meta.timestamp || putRemote.length || delRemote.length) {
  434. remoteChanged = true;
  435. remote.meta.timestamp = Date.now();
  436. promises.push(_this.put(_this.metaFile, JSON.stringify(remote.meta)));
  437. }
  438. if (!local.meta.timestamp || getRemote.length || delLocal.length || remoteChanged || outdated) {
  439. local.meta.timestamp = remote.meta.timestamp;
  440. }
  441. local.meta.lastSync = Date.now();
  442. _this.config.set('meta', local.meta);
  443. return Promise.all(promises);
  444. }));
  445. return Promise.all(promises.map(function (promise) {
  446. // ignore errors to ensure all promises are fulfilled
  447. return promise.then(_.noop, function (err) {
  448. return err || true;
  449. });
  450. }))
  451. .then(function (errors) {
  452. errors = errors.filter(function (err) {return err;});
  453. if (errors.length) throw errors;
  454. });
  455. })
  456. .then(function () {
  457. _this.syncState.set('idle');
  458. }, function (err) {
  459. _this.syncState.set('error');
  460. console.log('Failed syncing:', _this.name);
  461. console.log(err);
  462. });
  463. },
  464. });
  465. function register(Service) {
  466. var name = Service.prototype.name || Service.name;
  467. var service = new Service(name);
  468. serviceNames.push(name);
  469. services[name] = service;
  470. // setTimeout(function () {
  471. // inited && service.checkSync();
  472. // });
  473. return service;
  474. }
  475. function getCurrent() {
  476. return syncConfig.get('current');
  477. }
  478. function getService(name) {
  479. name = name || getCurrent();
  480. return services[name];
  481. }
  482. function initialize() {
  483. inited = true;
  484. // serviceNames.forEach(function (name) {
  485. // var service = services[name];
  486. // service.checkSync();
  487. // });
  488. var service = getService();
  489. service && service.checkSync();
  490. // sync();
  491. }
  492. function syncOne(service) {
  493. if (service.syncState.is(['ready', 'syncing'])) return;
  494. if (service.authState.is(['idle', 'error'])) return service.checkSync();
  495. if (service.authState.is('authorized')) return service.startSync();
  496. }
  497. function sync() {
  498. var service = getService();
  499. return service && syncOne(service).then(autoSync);
  500. }
  501. function checkAuthUrl(url) {
  502. return serviceNames.some(function (name) {
  503. var service = services[name];
  504. return service.checkAuth && service.checkAuth(url);
  505. });
  506. }
  507. function authorize() {
  508. var service = getService();
  509. service && service.authorize();
  510. }
  511. function revoke() {
  512. var service = getService();
  513. service && service.revoke();
  514. }
  515. options.hook(function (data) {
  516. ('sync.current' in data) && initialize();
  517. });
  518. exports.utils = {
  519. getFilename: getFilename,
  520. isScriptFile: isScriptFile,
  521. getURI: getURI,
  522. };
  523. exports.initialize = initialize;
  524. exports.sync = sync;
  525. exports.getStates = getStates;
  526. exports.checkAuthUrl = checkAuthUrl;
  527. exports.BaseService = BaseService;
  528. exports.register = register;
  529. exports.service = getService;
  530. exports.authorize = authorize;
  531. exports.revoke = revoke;