2021-01-01 14:27:58 +00:00
|
|
|
/* global API msg */// msg.js
|
2022-01-28 23:54:56 +00:00
|
|
|
/* global bgReady uuidIndex */// common.js
|
2021-12-12 00:05:58 +00:00
|
|
|
/* global chromeLocal chromeSync */// storage-util.js
|
2022-01-28 23:54:56 +00:00
|
|
|
/* global db */
|
2021-02-09 06:58:30 +00:00
|
|
|
/* global iconMan */
|
2021-01-01 14:27:58 +00:00
|
|
|
/* global prefs */
|
2022-01-28 23:54:56 +00:00
|
|
|
/* global styleUtil */
|
2021-01-01 14:27:58 +00:00
|
|
|
/* global tokenMan */
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
const syncMan = (() => {
|
|
|
|
//#region Init
|
|
|
|
|
|
|
|
const SYNC_DELAY = 1; // minutes
|
|
|
|
const SYNC_INTERVAL = 30; // minutes
|
|
|
|
const STATES = Object.freeze({
|
|
|
|
connected: 'connected',
|
|
|
|
connecting: 'connecting',
|
|
|
|
disconnected: 'disconnected',
|
|
|
|
disconnecting: 'disconnecting',
|
|
|
|
});
|
|
|
|
const STORAGE_KEY = 'sync/state/';
|
2021-12-12 00:05:58 +00:00
|
|
|
const NO_LOGIN = ['webdav'];
|
2021-01-01 14:27:58 +00:00
|
|
|
const status = /** @namespace SyncManager.Status */ {
|
|
|
|
STATES,
|
|
|
|
state: STATES.disconnected,
|
|
|
|
syncing: false,
|
|
|
|
progress: null,
|
|
|
|
currentDriveName: null,
|
|
|
|
errorMessage: null,
|
|
|
|
login: false,
|
|
|
|
};
|
2022-01-28 23:54:56 +00:00
|
|
|
const compareRevision = (rev1, rev2) => rev1 - rev2;
|
2021-02-09 06:58:30 +00:00
|
|
|
let lastError = null;
|
2021-01-01 14:27:58 +00:00
|
|
|
let ctrl;
|
|
|
|
let currentDrive;
|
|
|
|
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
|
2022-01-28 23:54:56 +00:00
|
|
|
let ready = bgReady.styles.then(() => {
|
2021-01-01 14:27:58 +00:00
|
|
|
ready = true;
|
|
|
|
prefs.subscribe('sync.enabled',
|
|
|
|
(_, val) => val === 'none'
|
|
|
|
? syncMan.stop()
|
|
|
|
: syncMan.start(val, true),
|
|
|
|
{runNow: true});
|
|
|
|
});
|
|
|
|
|
2021-07-06 21:19:18 +00:00
|
|
|
chrome.alarms.onAlarm.addListener(async ({name}) => {
|
|
|
|
if (name === 'syncNow') {
|
2021-12-08 16:00:30 +00:00
|
|
|
await syncMan.syncNow();
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
//#endregion
|
|
|
|
//#region Exports
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
async delete(...args) {
|
|
|
|
if (ready.then) await ready;
|
|
|
|
if (!currentDrive) return;
|
|
|
|
schedule();
|
|
|
|
return ctrl.delete(...args);
|
|
|
|
},
|
|
|
|
|
|
|
|
/** @returns {Promise<SyncManager.Status>} */
|
|
|
|
async getStatus() {
|
|
|
|
return status;
|
|
|
|
},
|
|
|
|
|
2021-02-14 15:24:49 +00:00
|
|
|
async login(name) {
|
2021-01-01 14:27:58 +00:00
|
|
|
if (ready.then) await ready;
|
2021-02-14 15:24:49 +00:00
|
|
|
if (!name) name = prefs.get('sync.enabled');
|
|
|
|
await tokenMan.revokeToken(name);
|
2021-01-01 14:27:58 +00:00
|
|
|
try {
|
|
|
|
await tokenMan.getToken(name, true);
|
2021-02-14 15:24:49 +00:00
|
|
|
status.login = true;
|
2021-01-01 14:27:58 +00:00
|
|
|
} catch (err) {
|
2021-02-14 15:24:49 +00:00
|
|
|
status.login = false;
|
2021-01-01 14:27:58 +00:00
|
|
|
throw err;
|
2021-02-14 15:24:49 +00:00
|
|
|
} finally {
|
|
|
|
emitStatusChange();
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2022-01-28 23:54:56 +00:00
|
|
|
async putDoc({_id, _rev}) {
|
2021-01-01 14:27:58 +00:00
|
|
|
if (ready.then) await ready;
|
|
|
|
if (!currentDrive) return;
|
|
|
|
schedule();
|
2022-01-28 23:54:56 +00:00
|
|
|
return ctrl.put(_id, _rev);
|
2021-01-01 14:27:58 +00:00
|
|
|
},
|
|
|
|
|
2021-12-12 00:05:58 +00:00
|
|
|
async setDriveOptions(driveName, options) {
|
|
|
|
const key = `secure/sync/driveOptions/${driveName}`;
|
|
|
|
await chromeSync.setValue(key, options);
|
|
|
|
},
|
|
|
|
|
|
|
|
async getDriveOptions(driveName) {
|
|
|
|
const key = `secure/sync/driveOptions/${driveName}`;
|
|
|
|
return await chromeSync.getValue(key) || {};
|
|
|
|
},
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
async start(name, fromPref = false) {
|
|
|
|
if (ready.then) await ready;
|
|
|
|
if (!ctrl) await initController();
|
2021-02-14 15:24:49 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
if (currentDrive) return;
|
2021-12-12 00:05:58 +00:00
|
|
|
currentDrive = await getDrive(name);
|
2021-01-01 14:27:58 +00:00
|
|
|
ctrl.use(currentDrive);
|
2021-02-14 15:24:49 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
status.state = STATES.connecting;
|
|
|
|
status.currentDriveName = currentDrive.name;
|
|
|
|
emitStatusChange();
|
2021-02-14 15:24:49 +00:00
|
|
|
|
2021-12-12 00:05:58 +00:00
|
|
|
if (fromPref || NO_LOGIN.includes(currentDrive.name)) {
|
2021-02-14 15:24:49 +00:00
|
|
|
status.login = true;
|
|
|
|
} else {
|
|
|
|
try {
|
|
|
|
await syncMan.login(name);
|
|
|
|
} catch (err) {
|
2021-01-01 14:27:58 +00:00
|
|
|
console.error(err);
|
2021-02-14 15:24:49 +00:00
|
|
|
status.errorMessage = err.message;
|
|
|
|
lastError = err;
|
|
|
|
emitStatusChange();
|
2021-01-01 14:27:58 +00:00
|
|
|
return syncMan.stop();
|
|
|
|
}
|
|
|
|
}
|
2021-02-14 15:24:49 +00:00
|
|
|
|
|
|
|
await ctrl.init();
|
|
|
|
|
|
|
|
await syncMan.syncNow(name);
|
2021-01-01 14:27:58 +00:00
|
|
|
prefs.set('sync.enabled', name);
|
|
|
|
status.state = STATES.connected;
|
|
|
|
schedule(SYNC_INTERVAL);
|
|
|
|
emitStatusChange();
|
|
|
|
},
|
|
|
|
|
|
|
|
async stop() {
|
|
|
|
if (ready.then) await ready;
|
|
|
|
if (!currentDrive) return;
|
|
|
|
chrome.alarms.clear('syncNow');
|
|
|
|
status.state = STATES.disconnecting;
|
|
|
|
emitStatusChange();
|
|
|
|
try {
|
2021-02-14 15:24:49 +00:00
|
|
|
await ctrl.uninit();
|
2021-01-01 14:27:58 +00:00
|
|
|
await tokenMan.revokeToken(currentDrive.name);
|
|
|
|
await chromeLocal.remove(STORAGE_KEY + currentDrive.name);
|
|
|
|
} catch (e) {}
|
|
|
|
currentDrive = null;
|
|
|
|
prefs.set('sync.enabled', 'none');
|
|
|
|
status.state = STATES.disconnected;
|
|
|
|
status.currentDriveName = null;
|
|
|
|
status.login = false;
|
|
|
|
emitStatusChange();
|
|
|
|
},
|
|
|
|
|
2021-12-08 16:00:30 +00:00
|
|
|
async syncNow() {
|
2021-01-01 14:27:58 +00:00
|
|
|
if (ready.then) await ready;
|
2021-02-14 15:24:49 +00:00
|
|
|
if (!currentDrive || !status.login) {
|
|
|
|
console.warn('cannot sync when disconnected');
|
|
|
|
return;
|
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
try {
|
2021-02-14 15:24:49 +00:00
|
|
|
await ctrl.syncNow();
|
2021-01-01 14:27:58 +00:00
|
|
|
status.errorMessage = null;
|
2021-02-09 06:58:30 +00:00
|
|
|
lastError = null;
|
2021-01-01 14:27:58 +00:00
|
|
|
} catch (err) {
|
2021-12-08 16:00:30 +00:00
|
|
|
err.message = translateErrorMessage(err);
|
2021-01-01 14:27:58 +00:00
|
|
|
status.errorMessage = err.message;
|
2021-02-09 06:58:30 +00:00
|
|
|
lastError = err;
|
2021-02-14 15:24:49 +00:00
|
|
|
if (isGrantError(err)) {
|
|
|
|
status.login = false;
|
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
|
|
|
emitStatusChange();
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
//#endregion
|
|
|
|
//#region Utils
|
|
|
|
|
|
|
|
async function initController() {
|
2022-03-31 10:56:51 +00:00
|
|
|
await require(['/vendor/db-to-cloud/db-to-cloud']); /* global dbToCloud */
|
2021-01-01 14:27:58 +00:00
|
|
|
ctrl = dbToCloud.dbToCloud({
|
2022-01-28 23:54:56 +00:00
|
|
|
onGet: styleUtil.uuid2style,
|
|
|
|
async onPut(doc) {
|
|
|
|
const id = uuidIndex.get(doc._id);
|
|
|
|
const oldCust = uuidIndex.custom[id];
|
|
|
|
const oldDoc = oldCust || styleUtil.id2style(id);
|
|
|
|
const diff = oldDoc ? compareRevision(oldDoc._rev, doc._rev) : -1;
|
|
|
|
if (!diff) return;
|
|
|
|
if (diff > 0) {
|
|
|
|
syncMan.putDoc(oldDoc);
|
|
|
|
} else if (oldCust) {
|
|
|
|
uuidIndex.custom[id] = doc;
|
|
|
|
} else {
|
|
|
|
delete doc.id;
|
|
|
|
if (id) doc.id = id;
|
|
|
|
doc.id = await db.styles.put(doc);
|
|
|
|
await styleUtil.handleSave(doc, {reason: 'sync'});
|
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
},
|
2022-01-28 23:54:56 +00:00
|
|
|
onDelete(_id, rev) {
|
|
|
|
const id = uuidIndex.get(_id);
|
|
|
|
const oldDoc = styleUtil.id2style(id);
|
|
|
|
return oldDoc &&
|
|
|
|
compareRevision(oldDoc._rev, rev) <= 0 &&
|
|
|
|
API.styles.delete(id, 'sync');
|
2021-01-01 14:27:58 +00:00
|
|
|
},
|
|
|
|
async onFirstSync() {
|
2022-01-28 23:54:56 +00:00
|
|
|
for (const i of Object.values(uuidIndex.custom).concat(await API.styles.getAll())) {
|
2021-01-01 14:27:58 +00:00
|
|
|
ctrl.put(i._id, i._rev);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
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();
|
|
|
|
},
|
|
|
|
compareRevision,
|
|
|
|
getState(drive) {
|
|
|
|
return chromeLocal.getValue(STORAGE_KEY + drive.name);
|
|
|
|
},
|
|
|
|
setState(drive, state) {
|
|
|
|
return chromeLocal.setValue(STORAGE_KEY + drive.name, state);
|
|
|
|
},
|
2021-12-08 16:00:30 +00:00
|
|
|
retryMaxAttempts: 10,
|
|
|
|
retryExp: 1.2,
|
|
|
|
retryDelay: 6,
|
2021-01-01 14:27:58 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function emitStatusChange() {
|
|
|
|
msg.broadcastExtension({method: 'syncStatusUpdate', status});
|
2021-02-09 09:37:36 +00:00
|
|
|
iconMan.overrideBadge(getErrorBadge());
|
2021-02-09 06:58:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function isNetworkError(err) {
|
2021-02-28 05:37:56 +00:00
|
|
|
return (
|
|
|
|
err.name === 'TypeError' && /networkerror|failed to fetch/i.test(err.message) ||
|
|
|
|
err.code === 502
|
|
|
|
);
|
2021-02-09 06:58:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function isGrantError(err) {
|
|
|
|
if (err.code === 401) return true;
|
|
|
|
if (err.code === 400 && /invalid_grant/.test(err.message)) return true;
|
2021-12-09 04:00:38 +00:00
|
|
|
if (err.name === 'TokenError') return true;
|
2021-02-09 06:58:30 +00:00
|
|
|
return false;
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
|
|
|
|
2021-02-09 09:37:36 +00:00
|
|
|
function getErrorBadge() {
|
2021-02-14 15:24:49 +00:00
|
|
|
if (status.state === STATES.connected &&
|
|
|
|
(!status.login || lastError && !isNetworkError(lastError))) {
|
2021-02-09 09:37:36 +00:00
|
|
|
return {
|
|
|
|
text: 'x',
|
|
|
|
color: '#F00',
|
2021-02-14 15:24:49 +00:00
|
|
|
title: !status.login ? 'syncErrorRelogin' : `${
|
|
|
|
chrome.i18n.getMessage('syncError')
|
|
|
|
}\n---------------------\n${
|
|
|
|
// splitting to limit each line length
|
|
|
|
lastError.message.replace(/.{60,}?\s(?=.{30,})/g, '$&\n')
|
|
|
|
}`,
|
2021-02-09 09:37:36 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-12 00:05:58 +00:00
|
|
|
async function getDrive(name) {
|
|
|
|
if (name === 'dropbox' || name === 'google' || name === 'onedrive' || name === 'webdav') {
|
|
|
|
const options = await syncMan.getDriveOptions(name);
|
|
|
|
options.getAccessToken = () => tokenMan.getToken(name);
|
2022-06-27 09:41:39 +00:00
|
|
|
options.fetch = name === 'webdav' ? fetchWebDAV.bind(options) : fetch;
|
2021-12-12 00:05:58 +00:00
|
|
|
return dbToCloud.drive[name](options);
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
|
|
|
throw new Error(`unknown cloud name: ${name}`);
|
|
|
|
}
|
|
|
|
|
2022-06-27 09:41:39 +00:00
|
|
|
/** @this {Object} DriveOptions */
|
|
|
|
function fetchWebDAV(url, init = {}) {
|
|
|
|
init.credentials = 'omit'; // circumventing nextcloud CSRF token error
|
|
|
|
init.headers = Object.assign({}, init.headers, {
|
|
|
|
Authorization: `Basic ${btoa(`${this.username || ''}:${this.password || ''}`)}`,
|
|
|
|
});
|
|
|
|
return fetch(url, init);
|
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
function schedule(delay = SYNC_DELAY) {
|
|
|
|
chrome.alarms.create('syncNow', {
|
2021-07-06 21:19:18 +00:00
|
|
|
delayInMinutes: delay, // fractional values are supported
|
2021-12-08 16:00:30 +00:00
|
|
|
periodInMinutes: SYNC_INTERVAL,
|
2021-01-01 14:27:58 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-12-08 16:00:30 +00:00
|
|
|
function translateErrorMessage(err) {
|
|
|
|
if (err.name === 'LockError') {
|
|
|
|
return browser.i18n.getMessage('syncErrorLock', new Date(err.expire).toLocaleString([], {timeStyle: 'short'}));
|
|
|
|
}
|
|
|
|
return err.message || String(err);
|
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
//#endregion
|
|
|
|
})();
|