Dropbox V2 API endpoints

This commit is contained in:
antelle 2017-04-16 17:00:35 +02:00
parent 8ee596b067
commit 8cac1b8cdd
12 changed files with 324 additions and 443 deletions

View File

@ -45,7 +45,7 @@ module.exports = function(grunt) {
const webpackConfig = {
entry: {
app: 'app',
vendor: ['jquery', 'underscore', 'backbone', 'kdbxweb', 'baron', 'dropbox', 'pikaday', 'filesaver', 'qrcode',
vendor: ['jquery', 'underscore', 'backbone', 'kdbxweb', 'baron', 'pikaday', 'filesaver', 'qrcode',
'argon2-asm', 'argon2-wasm', 'argon2']
},
output: {
@ -68,7 +68,6 @@ module.exports = function(grunt) {
jquery: 'jquery/dist/jquery.min.js',
hbs: path.resolve(__dirname, 'node_modules', 'handlebars/runtime.js'),
kdbxweb: 'kdbxweb/dist/kdbxweb.js',
dropbox: 'dropbox/lib/dropbox.min.js',
baron: 'baron/baron.min.js',
pikaday: 'pikaday/pikaday.js',
filesaver: 'FileSaver.js/FileSaver.min.js',

View File

@ -1,15 +1,9 @@
const DropboxLink = require('./dropbox-link');
const AuthReceiver = {
receive: function() {
const opener = window.opener || window.parent;
if (location.href.indexOf('state=') >= 0) {
DropboxLink.receive();
} else {
const message = this.urlArgsToMessage(window.location.href);
opener.postMessage(message, window.location.origin);
window.close();
}
const message = this.urlArgsToMessage(window.location.href);
opener.postMessage(message, window.location.origin);
window.close();
},
urlArgsToMessage: function(url) {

View File

@ -0,0 +1,102 @@
const AppSettingsModel = require('../models/app-settings-model');
const ChooserAppKey = 'qp7ctun6qt5n9d6';
const DropboxChooser = function(callback) {
this.cb = callback;
this.onMessage = this.onMessage.bind(this);
};
DropboxChooser.prototype.callback = function(err, res) {
if (this.cb) {
this.cb(err, res);
}
this.cb = null;
};
DropboxChooser.prototype.choose = function() {
const windowFeatures = 'width=640,height=552,left=357,top=100,resizable=yes,location=yes';
const url = this.buildUrl();
this.popup = window.open(url, 'dropbox', windowFeatures);
if (!this.popup) {
return this.callback('Failed to open window');
}
window.addEventListener('message', this.onMessage);
this.closeInt = setInterval(this.checkClose.bind(this), 200);
};
DropboxChooser.prototype.buildUrl = function() {
const urlParams = {
origin: encodeURIComponent(window.location.protocol + '//' + window.location.host),
'app_key': AppSettingsModel.instance.get('dropboxAppKey') || ChooserAppKey,
'link_type': 'direct',
trigger: 'js',
multiselect: 'false',
extensions: '',
folderselect: 'false',
iframe: 'false',
version: 2
};
return 'https://www.dropbox.com/chooser?' + Object.keys(urlParams).map(key => key + '=' + urlParams[key]).join('&');
};
DropboxChooser.prototype.onMessage = function(e) {
if (e.source !== this.popup) {
return;
}
const data = JSON.parse(e.data);
switch (data.method) {
case 'origin_request':
e.source.postMessage(JSON.stringify({ method: 'origin' }), 'https://www.dropbox.com');
break;
case 'files_selected':
this.popup.close();
this.success(data.params);
break;
case 'close_dialog':
this.popup.close();
break;
case 'web_session_error':
case 'web_session_unlinked':
this.callback(data.method);
break;
case 'resize':
this.popup.resize(data.params);
break;
case 'error':
this.callback(data.params);
break;
}
};
DropboxChooser.prototype.checkClose = function() {
if (this.popup.closed) {
clearInterval(this.closeInt);
window.removeEventListener('message', this.onMessage);
if (!this.result) {
this.callback('closed');
}
}
};
DropboxChooser.prototype.success = function(params) {
if (!params || !params[0] || !params[0].link || params[0].is_dir) {
return this.callback('bad result');
}
this.result = params[0];
this.readFile(this.result.link);
};
DropboxChooser.prototype.readFile = function(url) {
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', () => {
this.callback(null, { name: this.result.name, data: xhr.response });
});
xhr.addEventListener('error', this.callback.bind(this, 'download error'));
xhr.addEventListener('abort', this.callback.bind(this, 'download abort'));
xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.send();
};
module.exports = DropboxChooser;

View File

@ -1,331 +0,0 @@
const Dropbox = require('dropbox');
const Alerts = require('./alerts');
const Launcher = require('./launcher');
const Logger = require('../util/logger');
const Locale = require('../util/locale');
const UrlUtil = require('../util/url-util');
const AppSettingsModel = require('../models/app-settings-model');
const logger = new Logger('dropbox');
const DropboxKeys = {
AppFolder: 'qp7ctun6qt5n9d6',
FullDropbox: 'eor7hvv6u6oslq9'
};
const DropboxCustomErrors = {
BadKey: 'bad-key'
};
function getKey() {
return AppSettingsModel.instance.get('dropboxAppKey') || DropboxKeys.AppFolder;
}
const DropboxChooser = function(callback) {
this.cb = callback;
this.onMessage = this.onMessage.bind(this);
};
DropboxChooser.prototype.callback = function(err, res) {
if (this.cb) {
this.cb(err, res);
}
this.cb = null;
};
DropboxChooser.prototype.choose = function() {
const windowFeatures = 'width=640,height=552,left=357,top=100,resizable=yes,location=yes';
const url = this.buildUrl();
this.popup = window.open(url, 'dropbox', windowFeatures);
if (!this.popup) {
return this.callback('Failed to open window');
}
window.addEventListener('message', this.onMessage);
this.closeInt = setInterval(this.checkClose.bind(this), 200);
};
DropboxChooser.prototype.buildUrl = function() {
const urlParams = {
origin: encodeURIComponent(window.location.protocol + '//' + window.location.host),
'app_key': getKey(),
'link_type': 'direct',
trigger: 'js',
multiselect: 'false',
extensions: '',
folderselect: 'false',
iframe: 'false',
version: 2
};
return 'https://www.dropbox.com/chooser?' + Object.keys(urlParams).map(key => {
return key + '=' + urlParams[key];
}).join('&');
};
DropboxChooser.prototype.onMessage = function(e) {
if (e.source !== this.popup) {
return;
}
const data = JSON.parse(e.data);
switch (data.method) {
case 'origin_request':
e.source.postMessage(JSON.stringify({ method: 'origin' }), 'https://www.dropbox.com');
break;
case 'files_selected':
this.popup.close();
this.success(data.params);
break;
case 'close_dialog':
this.popup.close();
break;
case 'web_session_error':
case 'web_session_unlinked':
this.callback(data.method);
break;
case 'resize':
this.popup.resize(data.params);
break;
case 'error':
this.callback(data.params);
break;
}
};
DropboxChooser.prototype.checkClose = function() {
if (this.popup.closed) {
clearInterval(this.closeInt);
window.removeEventListener('message', this.onMessage);
if (!this.result) {
this.callback('closed');
}
}
};
DropboxChooser.prototype.success = function(params) {
if (!params || !params[0] || !params[0].link || params[0].is_dir) {
return this.callback('bad result');
}
this.result = params[0];
this.readFile(this.result.link);
};
DropboxChooser.prototype.readFile = function(url) {
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', () => {
this.callback(null, { name: this.result.name, data: xhr.response });
});
xhr.addEventListener('error', this.callback.bind(this, 'download error'));
xhr.addEventListener('abort', this.callback.bind(this, 'download abort'));
xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.send();
};
const DropboxLink = {
ERROR_CONFLICT: Dropbox.ApiError.CONFLICT,
ERROR_NOT_FOUND: Dropbox.ApiError.NOT_FOUND,
Keys: DropboxKeys,
_getClient: function(complete, overrideAppKey) {
if (this._dropboxClient && this._dropboxClient.isAuthenticated()) {
complete(null, this._dropboxClient);
return;
}
if (!overrideAppKey && !this.isValidKey()) {
return complete(DropboxCustomErrors.BadKey);
}
const client = new Dropbox.Client({key: overrideAppKey || getKey()});
if (Launcher) {
client.authDriver(new Dropbox.AuthDriver.Electron({ receiverUrl: location.href }));
} else {
client.authDriver(new Dropbox.AuthDriver.Popup({ receiverUrl: location.href }));
}
client.authenticate((error, client) => {
if (!error) {
this._dropboxClient = client;
}
complete(error, client);
});
},
_handleUiError: function(err, alertCallback, callback) {
if (!alertCallback) {
if (!Alerts.alertDisplayed) {
alertCallback = Alerts.error.bind(Alerts);
}
}
logger.error('Dropbox error', err);
switch (err.status) {
case Dropbox.ApiError.INVALID_TOKEN:
if (!Alerts.alertDisplayed) {
Alerts.yesno({
icon: 'dropbox',
header: Locale.dropboxLogin,
body: Locale.dropboxLoginBody,
buttons: [{result: 'yes', title: Locale.alertSignIn}, {result: '', title: Locale.alertCancel}],
success: () => {
this.authenticate(err => { callback(!err); });
},
cancel: () => {
callback(false);
}
});
return;
}
break;
case Dropbox.ApiError.NOT_FOUND:
alertCallback({
header: Locale.dropboxSyncError,
body: Locale.dropboxNotFoundBody
});
break;
case Dropbox.ApiError.OVER_QUOTA:
alertCallback({
header: Locale.dropboxFull,
body: Locale.dropboxFullBody
});
break;
case Dropbox.ApiError.RATE_LIMITED:
alertCallback({
header: Locale.dropboxSyncError,
body: Locale.dropboxRateLimitedBody
});
break;
case Dropbox.ApiError.NETWORK_ERROR:
alertCallback({
header: Locale.dropboxNetError,
body: Locale.dropboxNetErrorBody
});
break;
case Dropbox.ApiError.INVALID_PARAM:
case Dropbox.ApiError.OAUTH_ERROR:
case Dropbox.ApiError.INVALID_METHOD:
alertCallback({
header: Locale.dropboxSyncError,
body: Locale.dropboxErrorBody + ' ' + err.status
});
break;
case Dropbox.ApiError.CONFLICT:
break;
default:
alertCallback({
header: Locale.dropboxSyncError,
body: Locale.dropboxErrorRepeatBody + ' ' + err
});
break;
}
callback(false);
},
_callAndHandleError: function(callName, args, callback, errorAlertCallback) {
this._getClient((err, client) => {
if (err) {
return callback(err);
}
const ts = logger.ts();
logger.debug('Call', callName);
client[callName].apply(client, args.concat((...args) => {
const [err] = args;
logger.debug('Result', callName, logger.ts(ts), args);
if (err) {
this._handleUiError(err, errorAlertCallback, repeat => {
if (repeat) {
this._callAndHandleError(callName, args, callback, errorAlertCallback);
} else {
callback(err);
}
});
} else {
callback(...args);
}
}));
});
},
canUseBuiltInKeys: function() {
const isSelfHosted = !/^http(s?):\/\/localhost:8085/.test(location.href) &&
!/http(s?):\/\/(app|beta)\.keeweb\.info/.test(location.href);
return !!Launcher || !isSelfHosted;
},
getKey: getKey,
isValidKey: function() {
const key = getKey();
const isBuiltIn = key === DropboxKeys.AppFolder || key === DropboxKeys.FullDropbox;
return key && key.indexOf(' ') < 0 && (!isBuiltIn || this.canUseBuiltInKeys());
},
authenticate: function(complete, overrideAppKey) {
this._getClient(err => { complete(err); }, overrideAppKey);
},
logout: function() {
if (this._dropboxClient) {
try {
this._dropboxClient.signOut();
} catch (e) {
} finally {
this._dropboxClient.reset();
}
}
},
resetClient: function() {
this._dropboxClient = null;
},
receive: function() {
Dropbox.AuthDriver.Popup.oauthReceiver();
},
saveFile: function(fileName, data, rev, complete, alertCallback) {
if (rev) {
const opts = typeof rev === 'string' ? { lastVersionTag: rev, noOverwrite: true, noAutoRename: true } : undefined;
this._callAndHandleError('writeFile', [fileName, data, opts], complete, alertCallback);
} else {
const dir = UrlUtil.fileToDir(fileName);
this.list(dir, (err, files) => {
if (err) { return complete(err); }
const exists = files.some(file => file.toLowerCase() === fileName.toLowerCase());
if (exists) { return complete({ exists: true }); }
this._callAndHandleError('writeFile', [fileName, data], complete);
});
}
},
openFile: function(fileName, complete, errorAlertCallback) {
this._callAndHandleError('readFile', [fileName, { arrayBuffer: true }], complete, errorAlertCallback);
},
stat: function(fileName, complete, errorAlertCallback) {
this._callAndHandleError('stat', [fileName], complete, errorAlertCallback);
},
list: function(dir, complete) {
this._callAndHandleError('readdir', [dir || ''], (err, files, dirStat, filesStat) => {
if (files) {
files = files.filter(f => /\.kdbx$/i.test(f));
}
complete(err, files, dirStat, filesStat);
});
},
deleteFile: function(fileName, complete) {
this._callAndHandleError('remove', [fileName], complete);
},
mkdir: function(path, complete) {
this._callAndHandleError('mkdir', [path], complete);
},
canChooseFile: function() {
return !Launcher;
},
chooseFile: function(callback) {
new DropboxChooser(callback).choose();
}
};
module.exports = DropboxLink;

View File

@ -154,9 +154,6 @@
"openFailedRead": "Failed to read file",
"openNothingFound": "Nothing found",
"openNothingFoundBody": "No files which could be opened.",
"openNothingFoundBodyFolder": "Files are searched inside {} folder",
"openAppFolder": "app",
"openRootFolder": "root",
"openSelectFile": "Select a file",
"openSelectFileBody": "Select a file which you would like to open",
"openPassFor": "Password for",

View File

@ -75,8 +75,7 @@ _.extend(StorageBase.prototype, {
});
xhr.open(config.method || 'GET', config.url);
if (this._oauthToken) {
xhr.setRequestHeader('Authorization',
this._oauthToken.tokenType + ' ' + this._oauthToken.accessToken);
xhr.setRequestHeader('Authorization', 'Bearer ' + this._oauthToken.accessToken);
}
_.forEach(config.headers, (value, key) => {
xhr.setRequestHeader(key, value);
@ -128,9 +127,11 @@ _.extend(StorageBase.prototype, {
callback();
return;
}
const url = opts.url + '?client_id={cid}&scope={scope}&response_type=token&redirect_uri={url}'
const responseType = opts.code ? 'code' : 'token';
const url = opts.url + '?client_id={cid}&scope={scope}&response_type={type}&redirect_uri={url}'
.replace('{cid}', encodeURIComponent(opts.clientId))
.replace('{scope}', encodeURIComponent(opts.scope))
.replace('{type}', encodeURIComponent(responseType))
.replace('{url}', encodeURIComponent(this._getOauthRedirectUrl()));
this.logger.debug('OAuth popup opened');
if (!this._openPopup(url, 'OAuth', opts.width, opts.height)) {
@ -155,7 +156,7 @@ _.extend(StorageBase.prototype, {
} else {
this._oauthToken = token;
this.runtimeData.set(this.name + 'OAuthToken', token);
this.logger.debug('OAuth success');
this.logger.debug('OAuth token received');
callback();
}
};
@ -186,10 +187,12 @@ _.extend(StorageBase.prototype, {
_oauthRevokeToken: function(url) {
const token = this.runtimeData.get(this.name + 'OAuthToken');
if (token) {
this._xhr({
url: url.replace('{token}', token.accessToken),
statuses: [200, 401]
});
if (url) {
this._xhr({
url: url.replace('{token}', token.accessToken),
statuses: [200, 401]
});
}
this.runtimeData.unset(this.name + 'OAuthToken');
this._oauthToken = null;
}

View File

@ -1,7 +1,30 @@
const StorageBase = require('./storage-base');
const DropboxLink = require('../comp/dropbox-link');
const Locale = require('../util/locale');
const UrlUtil = require('../util/url-util');
const Launcher = require('../comp/launcher');
const DropboxKeys = {
AppFolder: 'qp7ctun6qt5n9d6',
FullDropbox: 'eor7hvv6u6oslq9'
};
const ApiError = {
NETWORK_ERROR: 0,
NO_CONTENT: 304,
INVALID_PARAM: 400,
INVALID_TOKEN: 401,
OAUTH_ERROR: 403,
NOT_FOUND: 404,
INVALID_METHOD: 405,
NOT_ACCEPTABLE: 406,
CONFLICT: 409,
RATE_LIMITED: 429,
SERVER_ERROR: 503,
OVER_QUOTA: 507
};
const DropboxCustomErrors = {
BadKey: 'bad-key'
};
const StorageDropbox = StorageBase.extend({
name: 'dropbox',
@ -14,10 +37,10 @@ const StorageDropbox = StorageBase.extend({
if (!err) {
return err;
}
if (err.status === DropboxLink.ERROR_NOT_FOUND) {
if (err.status === ApiError.NOT_FOUND) {
err.notFound = true;
}
if (err.status === DropboxLink.ERROR_CONFLICT) {
if (err.status === ApiError.CONFLICT) {
err.revConflict = true;
}
return err;
@ -53,8 +76,36 @@ const StorageDropbox = StorageBase.extend({
return folder;
},
_getKey: function() {
return this.appSettings.get('dropboxAppKey') || DropboxKeys.AppFolder;
},
_isValidKey: function() {
const key = this._getKey();
const isBuiltIn = key === DropboxKeys.AppFolder || key === DropboxKeys.FullDropbox;
return key && key.indexOf(' ') < 0 && (!isBuiltIn || this._canUseBuiltInKeys());
},
_canUseBuiltInKeys: function() {
const isSelfHosted = !/^http(s?):\/\/localhost:8085/.test(location.href) &&
!/http(s?):\/\/(app|beta)\.keeweb\.info/.test(location.href);
return !!Launcher || !isSelfHosted;
},
_getOAuthConfig: function() {
return {
scope: '',
url: 'https://www.dropbox.com/oauth2/authorize',
// tokenUrl: 'https://api.dropboxapi.com/oauth2/token',
// code: true
clientId: this._getKey(),
width: 600,
height: 400
};
},
needShowOpenConfig: function() {
return !DropboxLink.isValidKey();
return !this._isValidKey();
},
getOpenConfig: function() {
@ -69,19 +120,19 @@ const StorageDropbox = StorageBase.extend({
getSettingsConfig: function() {
const fields = [];
const appKey = DropboxLink.getKey();
const appKey = this._getKey();
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 folderField = {id: 'folder', title: 'dropboxFolder', desc: 'dropboxFolderSettingsDesc', type: 'text',
value: this.appSettings.get('dropboxFolder') || ''};
const canUseBuiltInKeys = DropboxLink.canUseBuiltInKeys();
const canUseBuiltInKeys = this._canUseBuiltInKeys();
if (canUseBuiltInKeys) {
fields.push(linkField);
if (appKey === DropboxLink.Keys.AppFolder) {
if (appKey === DropboxKeys.AppFolder) {
linkField.value = 'app';
} else if (appKey === DropboxLink.Keys.FullDropbox) {
} else if (appKey === DropboxKeys.FullDropbox) {
linkField.value = 'full';
fields.push(folderField);
} else {
@ -96,19 +147,18 @@ const StorageDropbox = StorageBase.extend({
},
applyConfig: function(config, callback) {
DropboxLink.authenticate(err => {
if (!err) {
if (config.folder) {
config.folder = this._fixConfigFolder(config.folder);
}
this.appSettings.set({
dropboxAppKey: config.key,
dropboxFolder: config.folder
});
DropboxLink.resetClient();
}
callback(err);
}, config.key);
if (config.key === DropboxKeys.AppFolder || config.key === DropboxKeys.FullDropbox) {
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,
dropboxFolder: config.folder
});
callback();
},
applySetting: function(key, value) {
@ -117,10 +167,10 @@ const StorageDropbox = StorageBase.extend({
key = 'dropboxAppKey';
switch (value) {
case 'app':
value = DropboxLink.Keys.AppFolder;
value = DropboxKeys.AppFolder;
break;
case 'full':
value = DropboxLink.Keys.FullDropbox;
value = DropboxKeys.FullDropbox;
break;
case 'custom':
value = '(your app key)';
@ -128,10 +178,11 @@ const StorageDropbox = StorageBase.extend({
default:
return;
}
DropboxLink.resetClient();
this._oauthRevokeToken();
break;
case 'key':
key = 'dropboxAppKey';
this._oauthRevokeToken();
break;
case 'folder':
key = 'dropboxFolder';
@ -147,60 +198,130 @@ const StorageDropbox = StorageBase.extend({
return '/' + fileName + '.kdbx';
},
_apiCall: function(args) {
this._oauthAuthorize(err => {
if (err) {
return args.error(err);
}
const host = args.host || 'api';
let headers;
let data = args.data;
if (args.apiArg) {
headers = { 'Dropbox-API-Arg': JSON.stringify(args.apiArg) };
if (args.data) {
headers['Content-Type'] = 'application/octet-stream';
}
} else if (args.data) {
data = JSON.stringify(data);
headers = {
'Content-Type': 'application/json'
};
}
this._xhr({
url: `https://${host}.dropboxapi.com/2/${args.method}`,
method: 'POST',
responseType: args.responseType || 'json',
headers: headers,
data: data,
statuses: args.statuses || undefined,
success: args.success,
error: (e, xhr) => {
let err = xhr.response && xhr.response.error || new Error('Network error');
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);
}
});
});
},
load: function(path, opts, callback) {
this.logger.debug('Load', path);
const ts = this.logger.ts();
path = this._toFullPath(path);
DropboxLink.openFile(path, (err, data, stat) => {
this.logger.debug('Loaded', path, stat ? stat.versionTag : null, this.logger.ts(ts));
err = this._convertError(err);
if (callback) { callback(err, data, stat ? { rev: stat.versionTag } : null); }
}, _.noop);
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
});
},
stat: function(path, opts, callback) {
this.logger.debug('Stat', path);
const ts = this.logger.ts();
path = this._toFullPath(path);
DropboxLink.stat(path, (err, stat) => {
if (stat && stat.isRemoved) {
err = new Error('File removed');
err.notFound = true;
}
this.logger.debug('Stated', path, stat ? stat.versionTag : null, this.logger.ts(ts));
err = this._convertError(err);
if (callback) { callback(err, stat ? { rev: stat.versionTag } : null); }
}, _.noop);
this._apiCall({
method: 'files/get_metadata',
data: { path },
success: stat => {
if (stat['.tag'] === 'file') {
stat = { rev: stat.rev };
} else if (stat['.tag'] === 'folder') {
stat = { folder: true };
}
this.logger.debug('Stated', path, stat.folder ? 'folder' : stat.rev, this.logger.ts(ts));
if (callback) { callback(null, stat); }
},
error: callback
});
},
save: function(path, opts, data, callback, rev) {
this.logger.debug('Save', path, rev);
const ts = this.logger.ts();
path = this._toFullPath(path);
DropboxLink.saveFile(path, data, rev, (err, stat) => {
this.logger.debug('Saved', path, this.logger.ts(ts));
if (!callback) { return; }
err = this._convertError(err);
callback(err, stat ? { rev: stat.versionTag } : null);
}, _.noop);
const arg = {
path,
mode: rev ? { '.tag': 'update', update: rev } : { '.tag': 'overwrite' }
};
this._apiCall({
method: 'files/upload',
host: 'content',
apiArg: arg,
data: data,
responseType: 'json',
success: stat => {
this.logger.debug('Saved', path, stat.rev, this.logger.ts(ts));
callback(null, { rev: stat.rev });
},
error: callback
});
},
list: function(callback) {
DropboxLink.authenticate((err) => {
if (err) { return callback(err); }
DropboxLink.list(this._toFullPath(''), (err, files, dirStat, filesStat) => {
if (err) { return callback(err); }
const fileList = filesStat
.filter(f => !f.isFolder && !f.isRemoved && UrlUtil.isKdbx(f.name))
this.logger.debug('List');
const ts = this.logger.ts();
this._apiCall({
method: 'files/list_folder',
data: {
path: this._toFullPath(''),
recursive: false
},
success: data => {
this.logger.debug('Listed', this.logger.ts(ts));
const fileList = data.entries
.filter(f => f['.tag'] === 'file' && f.rev && UrlUtil.isKdbx(f.name))
.map(f => ({
name: f.name,
path: this._toRelPath(f.path),
rev: f.versionTag
path: this._toRelPath(f['path_display']),
ref: f.rev
}));
const dir = dirStat.inAppFolder ? Locale.openAppFolder
: (UrlUtil.trimStartSlash(dirStat.path) || Locale.openRootFolder);
callback(null, fileList, dir);
});
callback(null, fileList);
},
error: callback
});
},
@ -208,28 +329,35 @@ const StorageDropbox = StorageBase.extend({
this.logger.debug('Remove', path);
const ts = this.logger.ts();
path = this._toFullPath(path);
DropboxLink.deleteFile(path, err => {
this.logger.debug('Removed', path, this.logger.ts(ts));
return callback && callback(err);
}, _.noop);
this._apiCall({
method: 'files/delete',
data: { path },
success: () => {
this.logger.debug('Removed', path, this.logger.ts(ts));
callback();
},
error: callback
});
},
mkdir: function(path, callback) {
DropboxLink.authenticate((err) => {
if (err) { return callback(err); }
this.logger.debug('Make dir', path);
const ts = this.logger.ts();
path = this._toFullPath(path);
DropboxLink.mkdir(path, err => {
this.logger.debug('Make dir', path);
const ts = this.logger.ts();
path = this._toFullPath(path);
this._apiCall({
method: 'files/create_folder',
data: { path },
success: () => {
this.logger.debug('Made dir', path, this.logger.ts(ts));
return callback && callback(err);
}, _.noop);
callback();
},
error: callback
});
},
setEnabled: function(enabled) {
if (!enabled) {
DropboxLink.logout();
this._oauthRevokeToken();
}
StorageBase.prototype.setEnabled.call(this, enabled);
}

View File

@ -1,7 +1,6 @@
const UrlUtil = {
multiSlashRegex: /\/{2,}/g,
lastPartRegex: /[^\/\\]+$/,
trimStartSlashRegex: /^\\/,
lastPartRegex: /\/?[^\/\\]+$/,
kdbxEndRegex: /\.kdbx$/i,
getDataFileName: function(url) {
@ -22,11 +21,7 @@ const UrlUtil = {
},
fileToDir: function(url) {
return url.replace(this.lastPartRegex, '');
},
trimStartSlash: function(url) {
return url.replace(this.trimStartSlashRegex, '');
return url.replace(this.lastPartRegex, '') || '/';
}
};

View File

@ -4,7 +4,7 @@ const OpenConfigView = require('./open-config-view');
const Keys = require('../const/keys');
const Alerts = require('../comp/alerts');
const SecureInput = require('../comp/secure-input');
const DropboxLink = require('../comp/dropbox-link');
const DropboxChooser = require('../comp/dropbox-chooser');
const FeatureDetector = require('../util/feature-detector');
const Logger = require('../util/logger');
const Locale = require('../util/locale');
@ -78,7 +78,7 @@ const OpenView = Backbone.View.extend({
!(this.model.settings.get('canOpenDemo') && !this.model.settings.get('demoOpened'));
this.renderTemplate({
lastOpenFiles: this.getLastOpenFiles(),
canOpenKeyFromDropbox: DropboxLink.canChooseFile() && Storage.dropbox.enabled,
canOpenKeyFromDropbox: !Launcher && Storage.dropbox.enabled,
demoOpened: this.model.settings.get('demoOpened'),
storageProviders: storageProviders,
canOpen: this.model.settings.get('canOpen'),
@ -304,14 +304,14 @@ const OpenView = Backbone.View.extend({
openKeyFileFromDropbox: function() {
if (!this.busy) {
DropboxLink.chooseFile((err, res) => {
new DropboxChooser((err, res) => {
if (err) {
return;
}
this.params.keyFileData = res.data;
this.params.keyFileName = res.name;
this.displayOpenKeyFile();
});
}).choose();
}
},
@ -598,7 +598,7 @@ const OpenView = Backbone.View.extend({
const icon = this.$el.find('.open__icon-storage[data-storage=' + storage.name + ']');
this.busy = true;
icon.toggleClass('flip3d', true);
storage.list((err, files, dir) => {
storage.list((err, files) => {
icon.toggleClass('flip3d', false);
this.busy = false;
if (err || !files) {
@ -613,13 +613,9 @@ const OpenView = Backbone.View.extend({
allStorageFiles[file.path] = file;
});
if (!buttons.length) {
let body = Locale.openNothingFoundBody;
if (dir) {
body += ' ' + Locale.openNothingFoundBodyFolder.replace('{}', dir);
}
Alerts.error({
header: Locale.openNothingFound,
body: body
body: Locale.openNothingFoundBody
});
return;
}

View File

@ -11,8 +11,6 @@
<li><a href="http://underscorejs.org/" target="_blank">underscore</a><span class="muted-color">, utility-belt library for JavaScript</span></li>
<li><a href="https://jquery.com/" target="_blank">jQuery</a><span class="muted-color">, fast, small, and feature-rich JavaScript library</span></li>
<li><a href="http://handlebarsjs.com/" target="_blank">handlebars</a><span class="muted-color">, semantic templates</span></li>
<li><a href="https://github.com/dropbox/dropbox-js" target="_blank">dropbox-js</a><span class="muted-color">, unofficial JavaScript library for
the Dropbox Core API</span></li>
<li><a href="https://github.com/LazarSoft/jsqrcode" target="_blank">jsqrcode</a><span class="muted-color">, QR code scanner,
<a href="{{licenseLinkApache}}" class="muted-color" target="_blank">Apache-2.0 license</a></span></li>
</ul>

View File

@ -25,7 +25,6 @@
"backbone": "1.3.3",
"baron": "2.2.2",
"bourbon": "4.2.7",
"dropbox": "keeweb/dropbox-js#0ac0efdc2711eece73f6ac044459e1fd0d5e9390",
"font-awesome": "4.7.0",
"kdbxweb": "1.0.1",
"normalize.css": "5.0.0",

View File

@ -3,6 +3,7 @@ Release notes
##### v1.5.0 (TBD)
`+` plugins
`*` translations are available only as plugins
`*` Dropbox API V2
`+` mobile field editing improvements
`+` file path hint in recent files list
`+` cacheConfigSettings config option