f9db43a2e9
* Add key * Add: a second index uuid, push changes to sync controller * Add: sync.js * Add: tokenManager * Change: log entire body for http error * Add: token flow * Fix: minor * Fix: move cleanup to stop function * Add: syncNow * Update dependencies * Fix: handle 401 error * Add: handle 401 error * Fix: then -> catch * Add: sync options to options page * Update db-to-cloud * Change: make prefs.set return a promise * Add: disble selector if connected * Add: update selector state * Fix: return promise in prefs.set * Fix: manage complex state * Fix: handle prefs change * Change: manage sync status in background * Add: show current status in the UI * Add: schedule a faster sync when db changed * Update dependencies * Add: include progress in sync status * Add: more detail status * Show status text only * Bump dependencies * Change: show loaded and total * Fix: syncTarget is undefined * Add: google and onedrive * Fix: token is not reused * Bump dependencies * Don't use minified version since it is hard to debug * Fix: expire time is incorrect * Change: switch google to code flow * Bump dependencies * Change: only modify pref if the initialization success? * Don't stop the sync if the first sync is not triggered by the user * Add: implement refresh token * Change: switch microsoft to code flow * Add: subtract expire with a latency * Add: microsoft client secret * Add: display error message * Fix: fromPref is not used * Change: try to revoke the token when log out * Add: revoke dropbox token * Fix: Google only generates one refresh token for one user by default * Bump dependencies, fix onedrive list issue * Fix: arguments sent to sync.put is wrong * Fix: don't schedule a sync on db changed if not connected * Bump dependencies. Fix issue of switching drives * Bump db-to-cloud, fix switching drive issue * Fix: only auth user on 401 error, don't display login window without user interaction * Fix: don't call revoke() if token is undefined * Add: login button to generate the access token interactively * Fix: make addMissingProperties a local * Fix: store missing props in an object * Fix: sync.getStatus should be sync * LATENCY -> NETWORK_LATENCY * Fix: cache the token forever if there is no expire time e.g. dropbox * Add some comments * Fix: i18n * Fix: i18n sync status * fixup! Fix: i18n sync status * Fix: 'sync to cloud' is displayed twice
239 lines
5.7 KiB
JavaScript
239 lines
5.7 KiB
JavaScript
/* global dbToCloud styleManager chromeLocal prefs tokenManager msg */
|
|
/* exported sync */
|
|
|
|
'use strict';
|
|
|
|
const sync = (() => {
|
|
const SYNC_DELAY = 1; // minutes
|
|
const SYNC_INTERVAL = 30; // minutes
|
|
|
|
const status = {
|
|
state: 'disconnected',
|
|
syncing: false,
|
|
progress: null,
|
|
currentDriveName: null,
|
|
errorMessage: null,
|
|
login: false
|
|
};
|
|
let currentDrive;
|
|
const ctrl = dbToCloud.dbToCloud({
|
|
onGet(id) {
|
|
return styleManager.getByUUID(id);
|
|
},
|
|
onPut(doc) {
|
|
return styleManager.putByUUID(doc);
|
|
},
|
|
onDelete(id, rev) {
|
|
return styleManager.deleteByUUID(id, rev);
|
|
},
|
|
onFirstSync() {
|
|
return styleManager.getAllStyles()
|
|
.then(styles => {
|
|
styles.forEach(i => ctrl.put(i._id, i._rev));
|
|
});
|
|
},
|
|
onProgress,
|
|
compareRevision(a, b) {
|
|
return styleManager.compareRevision(a, b);
|
|
},
|
|
getState(drive) {
|
|
const key = `sync/state/${drive.name}`;
|
|
return chromeLocal.get(key)
|
|
.then(obj => obj[key]);
|
|
},
|
|
setState(drive, state) {
|
|
const key = `sync/state/${drive.name}`;
|
|
return chromeLocal.set({
|
|
[key]: state
|
|
});
|
|
}
|
|
});
|
|
|
|
const initializing = prefs.initializing.then(() => {
|
|
prefs.subscribe(['sync.enabled'], onPrefChange);
|
|
onPrefChange(null, prefs.get('sync.enabled'));
|
|
});
|
|
|
|
chrome.alarms.onAlarm.addListener(info => {
|
|
if (info.name === 'syncNow') {
|
|
syncNow().catch(console.error);
|
|
}
|
|
});
|
|
|
|
return Object.assign({
|
|
getStatus: () => status
|
|
}, ensurePrepared({
|
|
start,
|
|
stop,
|
|
put: (...args) => {
|
|
if (!currentDrive) return;
|
|
schedule();
|
|
return ctrl.put(...args);
|
|
},
|
|
delete: (...args) => {
|
|
if (!currentDrive) return;
|
|
schedule();
|
|
return ctrl.delete(...args);
|
|
},
|
|
syncNow,
|
|
login
|
|
}));
|
|
|
|
function ensurePrepared(obj) {
|
|
return Object.entries(obj).reduce((o, [key, fn]) => {
|
|
o[key] = (...args) =>
|
|
initializing.then(() => fn(...args));
|
|
return o;
|
|
}, {});
|
|
}
|
|
|
|
function onProgress(e) {
|
|
if (e.phase === 'start') {
|
|
status.syncing = true;
|
|
} else if (e.phase === 'end') {
|
|
status.syncing = false;
|
|
status.progress = null;
|
|
} else {
|
|
status.progress = e;
|
|
}
|
|
emitStatusChange();
|
|
}
|
|
|
|
function schedule(delay = SYNC_DELAY) {
|
|
chrome.alarms.create('syncNow', {
|
|
delayInMinutes: delay,
|
|
periodInMinutes: SYNC_INTERVAL
|
|
});
|
|
}
|
|
|
|
function onPrefChange(key, value) {
|
|
if (value === 'none') {
|
|
stop().catch(console.error);
|
|
} else {
|
|
start(value, true).catch(console.error);
|
|
}
|
|
}
|
|
|
|
function withFinally(p, cleanup) {
|
|
return p.then(
|
|
result => {
|
|
cleanup(undefined, result);
|
|
return result;
|
|
},
|
|
err => {
|
|
cleanup(err);
|
|
throw err;
|
|
}
|
|
);
|
|
}
|
|
|
|
function syncNow() {
|
|
if (!currentDrive) {
|
|
return Promise.reject(new Error('cannot sync when disconnected'));
|
|
}
|
|
return withFinally(
|
|
(ctrl.isInit() ? ctrl.syncNow() : ctrl.start())
|
|
.catch(handle401Error),
|
|
err => {
|
|
status.errorMessage = err ? err.message : null;
|
|
emitStatusChange();
|
|
}
|
|
);
|
|
}
|
|
|
|
function handle401Error(err) {
|
|
if (err.code === 401) {
|
|
return tokenManager.revokeToken(currentDrive.name)
|
|
.catch(console.error)
|
|
.then(() => {
|
|
status.login = false;
|
|
emitStatusChange();
|
|
throw err;
|
|
});
|
|
}
|
|
if (/User interaction required|Requires user interaction/i.test(err.message)) {
|
|
status.login = false;
|
|
emitStatusChange();
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
function emitStatusChange() {
|
|
msg.broadcastExtension({method: 'syncStatusUpdate', status});
|
|
}
|
|
|
|
function login(name = prefs.get('sync.enabled')) {
|
|
return tokenManager.getToken(name, true)
|
|
.catch(err => {
|
|
if (/Authorization page could not be loaded/i.test(err.message)) {
|
|
// FIXME: Chrome always fails at the first login so we try again
|
|
return tokenManager.getToken(name);
|
|
}
|
|
throw err;
|
|
})
|
|
.then(() => {
|
|
status.login = true;
|
|
emitStatusChange();
|
|
});
|
|
}
|
|
|
|
function start(name, fromPref = false) {
|
|
if (currentDrive) {
|
|
return Promise.resolve();
|
|
}
|
|
currentDrive = getDrive(name);
|
|
ctrl.use(currentDrive);
|
|
status.state = 'connecting';
|
|
status.currentDriveName = currentDrive.name;
|
|
status.login = true;
|
|
emitStatusChange();
|
|
return withFinally(
|
|
(fromPref ? Promise.resolve() : login(name))
|
|
.catch(handle401Error)
|
|
.then(() => syncNow()),
|
|
err => {
|
|
// FIXME: should we move this logic to options.js?
|
|
if (err && !fromPref) {
|
|
console.error(err);
|
|
return stop();
|
|
}
|
|
prefs.set('sync.enabled', name);
|
|
schedule(SYNC_INTERVAL);
|
|
status.state = 'connected';
|
|
emitStatusChange();
|
|
}
|
|
);
|
|
}
|
|
|
|
function getDrive(name) {
|
|
if (name === 'dropbox' || name === 'google' || name === 'onedrive') {
|
|
return dbToCloud.drive[name]({
|
|
getAccessToken: () => tokenManager.getToken(name)
|
|
});
|
|
}
|
|
throw new Error(`unknown cloud name: ${name}`);
|
|
}
|
|
|
|
function stop() {
|
|
if (!currentDrive) {
|
|
return Promise.resolve();
|
|
}
|
|
chrome.alarms.clear('syncNow');
|
|
status.state = 'disconnecting';
|
|
emitStatusChange();
|
|
return withFinally(
|
|
ctrl.stop()
|
|
.then(() => tokenManager.revokeToken(currentDrive.name))
|
|
.then(() => chromeLocal.remove(`sync/state/${currentDrive.name}`)),
|
|
() => {
|
|
currentDrive = null;
|
|
prefs.set('sync.enabled', 'none');
|
|
status.state = 'disconnected';
|
|
status.currentDriveName = null;
|
|
status.login = false;
|
|
emitStatusChange();
|
|
}
|
|
);
|
|
}
|
|
})();
|