From f9db43a2e9280be5fbb72708d8d65c81f63d5a9e Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 6 Nov 2019 03:30:45 +0800 Subject: [PATCH] Add: sync database to a cloud drive (#787) * 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 --- _locales/en/messages.json | 55 ++++ background/background.js | 10 +- background/style-manager.js | 147 +++++++++-- background/sync.js | 238 ++++++++++++++++++ background/token-manager.js | 226 +++++++++++++++++ js/messaging.js | 6 +- js/prefs.js | 25 +- manifest.json | 11 +- options.html | 21 ++ options/options.css | 9 + options/options.js | 84 ++++++- package.json | 1 + tools/update-libraries.js | 3 + tools/zip.js | 3 +- vendor/codemirror/README.md | 2 +- vendor/codemirror/addon/fold/foldgutter.js | 11 +- vendor/codemirror/keymap/vim.js | 13 +- vendor/codemirror/lib/codemirror.css | 11 +- vendor/codemirror/lib/codemirror.js | 21 +- .../codemirror/mode/javascript/javascript.js | 18 +- vendor/codemirror/theme/yonce.css | 4 +- vendor/db-to-cloud/LICENSE | 22 ++ vendor/db-to-cloud/README.md | 9 + vendor/db-to-cloud/db-to-cloud.min.js | 2 + vendor/dropbox/README.md | 4 +- vendor/dropbox/dropbox-sdk.js | 86 ++++++- vendor/uuid/LICENSE | 21 ++ vendor/uuid/README.md | 9 + vendor/uuid/uuid.min.js | 1 + 29 files changed, 992 insertions(+), 81 deletions(-) create mode 100644 background/sync.js create mode 100644 background/token-manager.js create mode 100644 vendor/db-to-cloud/LICENSE create mode 100644 vendor/db-to-cloud/README.md create mode 100644 vendor/db-to-cloud/db-to-cloud.min.js create mode 100644 vendor/uuid/LICENSE create mode 100644 vendor/uuid/README.md create mode 100644 vendor/uuid/uuid.min.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c8368fad..1292222b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -974,6 +974,9 @@ "optionsCustomizeUpdate": { "message": "Updates" }, + "optionsCustomizeSync": { + "message": "Sync to cloud" + }, "optionsHeading": { "message": "Options", "description": "Heading for options section on manage page." @@ -1009,6 +1012,58 @@ "optionsUpdateInterval": { "message": "Userstyle autoupdate interval in hours (specify 0 to disable)" }, + "optionsSyncNone": { + "message": "None" + }, + "optionsSyncConnect": { + "message": "Connect" + }, + "optionsSyncDisconnect": { + "message": "Disconnect" + }, + "optionsSyncSyncNow": { + "message": "Sync now" + }, + "optionsSyncLogin": { + "message": "Login" + }, + "optionsSyncStatusPull": { + "message": "Pulling style $loaded$ of $total$", + "placeholders": { + "loaded": { + "content": "$1" + }, + "total": { + "content": "$2" + } + } + }, + "optionsSyncStatusPush": { + "message": "Pushing style $loaded$ of $total$", + "placeholders": { + "loaded": { + "content": "$1" + }, + "total": { + "content": "$2" + } + } + }, + "optionsSyncStatusSyncing": { + "message": "Syncing..." + }, + "optionsSyncStatusConnecting": { + "message": "Connecting..." + }, + "optionsSyncStatusConnected": { + "message": "Connected" + }, + "optionsSyncStatusDisconnecting": { + "message": "Disconnecting..." + }, + "optionsSyncStatusDisconnected": { + "message": "Disconnected" + }, "paginationCurrent": { "message": "Current page", "description": "Tooltip for the current page index in search results" diff --git a/background/background.js b/background/background.js index da59e025..80b4364d 100644 --- a/background/background.js +++ b/background/background.js @@ -1,6 +1,6 @@ /* global download prefs openURL FIREFOX CHROME VIVALDI debounce URLS ignoreChromeError getTab - styleManager msg navigatorUtil iconUtil workerUtil contentScripts */ + styleManager msg navigatorUtil iconUtil workerUtil contentScripts sync */ 'use strict'; // eslint-disable-next-line no-var @@ -63,7 +63,13 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, { return browser.runtime.openOptionsPage() .then(() => new Promise(resolve => setTimeout(resolve, 100))) .then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'})); - } + }, + + syncStart: sync.start, + syncStop: sync.stop, + syncNow: sync.syncNow, + getSyncStatus: sync.getStatus, + syncLogin: sync.login }); // eslint-disable-next-line no-var diff --git a/background/style-manager.js b/background/style-manager.js index d75d7b3f..8be5d2ac 100644 --- a/background/style-manager.js +++ b/background/style-manager.js @@ -1,6 +1,6 @@ /* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */ /* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty - getStyleWithNoCode msg */ + getStyleWithNoCode msg sync uuid */ /* exported styleManager */ 'use strict'; @@ -21,6 +21,7 @@ const styleManager = (() => { appliesTo: Set } */ const styles = new Map(); + const uuidIndex = new Map(); /* url => { maybeMatch: Set, @@ -62,11 +63,16 @@ const styleManager = (() => { handleLivePreviewConnections(); - return ensurePrepared({ + return Object.assign({ + compareRevision + }, ensurePrepared({ get, + getByUUID, getSectionsByUrl, + putByUUID, installStyle, deleteStyle, + deleteByUUID, editSave, findStyle, importStyle, @@ -79,7 +85,7 @@ const styleManager = (() => { removeExclusion, addInclusion, removeInclusion - }); + })); function handleLivePreviewConnections() { chrome.runtime.onConnect.addListener(port => { @@ -120,11 +126,48 @@ const styleManager = (() => { return noCode ? getStyleWithNoCode(data) : data; } + function getByUUID(uuid) { + const id = uuidIndex.get(uuid); + if (id) { + return get(id); + } + } + function getAllStyles(noCode = false) { const datas = [...styles.values()].map(s => s.data); return noCode ? datas.map(getStyleWithNoCode) : datas; } + function compareRevision(rev1, rev2) { + return rev1 - rev2; + } + + function putByUUID(doc) { + const id = uuidIndex.get(doc._id); + if (id) { + doc.id = id; + } else { + delete doc.id; + } + const oldDoc = id && styles.has(id) && styles.get(id).data; + let diff = -1; + if (oldDoc) { + diff = compareRevision(oldDoc._rev, doc._rev); + if (diff > 0) { + sync.put(oldDoc._id, oldDoc._rev); + return; + } + } + if (diff < 0) { + return db.exec('put', doc) + .then(event => { + doc.id = event.target.result; + uuidIndex.set(doc._id, doc.id); + return handleSave(doc, 'sync'); + }); + } + } + function toggleStyle(id, enabled) { const style = styles.get(id); const data = Object.assign({}, style.data, {enabled}); @@ -163,12 +206,11 @@ const styleManager = (() => { } function importMany(items) { + items.forEach(beforeSave); return db.exec('putMany', items) .then(events => { for (let i = 0; i < items.length; i++) { - if (!items[i].id) { - items[i].id = events[i].target.result; - } + afterSave(items[i], events[i].target.result); } return Promise.all(items.map(i => handleSave(i, 'import'))); }); @@ -247,10 +289,14 @@ const styleManager = (() => { return removeIncludeExclude(id, rule, 'inclusions'); } - function deleteStyle(id) { + function deleteStyle(id, reason) { const style = styles.get(id); + const rev = Date.now(); return db.exec('delete', id) .then(() => { + if (reason !== 'sync') { + sync.delete(style.data._id, rev); + } for (const url of style.appliesTo) { const cache = cachedStyleForUrl.get(url); if (cache) { @@ -258,6 +304,7 @@ const styleManager = (() => { } } styles.delete(id); + uuidIndex.delete(style.data._id); return msg.broadcast({ method: 'styleDeleted', style: {id} @@ -266,6 +313,15 @@ const styleManager = (() => { .then(() => id); } + function deleteByUUID(_id, rev) { + const id = uuidIndex.get(_id); + const oldDoc = id && styles.has(id) && styles.get(id).data; + if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) { + // FIXME: does it make sense to set reason to 'sync' in deleteByUUID? + return deleteStyle(id, 'sync'); + } + } + function ensurePrepared(methods) { const prepared = {}; for (const [name, fn] of Object.entries(methods)) { @@ -320,19 +376,33 @@ const styleManager = (() => { }); } - function saveStyle(style) { + function beforeSave(style) { if (!style.name) { throw new Error('style name is empty'); } if (style.id == null) { delete style.id; } + if (!style._id) { + style._id = uuid(); + } + style._rev = Date.now(); fixUsoMd5Issue(style); + } + + function afterSave(style, newId) { + if (style.id == null) { + style.id = newId; + } + uuidIndex.set(style._id, style.id); + sync.put(style._id, style._rev); + } + + function saveStyle(style) { + beforeSave(style); return db.exec('put', style) .then(event => { - if (style.id == null) { - style.id = event.target.result; - } + afterSave(style, event.target.result); return style; }); } @@ -451,22 +521,49 @@ const styleManager = (() => { } function prepare() { - return db.exec('getAll').then(event => { - const styleList = event.target.result; - if (!styleList) { - return; - } - for (const style of styleList) { - fixUsoMd5Issue(style); - styles.set(style.id, { - appliesTo: new Set(), - data: style - }); - if (!style.name) { - style.name = 'ID: ' + style.id; + const ADD_MISSING_PROPS = { + name: style => `ID: ${style.id}`, + _id: () => uuid(), + _rev: () => Date.now() + }; + + return db.exec('getAll') + .then(event => event.target.result || []) + .then(styleList => { + // setup missing _id, _rev + const updated = []; + for (const style of styleList) { + if (addMissingProperties(style)) { + updated.push(style); + } + } + if (updated.length) { + return db.exec('putMany', updated) + .then(() => styleList); + } + return styleList; + }) + .then(styleList => { + for (const style of styleList) { + fixUsoMd5Issue(style); + styles.set(style.id, { + appliesTo: new Set(), + data: style + }); + uuidIndex.set(style._id, style.id); + } + }); + + function addMissingProperties(style) { + let touched = false; + for (const key in ADD_MISSING_PROPS) { + if (!style[key]) { + style[key] = ADD_MISSING_PROPS[key](style); + touched = true; } } - }); + return touched; + } } function urlMatchStyle(query, style) { diff --git a/background/sync.js b/background/sync.js new file mode 100644 index 00000000..ca9bab90 --- /dev/null +++ b/background/sync.js @@ -0,0 +1,238 @@ +/* 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(); + } + ); + } +})(); diff --git a/background/token-manager.js b/background/token-manager.js new file mode 100644 index 00000000..9c21f85a --- /dev/null +++ b/background/token-manager.js @@ -0,0 +1,226 @@ +/* global chromeLocal promisify */ +/* exported tokenManager */ +'use strict'; + +const tokenManager = (() => { + const launchWebAuthFlow = promisify(chrome.identity.launchWebAuthFlow.bind(chrome.identity)); + const AUTH = { + dropbox: { + flow: 'token', + clientId: 'zg52vphuapvpng9', + authURL: 'https://www.dropbox.com/oauth2/authorize', + tokenURL: 'https://api.dropboxapi.com/oauth2/token', + revoke: token => + fetch('https://api.dropboxapi.com/2/auth/token/revoke', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}` + } + }) + }, + google: { + flow: 'code', + clientId: '283762574871-d4u58s4arra5jdan2gr00heasjlttt1e.apps.googleusercontent.com', + clientSecret: 'J0nc5TlR_0V_ex9-sZk-5faf', + authURL: 'https://accounts.google.com/o/oauth2/v2/auth', + authQuery: { + // NOTE: Google needs 'prompt' parameter to deliver multiple refresh + // tokens for multiple machines. + // https://stackoverflow.com/q/18519185 + access_type: 'offline', + prompt: 'consent' + }, + tokenURL: 'https://oauth2.googleapis.com/token', + scopes: ['https://www.googleapis.com/auth/drive.appdata'], + revoke: token => { + const params = {token}; + return postQuery(`https://accounts.google.com/o/oauth2/revoke?${stringifyQuery(params)}`); + } + }, + onedrive: { + flow: 'code', + clientId: '3864ce03-867c-4ad8-9856-371a097d47b1', + clientSecret: '9Pj=TpsrStq8K@1BiwB9PIWLppM:@s=w', + authURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', + tokenURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', + redirect_uri: 'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/', + scopes: ['Files.ReadWrite.AppFolder', 'offline_access'] + } + }; + const NETWORK_LATENCY = 30; // seconds + + return {getToken, revokeToken, getClientId, buildKeys}; + + function getClientId(name) { + return AUTH[name].clientId; + } + + function buildKeys(name) { + const k = { + TOKEN: `secure/token/${name}/token`, + EXPIRE: `secure/token/${name}/expire`, + REFRESH: `secure/token/${name}/refresh`, + }; + k.LIST = Object.values(k); + return k; + } + + function getToken(name, interactive) { + const k = buildKeys(name); + return chromeLocal.get(k.LIST) + .then(obj => { + if (!obj[k.TOKEN]) { + return authUser(name, k, interactive); + } + if (!obj[k.EXPIRE] || Date.now() < obj[k.EXPIRE]) { + return obj[k.TOKEN]; + } + if (obj[k.REFRESH]) { + return refreshToken(name, k, obj) + .catch(err => { + if (err.code === 401) { + return authUser(name, k, interactive); + } + throw err; + }); + } + return authUser(name, k, interactive); + }); + } + + function revokeToken(name) { + const provider = AUTH[name]; + const k = buildKeys(name); + return revoke() + .then(() => chromeLocal.remove(k.LIST)); + + function revoke() { + if (!provider.revoke) { + return Promise.resolve(); + } + return chromeLocal.get(k.TOKEN) + .then(obj => { + if (obj[k.TOKEN]) { + return provider.revoke(obj[k.TOKEN]); + } + }) + .catch(console.error); + } + } + + function refreshToken(name, k, obj) { + if (!obj[k.REFRESH]) { + return Promise.reject(new Error('no refresh token')); + } + const provider = AUTH[name]; + const body = { + client_id: provider.clientId, + refresh_token: obj[k.REFRESH], + grant_type: 'refresh_token', + scope: provider.scopes.join(' ') + }; + if (provider.clientSecret) { + body.client_secret = provider.clientSecret; + } + return postQuery(provider.tokenURL, body) + .then(result => { + if (!result.refresh_token) { + // reuse old refresh token + result.refresh_token = obj[k.REFRESH]; + } + return handleTokenResult(result, k); + }); + } + + function stringifyQuery(obj) { + const search = new URLSearchParams(); + for (const key of Object.keys(obj)) { + search.set(key, obj[key]); + } + return search.toString(); + } + + function authUser(name, k, interactive = false) { + const provider = AUTH[name]; + const state = Math.random().toFixed(8).slice(2); + const query = { + response_type: provider.flow, + client_id: provider.clientId, + redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL(), + state + }; + if (provider.scopes) { + query.scope = provider.scopes.join(' '); + } + if (provider.authQuery) { + Object.assign(query, provider.authQuery); + } + const url = `${provider.authURL}?${stringifyQuery(query)}`; + return launchWebAuthFlow({ + url, + interactive + }) + .then(url => { + const params = new URLSearchParams( + provider.flow === 'token' ? + new URL(url).hash.slice(1) : + new URL(url).search.slice(1) + ); + if (params.get('state') !== state) { + throw new Error(`unexpected state: ${params.get('state')}, expected: ${state}`); + } + if (provider.flow === 'token') { + const obj = {}; + for (const [key, value] of params.entries()) { + obj[key] = value; + } + return obj; + } + const code = params.get('code'); + const body = { + code, + grant_type: 'authorization_code', + client_id: provider.clientId, + redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL() + }; + if (provider.clientSecret) { + body.client_secret = provider.clientSecret; + } + return postQuery(provider.tokenURL, body); + }) + .then(result => handleTokenResult(result, k)); + } + + function handleTokenResult(result, k) { + return chromeLocal.set({ + [k.TOKEN]: result.access_token, + [k.EXPIRE]: result.expires_in ? Date.now() + (Number(result.expires_in) - NETWORK_LATENCY) * 1000 : undefined, + [k.REFRESH]: result.refresh_token + }) + .then(() => result.access_token); + } + + function postQuery(url, body) { + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + if (body) { + options.body = stringifyQuery(body); + } + return fetch(url, options) + .then(r => { + if (r.ok) { + return r.json(); + } + return r.text() + .then(body => { + const err = new Error(`failed to fetch (${r.status}): ${body}`); + err.code = r.status; + throw err; + }); + }); + } +})(); diff --git a/js/messaging.js b/js/messaging.js index 2aba882a..8cabad39 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -1,6 +1,6 @@ /* exported getActiveTab onTabReady stringAsRegExp getTabRealURL openURL getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual - closeCurrentTab */ + closeCurrentTab capitalize */ 'use strict'; const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]); @@ -509,3 +509,7 @@ function closeCurrentTab() { } }); } + +function capitalize(s) { + return s[0].toUpperCase() + s.slice(1); +} diff --git a/js/prefs.js b/js/prefs.js index 03cbd31f..4ac7cf3d 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -88,6 +88,8 @@ const prefs = (() => { 'hotkey.openManage': '', 'hotkey.styleDisableAll': '', + 'sync.enabled': 'none', + 'iconset': 0, // 0 = dark-themed icon // 1 = light-themed icon @@ -105,7 +107,10 @@ const prefs = (() => { specific: new Map(), }; - const initializing = promisify(chrome.storage.sync.get.bind(chrome.storage.sync))('settings') + const syncSet = promisify(chrome.storage.sync.set.bind(chrome.storage.sync)); + const syncGet = promisify(chrome.storage.sync.get.bind(chrome.storage.sync)); + + const initializing = syncGet('settings') .then(result => { if (result.settings) { setAll(result.settings, true); @@ -208,10 +213,10 @@ const prefs = (() => { } values[key] = value; emitChange(key, value); - if (synced || timer) { - return; + if (!synced && !timer) { + timer = syncPrefsLater(); } - timer = setTimeout(syncPrefs); + return timer; } function emitChange(key, value) { @@ -228,10 +233,14 @@ const prefs = (() => { } } - function syncPrefs() { - // FIXME: we always set the entire object? Ideally, this should only use `changes`. - chrome.storage.sync.set({settings: values}); - timer = null; + function syncPrefsLater() { + return new Promise((resolve, reject) => { + setTimeout(() => { + timer = null; + syncSet({settings: values}) + .then(resolve, reject); + }); + }); } function equal(a, b) { diff --git a/manifest.json b/manifest.json index 385f9663..e4e0b67b 100644 --- a/manifest.json +++ b/manifest.json @@ -35,6 +35,11 @@ "js/script-loader.js", "js/usercss.js", "js/cache.js", + "vendor/semver-bundle/semver.js", + "vendor/db-to-cloud/db-to-cloud.min.js", + "vendor/uuid/uuid.min.js", + "background/token-manager.js", + "background/sync.js", "background/content-scripts.js", "background/db.js", "background/style-manager.js", @@ -45,8 +50,7 @@ "background/style-via-api.js", "background/search-db.js", "background/update.js", - "background/openusercss-api.js", - "vendor/semver-bundle/semver.js" + "background/openusercss-api.js" ] }, "commands": { @@ -130,5 +134,6 @@ "id": "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}", "strict_min_version": "53.0" } - } + }, + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2ypG+Z/beZtoYrxxwXYhMwQiAiwRVnSHqdpOSzJdjsXVWdvJjlgEuZcU8kte75w58P45LsRUrwvU6N9x12S6eW84KNEBC8rlZj0RGNoxuhSAcdxneYzjJ9tBkZKOidVedYHHsi3LeaXiLuTNTBR+2lf3uCNcP0ebaFML9uDLdYTGEW4eL3hnEKYPSmT1/xkh4bSGTToCg4YNuWWWoTA0beEOpBWYkPVMarLDQgPzMN5Byu5w3lOS2zL0PPJcmdyk3ez/ZsB4PZKU+h17fVA6+YTvUfxUqLde5i2RiuZhEb6Coo5/W90ZW1yCDC9osjWrxMGYeUMQWHPIgFtDhk4K6QIDAQAB" } diff --git a/options.html b/options.html index af620626..f7c51576 100644 --- a/options.html +++ b/options.html @@ -137,6 +137,27 @@ +
+

+
+ +
+ + + + +
+
+
+

diff --git a/options/options.css b/options/options.css index 42100482..570c6341 100644 --- a/options/options.css +++ b/options/options.css @@ -313,3 +313,12 @@ html:not(.firefox):not(.opera) #updates { hyphens: auto; } } + +.sync-status { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.sync-status::first-letter { + text-transform: uppercase; +} diff --git a/options/options.js b/options/options.js index cf805ffe..cd1f9e74 100644 --- a/options/options.js +++ b/options/options.js @@ -1,7 +1,7 @@ /* global messageBox msg setupLivePrefs enforceInputRange $ $$ $create $createLink FIREFOX OPERA CHROME URLS openURL prefs t API ignoreChromeError - CHROME_HAS_BORDER_BUG */ + CHROME_HAS_BORDER_BUG capitalize */ 'use strict'; setupLivePrefs(); @@ -82,6 +82,88 @@ document.onclick = e => { } }; +// sync to cloud +(() => { + const cloud = document.querySelector('.sync-options .cloud-name'); + const connectButton = document.querySelector('.sync-options .connect'); + const disconnectButton = document.querySelector('.sync-options .disconnect'); + const syncButton = document.querySelector('.sync-options .sync-now'); + const statusText = document.querySelector('.sync-options .sync-status'); + const loginButton = document.querySelector('.sync-options .sync-login'); + + let status = {}; + + msg.onExtension(e => { + if (e.method === 'syncStatusUpdate') { + status = e.status; + updateButtons(); + } + }); + + API.getSyncStatus() + .then(_status => { + status = _status; + updateButtons(); + }); + + function validClick(e) { + return e.button === 0 && !e.ctrl && !e.alt && !e.shift; + } + + cloud.addEventListener('change', updateButtons); + + function updateButtons() { + if (status.currentDriveName) { + cloud.value = status.currentDriveName; + } + cloud.disabled = status.state !== 'disconnected'; + connectButton.disabled = status.state !== 'disconnected' || cloud.value === 'none'; + disconnectButton.disabled = status.state !== 'connected' || status.syncing; + syncButton.disabled = status.state !== 'connected' || status.syncing; + statusText.textContent = getStatusText(); + loginButton.style.display = status.state === 'connected' && !status.login ? '' : 'none'; + } + + function getStatusText() { + if (status.syncing) { + if (status.progress) { + const {phase, loaded, total} = status.progress; + return chrome.i18n.getMessage(`optionsSyncStatus${capitalize(phase)}`, [loaded + 1, total]) || + `${phase} ${loaded} / ${total}`; + } + return chrome.i18n.getMessage('optionsSyncStatusSyncing') || 'syncing'; + } + if ((status.state === 'connected' || status.state === 'disconnected') && status.errorMessage) { + return status.errorMessage; + } + return chrome.i18n.getMessage(`optionsSyncStatus${capitalize(status.state)}`) || status.state; + } + + connectButton.addEventListener('click', e => { + if (validClick(e)) { + API.syncStart(cloud.value).catch(console.error); + } + }); + + disconnectButton.addEventListener('click', e => { + if (validClick(e)) { + API.syncStop().catch(console.error); + } + }); + + syncButton.addEventListener('click', e => { + if (validClick(e)) { + API.syncNow().catch(console.error); + } + }); + + loginButton.addEventListener('click', e => { + if (validClick(e)) { + API.syncLogin().catch(console.error); + } + }); +})(); + function checkUpdates() { let total = 0; let checked = 0; diff --git a/package.json b/package.json index 4dae4d5b..d074a5e6 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "devDependencies": { "archiver": "^3.1.1", "codemirror": "^5.48.4", + "db-to-cloud": "^0.4.5", "dropbox": "^4.0.30", "endent": "^1.3.0", "eslint": "^6.3.0", diff --git a/tools/update-libraries.js b/tools/update-libraries.js index 9a14f4ec..7abc6e87 100644 --- a/tools/update-libraries.js +++ b/tools/update-libraries.js @@ -40,6 +40,9 @@ const files = { 'vendor/inflate.js → inflate.js', 'vendor/z-worker.js → z-worker.js', 'vendor/zip.js → zip.js' + ], + 'db-to-cloud': [ + 'dist/db-to-cloud.min.js → db-to-cloud.min.js' ] }; diff --git a/tools/zip.js b/tools/zip.js index ac51e543..8092b1a2 100644 --- a/tools/zip.js +++ b/tools/zip.js @@ -14,7 +14,8 @@ function createZip() { 'package.json', 'package-lock.json', 'yarn.lock', - '*.zip' + '*.zip', + '*.map' ]; const file = fs.createWriteStream(fileName); diff --git a/vendor/codemirror/README.md b/vendor/codemirror/README.md index 1d40e0e4..4aeeeef6 100644 --- a/vendor/codemirror/README.md +++ b/vendor/codemirror/README.md @@ -1,3 +1,3 @@ -## CodeMirror v5.48.0 +## CodeMirror v5.48.4 Only files & folders that exist in the `vendor/codemirror` folder are copied from the `node_modules/codemirror` folder. Except all theme files are copied, in case new themes have been added. diff --git a/vendor/codemirror/addon/fold/foldgutter.js b/vendor/codemirror/addon/fold/foldgutter.js index 988c67c4..fcb5021e 100644 --- a/vendor/codemirror/addon/fold/foldgutter.js +++ b/vendor/codemirror/addon/fold/foldgutter.js @@ -51,8 +51,13 @@ function isFolded(cm, line) { var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0)); - for (var i = 0; i < marks.length; ++i) - if (marks[i].__isFold && marks[i].find().from.line == line) return marks[i]; + for (var i = 0; i < marks.length; ++i) { + if (marks[i].__isFold) { + var fromPos = marks[i].find(-1); + if (fromPos && fromPos.line === line) + return marks[i]; + } + } } function marker(spec) { @@ -100,7 +105,7 @@ if (gutter != opts.gutter) return; var folded = isFolded(cm, line); if (folded) folded.clear(); - else cm.foldCode(Pos(line, 0), opts.rangeFinder); + else cm.foldCode(Pos(line, 0), opts); } function onChange(cm) { diff --git a/vendor/codemirror/keymap/vim.js b/vendor/codemirror/keymap/vim.js index 3be7a3f4..95b19b24 100644 --- a/vendor/codemirror/keymap/vim.js +++ b/vendor/codemirror/keymap/vim.js @@ -4073,7 +4073,7 @@ } // Unescape \ and / in the replace part, for PCRE mode. - var unescapes = {'\\/': '/', '\\\\': '\\', '\\n': '\n', '\\r': '\r', '\\t': '\t'}; + var unescapes = {'\\/': '/', '\\\\': '\\', '\\n': '\n', '\\r': '\r', '\\t': '\t', '\\&':'&'}; function unescapeRegexReplace(str) { var stream = new CodeMirror.StringStream(str); var output = []; @@ -4861,6 +4861,9 @@ var global = false; // True to replace all instances on a line, false to replace only 1. if (tokens.length) { regexPart = tokens[0]; + if (getOption('pcre') && regexPart !== '') { + regexPart = new RegExp(regexPart).source; //normalize not escaped characters + } replacePart = tokens[1]; if (regexPart && regexPart[regexPart.length - 1] === '$') { regexPart = regexPart.slice(0, regexPart.length - 1) + '\\n'; @@ -4868,7 +4871,7 @@ } if (replacePart !== undefined) { if (getOption('pcre')) { - replacePart = unescapeRegexReplace(replacePart); + replacePart = unescapeRegexReplace(replacePart.replace(/([^\\])&/g,"$1$$&")); } else { replacePart = translateRegexReplace(replacePart); } @@ -4899,7 +4902,11 @@ global = true; flagsPart.replace('g', ''); } - regexPart = regexPart.replace(/\//g, "\\/") + '/' + flagsPart; + if (getOption('pcre')) { + regexPart = regexPart + '/' + flagsPart; + } else { + regexPart = regexPart.replace(/\//g, "\\/") + '/' + flagsPart; + } } } if (regexPart) { diff --git a/vendor/codemirror/lib/codemirror.css b/vendor/codemirror/lib/codemirror.css index c7a8ae70..bc910fb9 100644 --- a/vendor/codemirror/lib/codemirror.css +++ b/vendor/codemirror/lib/codemirror.css @@ -13,7 +13,8 @@ .CodeMirror-lines { padding: 4px 0; /* Vertical padding around content */ } -.CodeMirror pre { +.CodeMirror pre.CodeMirror-line, +.CodeMirror pre.CodeMirror-line-like { padding: 0 4px; /* Horizontal padding of content */ } @@ -96,7 +97,7 @@ .CodeMirror-rulers { position: absolute; - left: 0; right: 0; top: -50px; bottom: -20px; + left: 0; right: 0; top: -50px; bottom: 0; overflow: hidden; } .CodeMirror-ruler { @@ -236,7 +237,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} cursor: text; min-height: 1px; /* prevents collapsing before first draw */ } -.CodeMirror pre { +.CodeMirror pre.CodeMirror-line, +.CodeMirror pre.CodeMirror-line-like { /* Reset some styles that the rest of the page might have set */ -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; border-width: 0; @@ -255,7 +257,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} -webkit-font-variant-ligatures: contextual; font-variant-ligatures: contextual; } -.CodeMirror-wrap pre { +.CodeMirror-wrap pre.CodeMirror-line, +.CodeMirror-wrap pre.CodeMirror-line-like { word-wrap: break-word; white-space: pre-wrap; word-break: normal; diff --git a/vendor/codemirror/lib/codemirror.js b/vendor/codemirror/lib/codemirror.js index 3ca19118..076a0b89 100644 --- a/vendor/codemirror/lib/codemirror.js +++ b/vendor/codemirror/lib/codemirror.js @@ -2284,7 +2284,7 @@ function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight} function paddingH(display) { if (display.cachedPaddingH) { return display.cachedPaddingH } - var e = removeChildrenAndAdd(display.measure, elt("pre", "x")); + var e = removeChildrenAndAdd(display.measure, elt("pre", "x", "CodeMirror-line-like")); var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; if (!isNaN(data.left) && !isNaN(data.right)) { display.cachedPaddingH = data; } @@ -2678,7 +2678,7 @@ function PosWithInfo(line, ch, sticky, outside, xRel) { var pos = Pos(line, ch, sticky); pos.xRel = xRel; - if (outside) { pos.outside = true; } + if (outside) { pos.outside = outside; } return pos } @@ -2687,16 +2687,16 @@ function coordsChar(cm, x, y) { var doc = cm.doc; y += cm.display.viewOffset; - if (y < 0) { return PosWithInfo(doc.first, 0, null, true, -1) } + if (y < 0) { return PosWithInfo(doc.first, 0, null, -1, -1) } var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; if (lineN > last) - { return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, true, 1) } + { return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, 1, 1) } if (x < 0) { x = 0; } var lineObj = getLine(doc, lineN); for (;;) { var found = coordsCharInner(cm, lineObj, lineN, x, y); - var collapsed = collapsedSpanAround(lineObj, found.ch + (found.xRel > 0 ? 1 : 0)); + var collapsed = collapsedSpanAround(lineObj, found.ch + (found.xRel > 0 || found.outside > 0 ? 1 : 0)); if (!collapsed) { return found } var rangeEnd = collapsed.find(1); if (rangeEnd.line == lineN) { return rangeEnd } @@ -2784,7 +2784,7 @@ // base X position var coords = cursorCoords(cm, Pos(lineNo$$1, ch, sticky), "line", lineObj, preparedMeasure); baseX = coords.left; - outside = y < coords.top || y >= coords.bottom; + outside = y < coords.top ? -1 : y >= coords.bottom ? 1 : 0; } ch = skipExtendingChars(lineObj.text, ch, 1); @@ -2853,7 +2853,7 @@ function textHeight(display) { if (display.cachedTextHeight != null) { return display.cachedTextHeight } if (measureText == null) { - measureText = elt("pre"); + measureText = elt("pre", null, "CodeMirror-line-like"); // Measure a bunch of lines, for browsers that compute // fractional heights. for (var i = 0; i < 49; ++i) { @@ -2873,7 +2873,7 @@ function charWidth(display) { if (display.cachedCharWidth != null) { return display.cachedCharWidth } var anchor = elt("span", "xxxxxxxxxx"); - var pre = elt("pre", [anchor]); + var pre = elt("pre", [anchor], "CodeMirror-line-like"); removeChildrenAndAdd(display.measure, pre); var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; if (width > 2) { display.cachedCharWidth = width; } @@ -5403,6 +5403,9 @@ if (doc.cm) { makeChangeSingleDocInEditor(doc.cm, change, spans); } else { updateDoc(doc, change, spans); } setSelectionNoUndo(doc, selAfter, sel_dontScroll); + + if (doc.cantEdit && skipAtomic(doc, Pos(doc.firstLine(), 0))) + { doc.cantEdit = false; } } // Handle the interaction of a change to a document with the editor @@ -9755,7 +9758,7 @@ addLegacyProps(CodeMirror); - CodeMirror.version = "5.48.0"; + CodeMirror.version = "5.48.4"; return CodeMirror; diff --git a/vendor/codemirror/mode/javascript/javascript.js b/vendor/codemirror/mode/javascript/javascript.js index eb67187d..8055f1ba 100644 --- a/vendor/codemirror/mode/javascript/javascript.js +++ b/vendor/codemirror/mode/javascript/javascript.js @@ -67,7 +67,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { if (ch == '"' || ch == "'") { state.tokenize = tokenString(ch); return state.tokenize(stream, state); - } else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) { + } else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) { return ret("number", "number"); } else if (ch == "." && stream.match("..")) { return ret("spread", "meta"); @@ -75,10 +75,10 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { return ret(ch); } else if (ch == "=" && stream.eat(">")) { return ret("=>", "operator"); - } else if (ch == "0" && stream.match(/^(?:x[\da-f]+|o[0-7]+|b[01]+)n?/i)) { + } else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) { return ret("number", "number"); } else if (/\d/.test(ch)) { - stream.match(/^\d*(?:n|(?:\.\d*)?(?:[eE][+\-]?\d+)?)?/); + stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/); return ret("number", "number"); } else if (ch == "/") { if (stream.eat("*")) { @@ -195,8 +195,12 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { ++depth; } else if (wordRE.test(ch)) { sawSomething = true; - } else if (/["'\/]/.test(ch)) { - return; + } else if (/["'\/`]/.test(ch)) { + for (;; --pos) { + if (pos == 0) return + var next = stream.string.charAt(pos - 1) + if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break } + } } else if (sawSomething && !depth) { ++pos; break; @@ -525,7 +529,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { cx.marked = "keyword" return cont(objprop) } else if (type == "[") { - return cont(expression, maybetypeOrIn, expect("]"), afterprop); + return cont(expression, maybetype, expect("]"), afterprop); } else if (type == "spread") { return cont(expressionNoComma, afterprop); } else if (value == "*") { @@ -621,7 +625,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { } else if (type == ":") { return cont(typeexpr) } else if (type == "[") { - return cont(expect("variable"), maybetype, expect("]"), typeprop) + return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop) } else if (type == "(") { return pass(functiondecl, typeprop) } diff --git a/vendor/codemirror/theme/yonce.css b/vendor/codemirror/theme/yonce.css index e01c0c3b..975f0788 100644 --- a/vendor/codemirror/theme/yonce.css +++ b/vendor/codemirror/theme/yonce.css @@ -24,8 +24,8 @@ .cm-s-yonce .CodeMirror-activeline .CodeMirror-linenumber.CodeMirror-gutter-elt { background: #1C1C1C; color: #fc4384; } .cm-s-yonce .CodeMirror-linenumber { color: #777; } .cm-s-yonce .CodeMirror-cursor { border-left: 2px solid #FC4384; } -.cm-s-yonce .cm-searching { background: rgb(243, 155, 53, .3) !important; outline: 1px solid #F39B35; } -.cm-s-yonce .cm-searching.CodeMirror-selectedtext { background: rgb(243, 155, 53, .7) !important; color: white; } +.cm-s-yonce .cm-searching { background: rgba(243, 155, 53, .3) !important; outline: 1px solid #F39B35; } +.cm-s-yonce .cm-searching.CodeMirror-selectedtext { background: rgba(243, 155, 53, .7) !important; color: white; } .cm-s-yonce .cm-keyword { color: #00A7AA; } /**/ .cm-s-yonce .cm-atom { color: #F39B35; } diff --git a/vendor/db-to-cloud/LICENSE b/vendor/db-to-cloud/LICENSE new file mode 100644 index 00000000..ff1af80e --- /dev/null +++ b/vendor/db-to-cloud/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2019 eight + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/db-to-cloud/README.md b/vendor/db-to-cloud/README.md new file mode 100644 index 00000000..49b759a1 --- /dev/null +++ b/vendor/db-to-cloud/README.md @@ -0,0 +1,9 @@ +## db-to-cloud v0.4.5 + +Installed via npm - source code: + +https://github.com/eight04/db-to-cloud/tree/v0.4.5 + +Bundled code: + +https://unpkg.com/db-to-cloud@0.4.5/dist/db-to-cloud.min.js diff --git a/vendor/db-to-cloud/db-to-cloud.min.js b/vendor/db-to-cloud/db-to-cloud.min.js new file mode 100644 index 00000000..ca1f42bf --- /dev/null +++ b/vendor/db-to-cloud/db-to-cloud.min.js @@ -0,0 +1,2 @@ +var dbToCloud=function(t){"use strict";function e(t,e,n,o,r,i,c){try{var a=t[i](c),l=a.value}catch(t){return void n(t)}a.done?e(l):Promise.resolve(l).then(o,r)}function n(t){return function(){var n=this,o=arguments;return new Promise((function(r,i){var c=t.apply(n,o);function a(t){e(c,r,i,a,l,"next",t)}function l(t){e(c,r,i,a,l,"throw",t)}a(void 0)}))}}function o(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function r(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);e&&(o=o.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,o)}return n}function i(t){for(var e=1;e=0||(r[n]=t[n]);return r}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(r[n]=t[n])}return r}function a(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){if(!(Symbol.iterator in Object(t)||"[object Arguments]"===Object.prototype.toString.call(t)))return;var n=[],o=!0,r=!1,i=void 0;try{for(var c,a=t[Symbol.iterator]();!(o=(c=a.next()).done)&&(n.push(c.value),!e||n.length!==e);o=!0);}catch(t){r=!0,i=t}finally{try{o||null==a.return||a.return()}finally{if(r)throw i}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}function l({maxActiveReader:t=1/0}={}){let e,n,o=0;const r={read:t=>i(t,!1),write:t=>i(t,!0),length:0};return r;function i(i,a){const l=function({fn:t,block:e=!1,prev:n,next:o,q:r=c(),q2:i=(t.length?c():null)}){return{fn:t,block:e,prev:n,next:o,q:r,q2:i}}({fn:i,block:a});return n?(n.next=l,l.prev=n,n=l,e||(e=n)):e=n=l,r.length++,function i(){const c=e;if(!c||c.block&&c.prev||c.prev&&c.prev.block||o>=t)return;c.block||o++;e=c.next;let a;try{a=c.fn(c.q2&&c.q2.resolve)}catch(t){return c.q.reject(t),void l()}c.q2&&c.q2.promise.then(s);if(a&&a.then){const t=a.then(c.q.resolve,c.q.reject);c.q2||t.then(l)}else if(c.q.resolve(a),!c.q2)return void l();i();function l(){s()}function s(t){c.prev&&(c.prev.next=c.next),c.next&&(c.next.prev=c.prev),n===c&&(n=c.prev),c.block||o--,r.length--,t&&t(),i()}}(),l.q.promise}function c(){const t={};return t.promise=new Promise((e,n)=>{t.resolve=e,t.reject=n}),t}}function s(t){let e,n=0;return()=>(n&&clearTimeout(n),n=setTimeout(o),e||(e=function(){const t={};return t.promise=new Promise((e,n)=>{t.resolve=e,t.reject=n}),t}()),e.promise);function o(){Promise.resolve(t()).then(e.resolve,e.reject),n=0,e=null}}const u={};function p(t){return String.fromCharCode(parseInt(t.slice(1),16))}function f(t){return"%".concat("00".concat(t.charCodeAt(0).toString(16)).slice(-2))}Object.defineProperty(u,"__esModule",{value:!0}),u.encode=function(t){return btoa(encodeURIComponent(t).replace(/%[0-9A-F]{2}/g,p))},u.decode=function(t){return decodeURIComponent(Array.from(atob(t),f).join(""))};class d extends Error{constructor(t,e,n=e&&e.status){super(t),this.code=n,this.origin=e,Error.captureStackTrace&&Error.captureStackTrace(this,d)}}function h(t){return new Promise(e=>setTimeout(e,t))}function y({fetch:t,cooldown:e=0,getAccessToken:o}){const r=l();return t=>r.write(function(){var o=n((function*(n){try{return yield function(t){return a.apply(this,arguments)}(t)}finally{e&&t.method&&"GET"!==t.method?setTimeout(n,e):n()}}));return function(t){return o.apply(this,arguments)}}());function a(){return(a=n((function*(e){let n=e.path,r=e.contentType,a=e.headers,l=e.format,s=c(e,["path","contentType","headers","format"]);const u={Authorization:"Bearer ".concat(yield o())};for(r&&(u["Content-Type"]=r),Object.assign(u,a);;){const e=yield t(n,i({headers:u},s));if(!e.ok){const t=e.headers.get("Retry-After");if(t){const e=Number(t);if(e){yield h(1e3*e);continue}}const n=yield e.text();throw new d("failed to fetch [".concat(e.status,"]: ").concat(n),e)}if(l)return yield e[l]();const o=e.headers.get("Content-Type");return/application\/json/.test(o)?yield e.json():yield e.text()}}))).apply(this,arguments)}}var g=Object.freeze({fsDrive:()=>{},github:function({userAgent:t="db-to-cloud",owner:e,repo:o,getAccessToken:r,fetch:i=("undefined"!=typeof self?self:global).fetch}){const c=y({fetch:i,getAccessToken:r,cooldown:1e3}),a=new Map;return{name:"github",get:p,put:d,post:function(t,e){return d(t,e,!1)},delete:function(t){return g.apply(this,arguments)},list:function(t){return s.apply(this,arguments)},shaCache:a};function l(e){return e.headers||(e.headers={}),e.headers["User-Agent"]||(e.headers["User-Agent"]=t),e.headers.Accept||(e.headers.Accept="application/vnd.github.v3+json"),e.path="https://api.github.com".concat(e.path),c(e)}function s(){return(s=n((function*(t){const n=yield l({path:"/repos/".concat(e,"/").concat(o,"/contents/").concat(t)}),r=[];var i=!0,c=!1,s=void 0;try{for(var u,p=n[Symbol.iterator]();!(i=(u=p.next()).done);i=!0){const t=u.value;r.push(t.name),a.set(t.path,t.sha)}}catch(t){c=!0,s=t}finally{try{i||null==p.return||p.return()}finally{if(c)throw s}}return r}))).apply(this,arguments)}function p(t){return f.apply(this,arguments)}function f(){return(f=n((function*(t){const n=yield l({path:"/repos/".concat(e,"/").concat(o,"/contents/").concat(t)});return a.set(n.path,n.sha),u.decode(n.content)}))).apply(this,arguments)}function d(t,e){return h.apply(this,arguments)}function h(){return(h=n((function*(t,n,r=!0){const i={message:"",content:u.encode(n)};r&&a.has(t)&&(i.sha=a.get(t));const c={method:"PUT",path:"/repos/".concat(e,"/").concat(o,"/contents/").concat(t),contentType:"application/json",body:JSON.stringify(i)};let s,f=!1;for(;!s;){try{s=yield l(c)}catch(e){if(422!==e.code||!e.message.includes('\\"sha\\" wasn\'t supplied'))throw e;if(!r||f)throw e.code="EEXIST",e;yield p(t)}f=!0}a.set(t,s.content.sha)}))).apply(this,arguments)}function g(){return(g=n((function*(t){try{let n=a.get(t);n||(yield p(t),n=a.get(t)),yield l({method:"DELETE",path:"/repos/".concat(e,"/").concat(o,"/contents/").concat(t),body:JSON.stringify({message:"",sha:n})})}catch(t){if(404===t.code)return;throw t}}))).apply(this,arguments)}},dropbox:function({getAccessToken:t,fetch:e=("undefined"!=typeof self?self:global).fetch}){const o=y({fetch:e,getAccessToken:t});return{name:"dropbox",get:function(t){return s.apply(this,arguments)},put:u,post:function(t,e){return f.apply(this,arguments)},delete:function(t){return d.apply(this,arguments)},list:function(t){return a.apply(this,arguments)}};function r(t){let e=t.path,n=t.body,r=c(t,["path","body"]);return o(i({method:"POST",path:"https://api.dropboxapi.com/2/".concat(e),contentType:"application/json",body:JSON.stringify(n)},r))}function a(){return(a=n((function*(t){const e=[];let n=yield r({path:"files/list_folder",body:{path:"/".concat(t)}});var o=!0,i=!1,c=void 0;try{for(var a,l=n.entries[Symbol.iterator]();!(o=(a=l.next()).done);o=!0){const t=a.value;e.push(t.name)}}catch(t){i=!0,c=t}finally{try{o||null==l.return||l.return()}finally{if(i)throw c}}if(!n.has_more)return e;for(;n.has_more;){n=yield r({path:"files/list_folder/continue",body:{cursor:n.cursor}});var s=!0,u=!1,p=void 0;try{for(var f,d=n.entries[Symbol.iterator]();!(s=(f=d.next()).done);s=!0){const t=f.value;e.push(t.name)}}catch(t){u=!0,p=t}finally{try{s||null==d.return||d.return()}finally{if(u)throw p}}}return e}))).apply(this,arguments)}function l(t){const e=new URLSearchParams;return e.set("arg",JSON.stringify(t)),e.toString()}function s(){return(s=n((function*(t){const e={path:"/".concat(t)};try{return yield o({path:"https://content.dropboxapi.com/2/files/download?".concat(l(e)),format:"text"})}catch(t){throw 409===t.code&&t.message.includes("not_found")&&(t.code="ENOENT"),t}}))).apply(this,arguments)}function u(t,e){return p.apply(this,arguments)}function p(){return(p=n((function*(t,e,n="overwrite"){const r={path:"/".concat(t),mode:n,autorename:!1};yield o({path:"https://content.dropboxapi.com/2/files/upload?".concat(l(r)),method:"POST",contentType:"application/octet-stream",body:e})}))).apply(this,arguments)}function f(){return(f=n((function*(t,e){try{return yield u(t,e,"add")}catch(t){throw 409===t.code&&t.message.includes("conflict")&&(t.code="EEXIST"),t}}))).apply(this,arguments)}function d(){return(d=n((function*(t){try{yield r({path:"files/delete_v2",body:{path:"/".concat(t)}})}catch(t){if(409===t.code&&t.message.includes("not_found"))return;throw t}}))).apply(this,arguments)}},onedrive:function({getAccessToken:t,fetch:e=("undefined"!=typeof self?self:global).fetch}){const o=y({fetch:e,getAccessToken:t});return{name:"onedrive",get:function(t){return a.apply(this,arguments)},put:function(t,e){return l.apply(this,arguments)},post:function(t,e){return s.apply(this,arguments)},delete:function(t){return u.apply(this,arguments)},list:function(t){return c.apply(this,arguments)}};function r(t){return i.apply(this,arguments)}function i(){return(i=n((function*(t){return t.path="https://graph.microsoft.com/v1.0/me/drive/special/approot".concat(t.path),yield o(t)}))).apply(this,arguments)}function c(){return(c=n((function*(t){t&&(t=":/".concat(t,":"));let e=yield r({path:"".concat(t,"/children?select=name")}),n=e.value.map(t=>t.name);for(;e["@odata.nextLink"];)e=yield o({path:e["@odata.nextLink"]}),n=n.concat(e.value.map(t=>t.name));return n}))).apply(this,arguments)}function a(){return(a=n((function*(t){return yield r({path:":/".concat(t,":/content"),format:"text"})}))).apply(this,arguments)}function l(){return(l=n((function*(t,e){yield r({method:"PUT",path:":/".concat(t,":/content"),headers:{"Content-Type":"text/plain"},body:e})}))).apply(this,arguments)}function s(){return(s=n((function*(t,e){try{yield r({method:"PUT",path:":/".concat(t,":/content?@microsoft.graph.conflictBehavior=fail"),headers:{"Content-Type":"text/plain"},body:e})}catch(t){throw 409===t.code&&t.message.includes("nameAlreadyExists")&&(t.code="EEXIST"),t}}))).apply(this,arguments)}function u(){return(u=n((function*(t){try{yield r({method:"DELETE",path:":/".concat(t,":")})}catch(t){if(404===t.code)return;throw t}}))).apply(this,arguments)}},google:function({getAccessToken:t,fetch:e=("undefined"!=typeof self?self:global).fetch,FormData:o=("undefined"!=typeof self?self:global).FormData,Blob:r=("undefined"!=typeof self?self:global).Blob}){const i=y({fetch:e,getAccessToken:t}),c=new Map;let a;return{name:"google",get:function(t){return T.apply(this,arguments)},put:function(t,e){return j.apply(this,arguments)},post:k,delete:function(t){return x.apply(this,arguments)},list:function(t){return b.apply(this,arguments)},init:function(){return w.apply(this,arguments)},acquireLock:function(t){return u.apply(this,arguments)},releaseLock:function(){return p.apply(this,arguments)},fileMetaCache:c};function l(t,e){return s.apply(this,arguments)}function s(){return(s=n((function*(t,e){yield i({method:"DELETE",path:"https://www.googleapis.com/drive/v3/files/".concat(t,"/revisions/").concat(e)})}))).apply(this,arguments)}function u(){return(u=n((function*(t){const e=c.get("lock.json"),n=(yield h(e.id,JSON.stringify({expire:Date.now()+60*t*1e3}))).headRevisionId,o=yield i({path:"https://www.googleapis.com/drive/v3/files/".concat(e.id,"/revisions?fields=revisions(id)")});for(let t=1;tDate.now())throw yield l(e.id,n),new d("failed to acquire lock",null,"EEXIST");yield l(e.id,r)}throw new Error("cannot find lock revision")}))).apply(this,arguments)}function p(){return(p=n((function*(){const t=c.get("lock.json");yield l(t.id,a),a=null}))).apply(this,arguments)}function f(){return(f=n((function*(t,e){t="https://www.googleapis.com/drive/v3/files?spaces=appDataFolder&fields=nextPageToken,files(id,name,headRevisionId)"+(t?"&"+t:"");let n=yield i({path:t});for(e(n);n.nextPageToken;)e(n=yield i({path:"".concat(t,"&pageToken=").concat(n.nextPageToken)}))}))).apply(this,arguments)}function h(t,e){return g.apply(this,arguments)}function g(){return(g=n((function*(t,e){return yield i({method:"PATCH",path:"https://www.googleapis.com/upload/drive/v3/files/".concat(t,"?uploadType=media&fields=headRevisionId"),headers:{"Content-Type":"text/plain"},body:e})}))).apply(this,arguments)}function v(t){return m.apply(this,arguments)}function m(){return(m=n((function*(t){t&&(t="q=".concat(encodeURIComponent(t))),yield function(t,e){return f.apply(this,arguments)}(t,t=>{var e=!0,n=!1,o=void 0;try{for(var r,i=t.files[Symbol.iterator]();!(e=(r=i.next()).done);e=!0){const t=r.value;c.set(t.name,t)}}catch(t){n=!0,o=t}finally{try{e||null==i.return||i.return()}finally{if(n)throw o}}})}))).apply(this,arguments)}function w(){return(w=n((function*(){yield v(),c.has("lock.json")||(yield k("lock.json","{}")),c.has("meta.json")||(yield k("meta.json","{}"))}))).apply(this,arguments)}function b(){return(b=n((function*(t){return[...c.values()].filter(e=>e.name.startsWith(t+"/")).map(t=>t.name.split("/")[1])}))).apply(this,arguments)}function T(){return(T=n((function*(t){let e=c.get(t);if(!(e||(yield v("name = '".concat(t,"'")),e=c.get(t))))throw new d("metaCache doesn't contain ".concat(t),null,"ENOENT");try{return yield i({path:"https://www.googleapis.com/drive/v3/files/".concat(e.id,"?alt=media")})}catch(t){throw 404===t.code&&(t.code="ENOENT"),t}}))).apply(this,arguments)}function j(){return(j=n((function*(t,e){if(!c.has(t))return yield k(t,e);const n=c.get(t),o=yield h(n.id,e);n.headRevisionId=o.headRevisionId}))).apply(this,arguments)}function k(t,e){return O.apply(this,arguments)}function O(){return(O=n((function*(t,e){const n=new o,a={name:t,parents:["appDataFolder"]};n.append("metadata",new r([JSON.stringify(a)],{type:"application/json; charset=UTF-8"})),n.append("media",new r([e],{type:"text/plain"}));const l=yield i({method:"POST",path:"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name,headRevisionId",body:n});c.set(l.name,l)}))).apply(this,arguments)}function x(){return(x=n((function*(t){const e=c.get(t);if(e)try{yield i({method:"DELETE",path:"https://www.googleapis.com/drive/v3/files/".concat(e.id)})}catch(t){if(404===t.code)return;throw t}}))).apply(this,arguments)}}});return t.dbToCloud=function({onGet:t,onPut:e,onDelete:o,onFirstSync:r,onWarn:i=console.error,onProgress:c,compareRevision:u,getState:p,setState:f,lockExpire:d=60}){let h,y,g;const v=new Map,m=s(()=>f(h,y)),w=new Map,b=l();return{use:function(t){h=function(t){const e=Object.create(t);return e.get=function(){var e=n((function*(e){return JSON.parse(yield t.get(e))}));return function(t){return e.apply(this,arguments)}}(),e.put=function(){var e=n((function*(e,n){return yield t.put(e,JSON.stringify(n))}));return function(t,n){return e.apply(this,arguments)}}(),e.post=function(){var e=n((function*(e,n){return yield t.post(e,JSON.stringify(n))}));return function(t,n){return e.apply(this,arguments)}}(),e.acquireLock||(e.acquireLock=function(t){return o.apply(this,arguments)},e.releaseLock=function(){return r.apply(this,arguments)}),e.getMeta||(e.getMeta=function(){return i.apply(this,arguments)},e.putMeta=function(t){return c.apply(this,arguments)}),e.peekChanges||(e.peekChanges=function(t){return a.apply(this,arguments)}),e;function o(){return(o=n((function*(t){try{yield this.post("lock.json",{expire:Date.now()+60*t*1e3})}catch(t){if("EEXIST"===t.code){const t=yield this.get("lock.json");Date.now()>t.expire&&(yield this.delete("lock.json"))}throw t}}))).apply(this,arguments)}function r(){return(r=n((function*(){yield this.delete("lock.json")}))).apply(this,arguments)}function i(){return(i=n((function*(){try{return yield this.get("meta.json")}catch(t){if("ENOENT"===t.code||404===t.code)return{};throw t}}))).apply(this,arguments)}function c(){return(c=n((function*(t){yield this.put("meta.json",t)}))).apply(this,arguments)}function a(){return(a=n((function*(t){return(yield this.getMeta()).lastChange!==t.lastChange}))).apply(this,arguments)}}(t)},start:function(){return b.write(n((function*(){if(!y||!y.enabled){if(!h)throw new Error("cloud drive is undefined");h.init&&(yield h.init()),(y=(yield p(h))||{}).enabled=!0,y.queue||(y.queue=[]),null==y.lastChange&&(yield r()),yield O()}})))},stop:function(){return b.write(n((function*(){y&&y.enabled&&(y=g=null,v.clear(),w.clear(),h.uninit&&(yield h.uninit()),yield m())})))},put:function(t,e){if(!y||!y.enabled)return;y.queue.push({_id:t,_rev:e,action:"put"}),m()},delete:function(t,e){if(!y||!y.enabled)return;y.queue.push({_id:t,_rev:e,action:"delete"}),m()},syncNow:function(t){return b.write(n((function*(){if(!y||!y.enabled)throw new Error("Cannot sync now, the sync is not enabled");yield O(t)})))},drive:()=>h,isInit:()=>Boolean(y&&y.enabled)};function T(){return(T=n((function*(){if(!(g=yield h.getMeta()).lastChange||g.lastChange===y.lastChange)return;let t=[];if(y.lastChange){const e=Math.floor((g.lastChange-1)/100);let n=Math.floor(y.lastChange/100);for(;n<=e;){const e=yield h.get("changes/".concat(n,".json"));v.set(n,e),t=t.concat(e),n++}t=t.slice(y.lastChange%100)}else t=(yield h.list("docs")).map(t=>({action:"put",_id:t.slice(0,-5)}));const n=new Map;var r=!0,l=!1,s=void 0;try{for(var u,p=t[Symbol.iterator]();!(r=(u=p.next()).done);r=!0){const t=u.value;n.set(t._id,t)}}catch(t){l=!0,s=t}finally{try{r||null==p.return||p.return()}finally{if(l)throw s}}let f=0;var d=!0,b=!1,T=void 0;try{for(var j,k=n[Symbol.iterator]();!(d=(j=k.next()).done);d=!0){const t=a(j.value,2),r=t[0],l=t[1];let s,u;if(c&&c({phase:"pull",total:n.size,loaded:f,change:l}),"delete"===l.action)yield o(r,l._rev);else if("put"===l.action){try{var O=yield h.get("docs/".concat(r,".json"));s=O.doc,u=O._rev}catch(t){if("ENOENT"===t.code||404===t.code){i("Cannot find ".concat(r,". Is it deleted without updating the history?")),f++;continue}throw t}yield e(s)}const p=l._rev||u;p&&w.set(r,p),f++}}catch(t){b=!0,T=t}finally{try{d||null==k.return||k.return()}finally{if(b)throw T}}y.lastChange=g.lastChange,yield m()}))).apply(this,arguments)}function j(){return(j=n((function*(){if(!y.queue.length)return;const e=y.queue.slice(),n=new Map;var o=!0,r=!1,i=void 0;try{for(var a,l=e[Symbol.iterator]();!(o=(a=l.next()).done);o=!0){const t=a.value;n.set(t._id,t)}}catch(t){r=!0,i=t}finally{try{o||null==l.return||l.return()}finally{if(r)throw i}}const s=[];var p=!0,f=!1,d=void 0;try{for(var b,T=n.values()[Symbol.iterator]();!(p=(b=T.next()).done);p=!0){const t=b.value,e=w.get(t._id);void 0!==e&&u(t._rev,e)<=0||s.push(t)}}catch(t){f=!0,d=t}finally{try{p||null==T.return||T.return()}finally{if(f)throw d}}let j,k,O=0;for(var x=0,E=s;x>} + */ +routes.fileRequestsCount = function (arg) { + return this.request('file_requests/count', arg, 'user', 'api', 'rpc'); +}; + /** * Creates a file request for this user. * @function Dropbox#fileRequestsCreate @@ -246,6 +257,26 @@ routes.fileRequestsCreate = function (arg) { return this.request('file_requests/create', arg, 'user', 'api', 'rpc'); }; +/** + * Delete a batch of closed file requests. + * @function Dropbox#fileRequestsDelete + * @arg {FileRequestsDeleteFileRequestArgs} arg - The request parameters. + * @returns {Promise.>} + */ +routes.fileRequestsDelete = function (arg) { + return this.request('file_requests/delete', arg, 'user', 'api', 'rpc'); +}; + +/** + * Delete all closed file requests owned by this user. + * @function Dropbox#fileRequestsDeleteAllClosed + * @arg {void} arg - The request parameters. + * @returns {Promise.>} + */ +routes.fileRequestsDeleteAllClosed = function (arg) { + return this.request('file_requests/delete_all_closed', arg, 'user', 'api', 'rpc'); +}; + /** * Returns the specified file request. * @function Dropbox#fileRequestsGet @@ -256,6 +287,18 @@ routes.fileRequestsGet = function (arg) { return this.request('file_requests/get', arg, 'user', 'api', 'rpc'); }; +/** + * Returns a list of file requests owned by this user. For apps with the app + * folder permission, this will only return file requests with destinations in + * the app folder. + * @function Dropbox#fileRequestsListV2 + * @arg {FileRequestsListFileRequestsArg} arg - The request parameters. + * @returns {Promise.>} + */ +routes.fileRequestsListV2 = function (arg) { + return this.request('file_requests/list_v2', arg, 'user', 'api', 'rpc'); +}; + /** * Returns a list of file requests owned by this user. For apps with the app * folder permission, this will only return file requests with destinations in @@ -268,6 +311,18 @@ routes.fileRequestsList = function (arg) { return this.request('file_requests/list', arg, 'user', 'api', 'rpc'); }; +/** + * Once a cursor has been retrieved from list_v2, use this to paginate through + * all file requests. The cursor must come from a previous call to list_v2 or + * list/continue. + * @function Dropbox#fileRequestsListContinue + * @arg {FileRequestsListFileRequestsContinueArg} arg - The request parameters. + * @returns {Promise.>} + */ +routes.fileRequestsListContinue = function (arg) { + return this.request('file_requests/list/continue', arg, 'user', 'api', 'rpc'); +}; + /** * Update a file request. * @function Dropbox#fileRequestsUpdate @@ -331,7 +386,7 @@ routes.filesCopy = function (arg) { /** * Copy multiple files or folders to different locations at once in the user's * Dropbox. This route will replace copy_batch. The main difference is this - * route will return stutus for each entry, while copy_batch raises failure if + * route will return status for each entry, while copy_batch raises failure if * any entry fails. This route will either finish synchronously, or return a job * ID and do the async copy job in background. Please use copy_batch/check_v2 to * check the job status. @@ -526,6 +581,18 @@ routes.filesDownloadZip = function (arg) { return this.request('files/download_zip', arg, 'user', 'content', 'download'); }; +/** + * Export a file from a user's Dropbox. This route only supports exporting files + * that cannot be downloaded directly and whose ExportResult.file_metadata has + * ExportInfo.export_as populated. + * @function Dropbox#filesExport + * @arg {FilesExportArg} arg - The request parameters. + * @returns {Promise.>} + */ +routes.filesExport = function (arg) { + return this.request('files/export', arg, 'user', 'content', 'download'); +}; + /** * Returns the metadata for a file or folder. Note: Metadata for the root folder * is unsupported. @@ -539,10 +606,11 @@ routes.filesGetMetadata = function (arg) { /** * Get a preview for a file. Currently, PDF previews are generated for files - * with the following extensions: .ai, .doc, .docm, .docx, .eps, .odp, .odt, - * .pps, .ppsm, .ppsx, .ppt, .pptm, .pptx, .rtf. HTML previews are generated for - * files with the following extensions: .csv, .ods, .xls, .xlsm, .xlsx. Other - * formats will return an unsupported extension error. + * with the following extensions: .ai, .doc, .docm, .docx, .eps, .gdoc, + * .gslides, .odp, .odt, .pps, .ppsm, .ppsx, .ppt, .pptm, .pptx, .rtf. HTML + * previews are generated for files with the following extensions: .csv, .ods, + * .xls, .xlsm, .gsheet, .xlsx. Other formats will return an unsupported + * extension error. * @function Dropbox#filesGetPreview * @arg {FilesPreviewArg} arg - The request parameters. * @returns {Promise.>} @@ -553,8 +621,8 @@ routes.filesGetPreview = function (arg) { /** * Get a temporary link to stream content of a file. This link will expire in - * four hours and afterwards you will get 410 Gone. So this URL should not be - * used to display content directly in the browser. Content-Type of the link is + * four hours and afterwards you will get 410 Gone. This URL should not be used + * to display content directly in the browser. The Content-Type of the link is * determined automatically by the file's mime type. * @function Dropbox#filesGetTemporaryLink * @arg {FilesGetTemporaryLinkArg} arg - The request parameters. @@ -736,8 +804,8 @@ routes.filesMove = function (arg) { /** * Move multiple files or folders to different locations at once in the user's - * Dropbox. This route will replace move_batch_v2. The main difference is this - * route will return stutus for each entry, while move_batch raises failure if + * Dropbox. This route will replace move_batch. The main difference is this + * route will return status for each entry, while move_batch raises failure if * any entry fails. This route will either finish synchronously, or return a job * ID and do the async move job in background. Please use move_batch/check_v2 to * check the job status. diff --git a/vendor/uuid/LICENSE b/vendor/uuid/LICENSE new file mode 100644 index 00000000..8c84e398 --- /dev/null +++ b/vendor/uuid/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2010-2016 Robert Kieffer and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/uuid/README.md b/vendor/uuid/README.md new file mode 100644 index 00000000..b8a9a51e --- /dev/null +++ b/vendor/uuid/README.md @@ -0,0 +1,9 @@ +## uuid v3.3.3 + +Installed via npm - source code: + +https://github.com/kelektiv/node-uuid/tree/v3.3.3 + +Bundled code: + +https://bundle.run/uuid@3.3.3/v4.js diff --git a/vendor/uuid/uuid.min.js b/vendor/uuid/uuid.min.js new file mode 100644 index 00000000..eb688a0f --- /dev/null +++ b/vendor/uuid/uuid.min.js @@ -0,0 +1 @@ +!function(n){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).uuid=n()}}(function(){return function(){return function n(e,r,t){function o(f,u){if(!r[f]){if(!e[f]){var d="function"==typeof require&&require;if(!u&&d)return d(f,!0);if(i)return i(f,!0);var a=new Error("Cannot find module '"+f+"'");throw a.code="MODULE_NOT_FOUND",a}var p=r[f]={exports:{}};e[f][0].call(p.exports,function(n){return o(e[f][1][n]||n)},p,p.exports,n,e,r,t)}return r[f].exports}for(var i="function"==typeof require&&require,f=0;f>>((3&e)<<3)&255;return i}}},{}],3:[function(n,e,r){var t=n("./lib/rng"),o=n("./lib/bytesToUuid");e.exports=function(n,e,r){var i=e&&r||0;"string"==typeof n&&(e="binary"===n?new Array(16):null,n=null);var f=(n=n||{}).random||(n.rng||t)();if(f[6]=15&f[6]|64,f[8]=63&f[8]|128,e)for(var u=0;u<16;++u)e[i+u]=f[u];return e||o(f)}},{"./lib/bytesToUuid":1,"./lib/rng":2}]},{},[3])(3)}); \ No newline at end of file