keeweb/app/scripts/storage/impl/storage-dropbox.js

414 lines
13 KiB
JavaScript
Raw Normal View History

2019-09-15 14:16:32 +02:00
import { StorageBase } from 'storage/storage-base';
import { Features } from 'util/features';
import { UrlFormat } from 'util/formatting/url-format';
import { DropboxApps } from 'const/cloud-storage-apps';
2020-04-18 15:48:29 +02:00
import { Locale } from 'util/locale';
2017-04-16 17:00:35 +02:00
const DropboxCustomErrors = {
BadKey: 'bad-key'
};
2015-12-12 09:53:50 +01:00
// https://www.dropbox.com/developers/documentation/http/documentation#oauth2-authorize
2019-09-18 21:26:43 +02:00
class StorageDropbox extends StorageBase {
name = 'dropbox';
icon = 'dropbox';
enabled = true;
uipos = 20;
backup = true;
2015-12-02 21:39:40 +01:00
2019-08-18 10:17:09 +02:00
_toFullPath(path) {
2019-09-17 19:50:42 +02:00
const rootFolder = this.appSettings.dropboxFolder;
2016-03-14 06:04:55 +01:00
if (rootFolder) {
2019-09-15 08:11:11 +02:00
path = UrlFormat.fixSlashes('/' + rootFolder + '/' + path);
2016-03-14 06:04:55 +01:00
}
return path;
2019-09-18 21:26:43 +02:00
}
2016-03-14 06:04:55 +01:00
2019-08-18 10:17:09 +02:00
_toRelPath(path) {
2019-09-17 19:50:42 +02:00
const rootFolder = this.appSettings.dropboxFolder;
2016-03-14 06:04:55 +01:00
if (rootFolder) {
2017-01-31 07:50:28 +01:00
const ix = path.toLowerCase().indexOf(rootFolder.toLowerCase());
2016-03-14 06:04:55 +01:00
if (ix === 0) {
path = path.substr(rootFolder.length);
} else if (ix === 1) {
path = path.substr(rootFolder.length + 1);
}
2019-09-15 08:11:11 +02:00
path = UrlFormat.fixSlashes('/' + path);
2016-03-14 06:04:55 +01:00
}
return path;
2019-09-18 21:26:43 +02:00
}
2016-03-14 06:04:55 +01:00
2019-08-18 10:17:09 +02:00
_fixConfigFolder(folder) {
folder = folder.replace(/\\/g, '/').trim();
if (folder[0] === '/') {
folder = folder.substr(1);
}
return folder;
2019-09-18 21:26:43 +02:00
}
2019-08-18 10:17:09 +02:00
_getKey() {
return this.appSettings.dropboxAppKey || DropboxApps.AppFolder.id;
}
_getSecret() {
const key = this._getKey();
if (key === DropboxApps.AppFolder.id) {
return DropboxApps.AppFolder.secret;
}
if (key === DropboxApps.FullDropbox.id) {
return DropboxApps.FullDropbox.secret;
}
return this.appSettings.dropboxSecret;
2019-09-18 21:26:43 +02:00
}
2017-04-16 17:00:35 +02:00
2019-08-18 10:17:09 +02:00
_isValidKey() {
2017-04-16 17:00:35 +02:00
const key = this._getKey();
const isBuiltIn = key === DropboxApps.AppFolder.id || key === DropboxApps.FullDropbox.id;
2017-04-16 17:00:35 +02:00
return key && key.indexOf(' ') < 0 && (!isBuiltIn || this._canUseBuiltInKeys());
2019-09-18 21:26:43 +02:00
}
2017-04-16 17:00:35 +02:00
2019-08-18 10:17:09 +02:00
_canUseBuiltInKeys() {
2019-09-15 08:11:11 +02:00
return !Features.isSelfHosted;
2019-09-18 21:26:43 +02:00
}
2017-04-16 17:00:35 +02:00
2019-08-18 10:17:09 +02:00
_getOAuthConfig() {
2017-04-16 17:00:35 +02:00
return {
2020-09-12 12:03:20 +02:00
scope:
'files.content.read files.content.write files.metadata.read files.metadata.write',
2017-04-16 17:00:35 +02:00
url: 'https://www.dropbox.com/oauth2/authorize',
tokenUrl: 'https://api.dropboxapi.com/oauth2/token',
2017-04-16 17:00:35 +02:00
clientId: this._getKey(),
clientSecret: this._getSecret(),
2020-09-12 12:04:16 +02:00
pkce: true,
2017-04-16 17:00:35 +02:00
width: 600,
height: 400
};
2019-09-18 21:26:43 +02:00
}
2017-04-16 17:00:35 +02:00
2019-08-18 10:17:09 +02:00
needShowOpenConfig() {
return !this._isValidKey() || !this._getSecret();
2019-09-18 21:26:43 +02:00
}
2016-03-13 17:45:55 +01:00
2019-08-18 10:17:09 +02:00
getOpenConfig() {
2016-03-13 17:45:55 +01:00
return {
desc: 'dropboxSetupDesc',
fields: [
2019-08-16 23:05:39 +02:00
{
id: 'key',
title: 'dropboxAppKey',
desc: 'dropboxAppKeyDesc',
type: 'text',
required: true,
pattern: '\\w+'
},
{
id: 'secret',
title: 'dropboxAppSecret',
desc: 'dropboxAppSecretDesc',
2020-05-22 10:15:21 +02:00
type: 'password',
required: true,
pattern: '\\w+'
},
2019-08-16 23:05:39 +02:00
{
id: 'folder',
title: 'dropboxFolder',
desc: 'dropboxFolderDesc',
type: 'text',
placeholder: 'dropboxFolderPlaceholder'
}
2016-03-13 17:45:55 +01:00
]
};
2019-09-18 21:26:43 +02:00
}
2016-03-13 17:45:55 +01:00
2019-08-18 10:17:09 +02:00
getSettingsConfig() {
2017-01-31 07:50:28 +01:00
const fields = [];
2017-04-16 17:00:35 +02:00
const appKey = this._getKey();
2019-08-16 23:05:39 +02:00
const linkField = {
id: 'link',
title: 'dropboxLink',
type: 'select',
value: 'custom',
options: { app: 'dropboxLinkApp', full: 'dropboxLinkFull', custom: 'dropboxLinkCustom' }
};
const keyField = {
id: 'key',
title: 'dropboxAppKey',
desc: 'dropboxAppKeyDesc',
type: 'text',
required: true,
pattern: '\\w+',
value: appKey
};
const secretField = {
id: 'secret',
title: 'dropboxAppSecret',
desc: 'dropboxAppSecretDesc',
2020-05-22 10:15:21 +02:00
type: 'password',
required: true,
pattern: '\\w+',
value: this.appSettings.dropboxSecret || ''
};
2019-08-16 23:05:39 +02:00
const folderField = {
id: 'folder',
title: 'dropboxFolder',
desc: 'dropboxFolderSettingsDesc',
type: 'text',
2019-09-17 19:50:42 +02:00
value: this.appSettings.dropboxFolder || ''
2019-08-16 23:05:39 +02:00
};
2017-04-16 17:00:35 +02:00
const canUseBuiltInKeys = this._canUseBuiltInKeys();
if (canUseBuiltInKeys) {
fields.push(linkField);
if (appKey === DropboxApps.AppFolder.id) {
linkField.value = 'app';
} else if (appKey === DropboxApps.FullDropbox.id) {
linkField.value = 'full';
fields.push(folderField);
} else {
fields.push(keyField);
fields.push(secretField);
fields.push(folderField);
}
} else {
fields.push(keyField);
fields.push(secretField);
fields.push(folderField);
}
2019-08-18 10:17:09 +02:00
return { fields };
2019-09-18 21:26:43 +02:00
}
2019-08-18 10:17:09 +02:00
applyConfig(config, callback) {
if (config.key === DropboxApps.AppFolder.id || config.key === DropboxApps.FullDropbox.id) {
2017-04-16 17:00:35 +02:00
return callback(DropboxCustomErrors.BadKey);
}
// TODO: try to connect using new key
if (config.folder) {
config.folder = this._fixConfigFolder(config.folder);
}
this.appSettings.set({
dropboxAppKey: config.key,
dropboxSecret: config.secret,
2017-04-16 17:00:35 +02:00
dropboxFolder: config.folder
});
callback();
2019-09-18 21:26:43 +02:00
}
2016-03-13 17:08:25 +01:00
2019-08-18 10:17:09 +02:00
applySetting(key, value) {
switch (key) {
case 'link':
key = 'dropboxAppKey';
switch (value) {
case 'app':
value = DropboxApps.AppFolder.id;
break;
case 'full':
value = DropboxApps.FullDropbox.id;
break;
case 'custom':
2020-04-18 15:48:29 +02:00
value = `(${Locale.dropboxAppKeyHint})`;
break;
2016-03-17 22:49:39 +01:00
default:
return;
}
2020-04-17 21:36:56 +02:00
this.logout();
break;
case 'key':
key = 'dropboxAppKey';
2020-04-17 21:36:56 +02:00
this.logout();
break;
case 'secret':
key = 'dropboxSecret';
2020-04-17 21:36:56 +02:00
this.logout();
break;
case 'folder':
key = 'dropboxFolder';
value = this._fixConfigFolder(value);
break;
default:
return;
}
2019-09-17 19:50:42 +02:00
this.appSettings[key] = value;
2019-09-18 21:26:43 +02:00
}
2019-08-18 10:17:09 +02:00
getPathForName(fileName) {
2015-12-12 16:43:43 +01:00
return '/' + fileName + '.kdbx';
2019-09-18 21:26:43 +02:00
}
2015-12-12 16:43:43 +01:00
_encodeJsonHttpHeader(json) {
2019-08-18 08:05:38 +02:00
return json.replace(
/[\u007f-\uffff]/g,
2020-06-01 16:53:51 +02:00
(c) => '\\u' + ('000' + c.charCodeAt(0).toString(16)).slice(-4)
2019-08-18 08:05:38 +02:00
);
2019-09-18 21:26:43 +02:00
}
2019-08-18 10:17:09 +02:00
_apiCall(args) {
2020-06-01 16:53:51 +02:00
this._oauthAuthorize((err) => {
2017-04-16 17:00:35 +02:00
if (err) {
return args.error(err);
}
const host = args.host || 'api';
let headers;
let data = args.data;
let dataType;
2017-04-16 17:00:35 +02:00
if (args.apiArg) {
2019-08-18 08:05:38 +02:00
headers = {
'Dropbox-API-Arg': this._encodeJsonHttpHeader(JSON.stringify(args.apiArg))
};
2017-04-16 17:00:35 +02:00
} else if (args.data) {
data = JSON.stringify(data);
dataType = 'application/json';
2017-04-16 17:00:35 +02:00
}
this._xhr({
url: `https://${host}.dropboxapi.com/2/${args.method}`,
method: 'POST',
responseType: args.responseType || 'json',
2019-08-18 10:17:09 +02:00
headers,
data,
dataType,
2017-04-16 17:00:35 +02:00
statuses: args.statuses || undefined,
success: args.success,
error: (e, xhr) => {
2019-08-16 23:05:39 +02:00
let err = (xhr.response && xhr.response.error) || new Error('Network error');
2017-04-16 17:00:35 +02:00
if (err && err.path && err.path['.tag'] === 'not_found') {
err = new Error('File removed');
err.notFound = true;
this.logger.debug('File not found', args.method);
} else {
this.logger.error('API error', args.method, xhr.status, err);
}
err.status = xhr.status;
args.error(err);
}
});
});
2019-09-18 21:26:43 +02:00
}
2017-04-16 17:00:35 +02:00
2019-08-18 10:17:09 +02:00
load(path, opts, callback) {
2016-07-17 13:30:38 +02:00
this.logger.debug('Load', path);
2017-01-31 07:50:28 +01:00
const ts = this.logger.ts();
2016-07-17 13:30:38 +02:00
path = this._toFullPath(path);
2017-04-16 17:00:35 +02:00
this._apiCall({
method: 'files/download',
host: 'content',
apiArg: { path },
responseType: 'arraybuffer',
success: (response, xhr) => {
const stat = JSON.parse(xhr.getResponseHeader('dropbox-api-result'));
this.logger.debug('Loaded', path, stat.rev, this.logger.ts(ts));
callback(null, response, { rev: stat.rev });
},
error: callback
});
2019-09-18 21:26:43 +02:00
}
2015-12-08 20:18:35 +01:00
2019-08-18 10:17:09 +02:00
stat(path, opts, callback) {
2016-07-17 13:30:38 +02:00
this.logger.debug('Stat', path);
2017-01-31 07:50:28 +01:00
const ts = this.logger.ts();
2016-07-17 13:30:38 +02:00
path = this._toFullPath(path);
2017-04-16 17:00:35 +02:00
this._apiCall({
method: 'files/get_metadata',
data: { path },
2020-06-01 16:53:51 +02:00
success: (stat) => {
2017-04-16 17:00:35 +02:00
if (stat['.tag'] === 'file') {
stat = { rev: stat.rev };
} else if (stat['.tag'] === 'folder') {
stat = { folder: true };
}
2019-08-18 08:05:38 +02:00
this.logger.debug(
'Stated',
path,
stat.folder ? 'folder' : stat.rev,
this.logger.ts(ts)
);
2019-08-16 23:05:39 +02:00
if (callback) {
callback(null, stat);
}
2017-04-16 17:00:35 +02:00
},
error: callback
});
2019-09-18 21:26:43 +02:00
}
2015-12-02 21:39:40 +01:00
2019-08-18 10:17:09 +02:00
save(path, opts, data, callback, rev) {
2016-07-17 13:30:38 +02:00
this.logger.debug('Save', path, rev);
2017-01-31 07:50:28 +01:00
const ts = this.logger.ts();
2016-07-17 13:30:38 +02:00
path = this._toFullPath(path);
2017-04-16 17:00:35 +02:00
const arg = {
path,
mode: rev ? { '.tag': 'update', update: rev } : { '.tag': 'overwrite' }
};
this._apiCall({
method: 'files/upload',
host: 'content',
apiArg: arg,
2019-08-18 10:17:09 +02:00
data,
2017-04-16 17:00:35 +02:00
responseType: 'json',
2020-06-01 16:53:51 +02:00
success: (stat) => {
2017-04-16 17:00:35 +02:00
this.logger.debug('Saved', path, stat.rev, this.logger.ts(ts));
callback(null, { rev: stat.rev });
},
error: callback
});
2019-09-18 21:26:43 +02:00
}
2016-03-13 17:08:25 +01:00
2019-08-18 10:17:09 +02:00
list(dir, callback) {
2017-04-16 17:00:35 +02:00
this.logger.debug('List');
const ts = this.logger.ts();
this._apiCall({
method: 'files/list_folder',
data: {
2017-11-26 17:26:58 +01:00
path: this._toFullPath(dir || ''),
2017-04-16 17:00:35 +02:00
recursive: false
},
2020-06-01 16:53:51 +02:00
success: (data) => {
2017-04-16 17:00:35 +02:00
this.logger.debug('Listed', this.logger.ts(ts));
2020-06-01 16:53:51 +02:00
const fileList = data.entries.map((f) => ({
2019-08-16 23:05:39 +02:00
name: f.name,
2019-09-08 09:38:31 +02:00
path: this._toRelPath(f.path_display),
2019-08-16 23:05:39 +02:00
rev: f.rev,
dir: f['.tag'] !== 'file'
}));
2017-04-16 17:00:35 +02:00
callback(null, fileList);
},
error: callback
2016-03-13 17:08:25 +01:00
});
2019-09-18 21:26:43 +02:00
}
2016-03-27 18:38:33 +02:00
2019-08-18 10:17:09 +02:00
remove(path, callback) {
2016-07-17 13:30:38 +02:00
this.logger.debug('Remove', path);
2017-01-31 07:50:28 +01:00
const ts = this.logger.ts();
2016-07-17 13:30:38 +02:00
path = this._toFullPath(path);
2017-04-16 17:00:35 +02:00
this._apiCall({
method: 'files/delete',
data: { path },
success: () => {
this.logger.debug('Removed', path, this.logger.ts(ts));
callback();
},
error: callback
});
2019-09-18 21:26:43 +02:00
}
2019-08-18 10:17:09 +02:00
mkdir(path, callback) {
2017-04-16 17:00:35 +02:00
this.logger.debug('Make dir', path);
const ts = this.logger.ts();
path = this._toFullPath(path);
this._apiCall({
method: 'files/create_folder',
data: { path },
success: () => {
2016-08-21 17:43:59 +02:00
this.logger.debug('Made dir', path, this.logger.ts(ts));
2017-04-16 17:00:35 +02:00
callback();
},
error: callback
2016-08-21 17:43:59 +02:00
});
2019-09-18 21:26:43 +02:00
}
2016-08-21 17:43:59 +02:00
2020-04-17 21:36:56 +02:00
logout() {
this._oauthRevokeToken('https://api.dropboxapi.com/2/auth/token/revoke', {
method: 'POST'
});
2015-12-02 21:39:40 +01:00
}
2019-09-18 21:26:43 +02:00
}
2015-12-02 21:39:40 +01:00
2019-09-15 14:16:32 +02:00
export { StorageDropbox };