mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-24 08:12:24 +02:00
Merge branch 'dropbox'
This commit is contained in:
commit
613fa20806
3
CliClient/.gitignore
vendored
3
CliClient/.gitignore
vendored
@ -18,4 +18,5 @@ tests/cli-integration/
|
||||
tests/sync
|
||||
out.txt
|
||||
linkToLocal.sh
|
||||
yarn-error.log
|
||||
yarn-error.log
|
||||
tests/support/dropbox-auth.txt
|
@ -78,10 +78,26 @@ class Command extends BaseCommand {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (syncTargetMd.name === 'dropbox') { // Dropbox
|
||||
const api = await syncTarget.api();
|
||||
const loginUrl = api.loginUrl();
|
||||
this.stdout(_('To allow Joplin to synchronise with Dropbox, please follow the steps below:'));
|
||||
this.stdout(_('Step 1: Open this URL in your browser to authorise the application:'));
|
||||
this.stdout(loginUrl);
|
||||
const authCode = await this.prompt(_('Step 2: Enter the code provided by Dropbox:'), { type: 'string' });
|
||||
if (!authCode) {
|
||||
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await api.execAuthToken(authCode);
|
||||
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', response.access_token);
|
||||
api.setAuthToken(response.access_token);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.stdout(_('Not authentified with %s. Please provide any missing credentials.', syncTarget.label()));
|
||||
this.stdout(_('Not authentified with %s. Please provide any missing credentials.', syncTargetMd.label));
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -100,6 +116,7 @@ class Command extends BaseCommand {
|
||||
this.releaseLockFn_ = null;
|
||||
|
||||
// Lock is unique per profile/database
|
||||
// TODO: use SQLite database to do lock?
|
||||
const lockFilePath = require('os').tmpdir() + '/synclock_' + md5(escape(Setting.value('profileDir'))); // https://github.com/pvorb/node-md5/issues/41
|
||||
if (!await fs.pathExists(lockFilePath)) await fs.writeFile(lockFilePath, 'synclock');
|
||||
|
||||
@ -130,7 +147,7 @@ class Command extends BaseCommand {
|
||||
|
||||
const syncTarget = reg.syncTarget(this.syncTargetId_);
|
||||
|
||||
if (!syncTarget.isAuthenticated()) {
|
||||
if (!await syncTarget.isAuthenticated()) {
|
||||
app().gui().showConsole();
|
||||
app().gui().maximizeConsole();
|
||||
|
||||
@ -197,7 +214,7 @@ class Command extends BaseCommand {
|
||||
|
||||
const syncTarget = reg.syncTarget(syncTargetId);
|
||||
|
||||
if (syncTarget.isAuthenticated()) {
|
||||
if (await syncTarget.isAuthenticated()) {
|
||||
const sync = await syncTarget.synchronizer();
|
||||
if (sync) await sync.cancel();
|
||||
} else {
|
||||
|
5
CliClient/package-lock.json
generated
5
CliClient/package-lock.json
generated
@ -983,11 +983,6 @@
|
||||
"wrappy": "1.0.2"
|
||||
}
|
||||
},
|
||||
"os-tmpdir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
|
||||
},
|
||||
"parse-data-uri": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-data-uri/-/parse-data-uri-0.2.0.tgz",
|
||||
|
@ -339,22 +339,17 @@ describe('Synchronizer', function() {
|
||||
it('should delete local folder', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let folder2 = await Folder.save({ title: "folder2" });
|
||||
await synchronizer().start();
|
||||
let context1 = await synchronizer().start();
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
await sleep(0.1);
|
||||
|
||||
let context2 = await synchronizer().start();
|
||||
await Folder.delete(folder2.id);
|
||||
|
||||
await synchronizer().start();
|
||||
await synchronizer().start({ context: context2 });
|
||||
|
||||
await switchClient(1);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
await synchronizer().start({ context: context1 });
|
||||
let items = await allItems();
|
||||
await localItemsSameAsRemote(items, expect);
|
||||
}));
|
||||
@ -377,7 +372,7 @@ describe('Synchronizer', function() {
|
||||
expect(items.length).toBe(1);
|
||||
expect(items[0].title).toBe('note1');
|
||||
expect(items[0].is_conflict).toBe(1);
|
||||
}));
|
||||
}));
|
||||
|
||||
it('should resolve conflict if note has been deleted remotely and locally', asyncTest(async () => {
|
||||
let folder = await Folder.save({ title: "folder" });
|
||||
@ -438,7 +433,7 @@ describe('Synchronizer', function() {
|
||||
|
||||
expect(items1.length).toBe(0);
|
||||
expect(items1.length).toBe(items2.length);
|
||||
}));
|
||||
}));
|
||||
|
||||
it('should handle conflict when remote note is deleted then local note is modified', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
@ -547,11 +542,11 @@ describe('Synchronizer', function() {
|
||||
let n1 = await Note.save({ title: "mynote" });
|
||||
let n2 = await Note.save({ title: "mynote2" });
|
||||
let tag = await Tag.save({ title: 'mytag' });
|
||||
await synchronizer().start();
|
||||
let context1 = await synchronizer().start();
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
let context2 = await synchronizer().start();
|
||||
if (withEncryption) {
|
||||
const masterKey_2 = await MasterKey.load(masterKey.id);
|
||||
await encryptionService().loadMasterKey(masterKey_2, '123456', true);
|
||||
@ -565,21 +560,21 @@ describe('Synchronizer', function() {
|
||||
await Tag.addNote(remoteTag.id, n2.id);
|
||||
let noteIds = await Tag.noteIds(tag.id);
|
||||
expect(noteIds.length).toBe(2);
|
||||
await synchronizer().start();
|
||||
context2 = await synchronizer().start({ context: context2 });
|
||||
|
||||
await switchClient(1);
|
||||
|
||||
await synchronizer().start();
|
||||
context1 = await synchronizer().start({ context: context1 });
|
||||
let remoteNoteIds = await Tag.noteIds(tag.id);
|
||||
expect(remoteNoteIds.length).toBe(2);
|
||||
await Tag.removeNote(tag.id, n1.id);
|
||||
remoteNoteIds = await Tag.noteIds(tag.id);
|
||||
expect(remoteNoteIds.length).toBe(1);
|
||||
await synchronizer().start();
|
||||
context1 = await synchronizer().start({ context: context1 });
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
context2 = await synchronizer().start({ context: context2 });
|
||||
noteIds = await Tag.noteIds(tag.id);
|
||||
expect(noteIds.length).toBe(1);
|
||||
expect(remoteNoteIds[0]).toBe(noteIds[0]);
|
||||
|
@ -16,6 +16,7 @@ const { FileApi } = require('lib/file-api.js');
|
||||
const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js');
|
||||
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
|
||||
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav.js');
|
||||
const { FileApiDriverDropbox } = require('lib/file-api-driver-dropbox.js');
|
||||
const BaseService = require('lib/services/BaseService.js');
|
||||
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
@ -25,9 +26,11 @@ const SyncTargetMemory = require('lib/SyncTargetMemory.js');
|
||||
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
||||
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
||||
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
|
||||
const SyncTargetDropbox = require('lib/SyncTargetDropbox.js');
|
||||
const EncryptionService = require('lib/services/EncryptionService.js');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
|
||||
const WebDavApi = require('lib/WebDavApi');
|
||||
const DropboxApi = require('lib/DropboxApi');
|
||||
|
||||
let databases_ = [];
|
||||
let synchronizers_ = [];
|
||||
@ -51,10 +54,12 @@ SyncTargetRegistry.addClass(SyncTargetMemory);
|
||||
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||
SyncTargetRegistry.addClass(SyncTargetNextcloud);
|
||||
SyncTargetRegistry.addClass(SyncTargetDropbox);
|
||||
|
||||
// const syncTargetId_ = SyncTargetRegistry.nameToId("nextcloud");
|
||||
const syncTargetId_ = SyncTargetRegistry.nameToId("memory");
|
||||
//const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem');
|
||||
// const syncTargetId_ = SyncTargetRegistry.nameToId('dropbox');
|
||||
const syncDir = __dirname + '/../tests/sync';
|
||||
|
||||
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 100;//400;
|
||||
@ -247,25 +252,15 @@ function fileApi() {
|
||||
|
||||
const api = new WebDavApi(options);
|
||||
fileApi_ = new FileApi('', new FileApiDriverWebDav(api));
|
||||
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('dropbox')) {
|
||||
const api = new DropboxApi();
|
||||
const authTokenPath = __dirname + '/support/dropbox-auth.txt';
|
||||
const authToken = fs.readFileSync(authTokenPath, 'utf8');
|
||||
if (!authToken) throw new Error('Dropbox auth token missing in ' + authTokenPath);
|
||||
api.setAuthToken(authToken);
|
||||
fileApi_ = new FileApi('', new FileApiDriverDropbox(api));
|
||||
}
|
||||
|
||||
// } else if (syncTargetId == Setting.SYNC_TARGET_ONEDRIVE) {
|
||||
// let auth = require('./onedrive-auth.json');
|
||||
// if (!auth) {
|
||||
// const oneDriveApiUtils = new OneDriveApiNodeUtils(oneDriveApi);
|
||||
// auth = await oneDriveApiUtils.oauthDance();
|
||||
// fs.writeFileSync('./onedrive-auth.json', JSON.stringify(auth));
|
||||
// process.exit(1);
|
||||
// } else {
|
||||
// auth = JSON.parse(auth);
|
||||
// }
|
||||
|
||||
// // const oneDriveApiUtils = new OneDriveApiNodeUtils(reg.oneDriveApi());
|
||||
// // const auth = await oneDriveApiUtils.oauthDance(this);
|
||||
// // Setting.setValue('sync.3.auth', auth ? JSON.stringify(auth) : null);
|
||||
// // if (!auth) return;
|
||||
// }
|
||||
|
||||
fileApi_.setLogger(logger);
|
||||
fileApi_.setSyncTargetId(syncTargetId_);
|
||||
fileApi_.requestRepeatCount_ = 0;
|
||||
@ -306,8 +301,9 @@ function asyncTest(callback) {
|
||||
await callback();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
done();
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
|
62
ElectronClient/app/gui/DropboxLoginScreen.jsx
Normal file
62
ElectronClient/app/gui/DropboxLoginScreen.jsx
Normal file
@ -0,0 +1,62 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { Header } = require('./Header.min.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Shared = require('lib/components/shared/dropbox-login-shared');
|
||||
|
||||
class DropboxLoginScreenComponent extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.shared_ = new Shared(
|
||||
this,
|
||||
(msg) => bridge().showInfoMessageBox(msg),
|
||||
(msg) => bridge().showErrorMessageBox(msg)
|
||||
);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.shared_.refreshUrl();
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const headerStyle = {
|
||||
width: style.width,
|
||||
};
|
||||
|
||||
const inputStyle = Object.assign({}, theme.inputStyle, { width: 500 });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header style={headerStyle} />
|
||||
<div style={{padding: theme.margin}}>
|
||||
<p style={theme.textStyle}>{_('To allow Joplin to synchronise with Dropbox, please follow the steps below:')}</p>
|
||||
<p style={theme.textStyle}>{_('Step 1: Open this URL in your browser to authorise the application:')}</p>
|
||||
<a style={theme.textStyle} href="#" onClick={this.shared_.loginUrl_click}>{this.state.loginUrl}</a>
|
||||
<p style={theme.textStyle}>{_('Step 2: Enter the code provided by Dropbox:')}</p>
|
||||
<p><input type="text" value={this.state.authCode} onChange={this.shared_.authCodeInput_change} style={inputStyle}/></p>
|
||||
<button disabled={this.state.checkingAuthToken} onClick={this.shared_.submit_click}>{_('Submit')}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
};
|
||||
};
|
||||
|
||||
const DropboxLoginScreen = connect(mapStateToProps)(DropboxLoginScreenComponent);
|
||||
|
||||
module.exports = { DropboxLoginScreen };
|
@ -370,7 +370,7 @@ class NoteTextComponent extends React.Component {
|
||||
webviewReady: true,
|
||||
});
|
||||
|
||||
if (Setting.value('env') === 'dev') this.webview_.openDevTools();
|
||||
// if (Setting.value('env') === 'dev') this.webview_.openDevTools();
|
||||
}
|
||||
|
||||
webview_ref(element) {
|
||||
|
@ -8,6 +8,7 @@ const Setting = require('lib/models/Setting.js');
|
||||
|
||||
const { MainScreen } = require('./MainScreen.min.js');
|
||||
const { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js');
|
||||
const { DropboxLoginScreen } = require('./DropboxLoginScreen.min.js');
|
||||
const { StatusScreen } = require('./StatusScreen.min.js');
|
||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||
const { ConfigScreen } = require('./ConfigScreen.min.js');
|
||||
@ -75,6 +76,7 @@ class RootComponent extends React.Component {
|
||||
const screens = {
|
||||
Main: { screen: MainScreen },
|
||||
OneDriveLogin: { screen: OneDriveLoginScreen, title: () => _('OneDrive Login') },
|
||||
DropboxLogin: { screen: DropboxLoginScreen, title: () => _('Dropbox Login') },
|
||||
Import: { screen: ImportScreen, title: () => _('Import') },
|
||||
Config: { screen: ConfigScreen, title: () => _('Options') },
|
||||
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
|
||||
|
2
ElectronClient/app/package-lock.json
generated
2
ElectronClient/app/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "1.0.79",
|
||||
"version": "1.0.80",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "1.0.79",
|
||||
"version": "1.0.80",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
@ -62,6 +62,7 @@ globalStyle.icon = {
|
||||
globalStyle.lineInput = {
|
||||
color: globalStyle.color,
|
||||
backgroundColor: globalStyle.backgroundColor,
|
||||
fontFamily: globalStyle.fontFamily,
|
||||
};
|
||||
|
||||
globalStyle.textStyle = {
|
||||
|
@ -28,7 +28,7 @@ Linux | <a href='https://github.com/laurent22/joplin/releases/download/
|
||||
|
||||
Operating System | Download | Alt. Download
|
||||
-----------------|----------|----------------
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.112/joplin-v1.0.112.apk)
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.114/joplin-v1.0.114.apk)
|
||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeIOS.png'/></a> | -
|
||||
|
||||
## Terminal application
|
||||
|
@ -90,8 +90,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 22
|
||||
versionCode 2097290
|
||||
versionName "1.0.112"
|
||||
versionCode 2097292
|
||||
versionName "1.0.114"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
||||
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
|
||||
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
|
||||
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV.js');
|
||||
const SyncTargetDropbox = require('lib/SyncTargetDropbox.js');
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||
const BaseService = require('lib/services/BaseService');
|
||||
@ -38,6 +39,7 @@ SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
|
||||
SyncTargetRegistry.addClass(SyncTargetNextcloud);
|
||||
SyncTargetRegistry.addClass(SyncTargetWebDAV);
|
||||
SyncTargetRegistry.addClass(SyncTargetDropbox);
|
||||
|
||||
class BaseApplication {
|
||||
|
||||
@ -421,8 +423,14 @@ class BaseApplication {
|
||||
if (Setting.value('firstStart')) {
|
||||
const locale = shim.detectAndSetLocale(Setting);
|
||||
reg.logger().info('First start: detected locale as ' + locale);
|
||||
if (Setting.value('env') === 'dev') Setting.setValue('sync.target', SyncTargetRegistry.nameToId('onedrive_dev'));
|
||||
Setting.setValue('firstStart', 0)
|
||||
|
||||
if (Setting.value('env') === 'dev') {
|
||||
Setting.setValue('showTrayIcon', 0);
|
||||
Setting.setValue('autoUpdateEnabled', 0);
|
||||
Setting.setValue('sync.interval', 3600);
|
||||
}
|
||||
|
||||
Setting.setValue('firstStart', 0);
|
||||
} else {
|
||||
setLocale(Setting.value('locale'));
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ class BaseSyncTarget {
|
||||
return this.db_;
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
async isAuthenticated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -66,6 +66,10 @@ class BaseSyncTarget {
|
||||
return this.fileApi_;
|
||||
}
|
||||
|
||||
fileApiSync() {
|
||||
return this.fileApi_;
|
||||
}
|
||||
|
||||
// Usually each sync target should create and setup its own file API via initFileApi()
|
||||
// but for testing purposes it might be convenient to provide it here so that multiple
|
||||
// clients can share and sync to the same file api (see test-utils.js)
|
||||
@ -109,7 +113,7 @@ class BaseSyncTarget {
|
||||
|
||||
async syncStarted() {
|
||||
if (!this.synchronizer_) return false;
|
||||
if (!this.isAuthenticated()) return false;
|
||||
if (!await this.isAuthenticated()) return false;
|
||||
const sync = await this.synchronizer();
|
||||
return sync.state() != 'idle';
|
||||
}
|
||||
|
214
ReactNativeClient/lib/DropboxApi.js
Normal file
214
ReactNativeClient/lib/DropboxApi.js
Normal file
@ -0,0 +1,214 @@
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const JoplinError = require('lib/JoplinError');
|
||||
const URL = require('url-parse');
|
||||
const { time } = require('lib/time-utils');
|
||||
const EventDispatcher = require('lib/EventDispatcher');
|
||||
|
||||
class DropboxApi {
|
||||
|
||||
constructor(options) {
|
||||
this.logger_ = new Logger();
|
||||
this.options_ = options;
|
||||
this.authToken_ = null;
|
||||
this.dispatcher_ = new EventDispatcher();
|
||||
}
|
||||
|
||||
clientId() {
|
||||
return this.options_.id;
|
||||
}
|
||||
|
||||
clientSecret() {
|
||||
return this.options_.secret;
|
||||
}
|
||||
|
||||
setLogger(l) {
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
authToken() {
|
||||
return this.authToken_; // Without the "Bearer " prefix
|
||||
}
|
||||
|
||||
on(eventName, callback) {
|
||||
return this.dispatcher_.on(eventName, callback);
|
||||
}
|
||||
|
||||
setAuthToken(v) {
|
||||
this.authToken_ = v;
|
||||
this.dispatcher_.dispatch('authRefreshed', this.authToken());
|
||||
}
|
||||
|
||||
loginUrl() {
|
||||
return 'https://www.dropbox.com/oauth2/authorize?response_type=code&client_id=' + this.clientId();
|
||||
}
|
||||
|
||||
baseUrl(endPointFormat) {
|
||||
if (['content', 'api'].indexOf(endPointFormat) < 0) throw new Error('Invalid end point format: ' + endPointFormat);
|
||||
return 'https://' + endPointFormat + '.dropboxapi.com/2';
|
||||
}
|
||||
|
||||
requestToCurl_(url, options) {
|
||||
let output = [];
|
||||
output.push('curl');
|
||||
if (options.method) output.push('-X ' + options.method);
|
||||
if (options.headers) {
|
||||
for (let n in options.headers) {
|
||||
if (!options.headers.hasOwnProperty(n)) continue;
|
||||
output.push('-H ' + "'" + n + ': ' + options.headers[n] + "'");
|
||||
}
|
||||
}
|
||||
if (options.body) output.push('--data ' + '"' + options.body + '"');
|
||||
output.push(url);
|
||||
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
async execAuthToken(authCode) {
|
||||
const postData = {
|
||||
code: authCode,
|
||||
grant_type: 'authorization_code',
|
||||
client_id: this.clientId(),
|
||||
client_secret: this.clientSecret(),
|
||||
};
|
||||
|
||||
var formBody = [];
|
||||
for (var property in postData) {
|
||||
var encodedKey = encodeURIComponent(property);
|
||||
var encodedValue = encodeURIComponent(postData[property]);
|
||||
formBody.push(encodedKey + "=" + encodedValue);
|
||||
}
|
||||
formBody = formBody.join("&");
|
||||
|
||||
const response = await shim.fetch('https://api.dropboxapi.com/oauth2/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||
},
|
||||
body: formBody
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
if (!response.ok) throw new Error(responseText);
|
||||
return JSON.parse(responseText);
|
||||
}
|
||||
|
||||
isTokenError(status, responseText) {
|
||||
if (status === 401) return true;
|
||||
if (responseText.indexOf('OAuth 2 access token is malformed') >= 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
async exec(method, path = '', body = null, headers = null, options = null) {
|
||||
if (headers === null) headers = {};
|
||||
if (options === null) options = {};
|
||||
if (!options.target) options.target = 'string';
|
||||
|
||||
const authToken = this.authToken();
|
||||
|
||||
if (authToken) headers['Authorization'] = 'Bearer ' + authToken;
|
||||
|
||||
const endPointFormat = ['files/upload', 'files/download'].indexOf(path) >= 0 ? 'content' : 'api';
|
||||
|
||||
if (endPointFormat === 'api') {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
if (body && typeof body === 'object') body = JSON.stringify(body);
|
||||
} else {
|
||||
headers['Content-Type'] = 'application/octet-stream';
|
||||
}
|
||||
|
||||
const fetchOptions = {};
|
||||
fetchOptions.headers = headers;
|
||||
fetchOptions.method = method;
|
||||
if (options.path) fetchOptions.path = options.path;
|
||||
if (body) fetchOptions.body = body;
|
||||
|
||||
const url = path.indexOf('https://') === 0 ? path : this.baseUrl(endPointFormat) + '/' + path;
|
||||
|
||||
let tryCount = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
let response = null;
|
||||
|
||||
// console.info(this.requestToCurl_(url, fetchOptions));
|
||||
|
||||
// console.info(method + ' ' + url);
|
||||
|
||||
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
|
||||
response = await shim.uploadBlob(url, fetchOptions);
|
||||
} else if (options.target == 'string') {
|
||||
response = await shim.fetch(url, fetchOptions);
|
||||
} else { // file
|
||||
response = await shim.fetchBlob(url, fetchOptions);
|
||||
}
|
||||
|
||||
const responseText = await response.text();
|
||||
|
||||
// console.info('Response: ' + responseText);
|
||||
|
||||
let responseJson_ = null;
|
||||
const loadResponseJson = () => {
|
||||
if (!responseText) return null;
|
||||
if (responseJson_) return responseJson_;
|
||||
try {
|
||||
responseJson_ = JSON.parse(responseText);
|
||||
} catch (error) {
|
||||
return { error: responseText };
|
||||
}
|
||||
return responseJson_;
|
||||
}
|
||||
|
||||
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
|
||||
const newError = (message) => {
|
||||
const json = loadResponseJson();
|
||||
let code = '';
|
||||
if (json && json.error_summary) {
|
||||
code = json.error_summary;
|
||||
}
|
||||
|
||||
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
|
||||
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
|
||||
const shortResponseText = (responseText + '').substr(0, 1024);
|
||||
const error = new JoplinError(method + ' ' + path + ': ' + message + ' (' + response.status + '): ' + shortResponseText, code);
|
||||
error.httpStatus = response.status;
|
||||
return error;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const json = loadResponseJson();
|
||||
if (this.isTokenError(response.status, responseText)) {
|
||||
this.setAuthToken(null);
|
||||
}
|
||||
|
||||
// When using fetchBlob we only get a string (not xml or json) back
|
||||
if (options.target === 'file') throw newError('fetchBlob error');
|
||||
|
||||
throw newError('Error');
|
||||
}
|
||||
|
||||
if (options.responseFormat === 'text') return responseText;
|
||||
|
||||
return loadResponseJson();
|
||||
} catch (error) {
|
||||
tryCount++;
|
||||
if (error.code.indexOf('too_many_write_operations') >= 0) {
|
||||
this.logger().warn('too_many_write_operations ' + tryCount);
|
||||
if (tryCount >= 3) {
|
||||
throw error;
|
||||
}
|
||||
await time.sleep(tryCount * 2);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = DropboxApi;
|
@ -32,4 +32,4 @@ class EventDispatcher {
|
||||
|
||||
}
|
||||
|
||||
module.exports = { EventDispatcher };
|
||||
module.exports = EventDispatcher;
|
73
ReactNativeClient/lib/SyncTargetDropbox.js
Normal file
73
ReactNativeClient/lib/SyncTargetDropbox.js
Normal file
@ -0,0 +1,73 @@
|
||||
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const DropboxApi = require('lib/DropboxApi');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { parameters } = require('lib/parameters.js');
|
||||
const { FileApi } = require('lib/file-api.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const { FileApiDriverDropbox } = require('lib/file-api-driver-dropbox.js');
|
||||
|
||||
class SyncTargetDropbox extends BaseSyncTarget {
|
||||
|
||||
static id() {
|
||||
return 7;
|
||||
}
|
||||
|
||||
constructor(db, options = null) {
|
||||
super(db, options);
|
||||
this.api_ = null;
|
||||
}
|
||||
|
||||
static targetName() {
|
||||
return 'dropbox';
|
||||
}
|
||||
|
||||
static label() {
|
||||
return _('Dropbox');
|
||||
}
|
||||
|
||||
authRouteName() {
|
||||
return 'DropboxLogin';
|
||||
}
|
||||
|
||||
async isAuthenticated() {
|
||||
const f = await this.fileApi();
|
||||
return !!f.driver().api().authToken();
|
||||
}
|
||||
|
||||
async api() {
|
||||
const fileApi = await this.fileApi();
|
||||
return fileApi.driver().api();
|
||||
}
|
||||
|
||||
async initFileApi() {
|
||||
const params = parameters().dropbox;
|
||||
|
||||
const api = new DropboxApi({
|
||||
id: params.id,
|
||||
secret: params.secret,
|
||||
});
|
||||
|
||||
api.on('authRefreshed', (auth) => {
|
||||
this.logger().info('Saving updated OneDrive auth.');
|
||||
Setting.setValue('sync.' + SyncTargetDropbox.id() + '.auth', auth ? auth : null);
|
||||
});
|
||||
|
||||
const authToken = Setting.value('sync.' + SyncTargetDropbox.id() + '.auth');
|
||||
api.setAuthToken(authToken);
|
||||
|
||||
const appDir = '';
|
||||
const fileApi = new FileApi(appDir, new FileApiDriverDropbox(api));
|
||||
fileApi.setSyncTargetId(SyncTargetDropbox.id());
|
||||
fileApi.setLogger(this.logger());
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
async initSynchronizer() {
|
||||
if (!(await this.isAuthenticated())) throw new Error('User is not authentified');
|
||||
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = SyncTargetDropbox;
|
@ -19,7 +19,7 @@ class SyncTargetFilesystem extends BaseSyncTarget {
|
||||
return _('File system');
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
async isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ class SyncTargetMemory extends BaseSyncTarget {
|
||||
return 'Memory';
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
async isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ class SyncTargetNextcloud extends BaseSyncTarget {
|
||||
return _('Nextcloud');
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
async isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ class SyncTargetOneDrive extends BaseSyncTarget {
|
||||
return _('OneDrive');
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
async isAuthenticated() {
|
||||
return this.api().auth();
|
||||
}
|
||||
|
||||
@ -80,7 +80,7 @@ class SyncTargetOneDrive extends BaseSyncTarget {
|
||||
}
|
||||
|
||||
async initSynchronizer() {
|
||||
if (!this.isAuthenticated()) throw new Error('User is not authentified');
|
||||
if (!await this.isAuthenticated()) throw new Error('User is not authentified');
|
||||
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ class SyncTargetWebDAV extends BaseSyncTarget {
|
||||
return _('WebDAV');
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
async isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ const globalStyle = {
|
||||
dividerColor: "#dddddd",
|
||||
selectedColor: '#e5e5e5',
|
||||
disabledOpacity: 0.2,
|
||||
colorUrl: '#000CFF',
|
||||
|
||||
raisedBackgroundColor: "#0080EF",
|
||||
raisedColor: "#003363",
|
||||
@ -89,10 +90,17 @@ function addExtraStyles(style) {
|
||||
fontSize: style.fontSize,
|
||||
};
|
||||
|
||||
style.urlText = {
|
||||
color: style.colorUrl,
|
||||
fontSize: style.fontSize,
|
||||
};
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
function themeStyle(theme) {
|
||||
if (!theme) throw new Error('Theme not set');
|
||||
|
||||
if (themeCache_[theme]) return themeCache_[theme];
|
||||
|
||||
let output = Object.assign({}, globalStyle);
|
||||
|
83
ReactNativeClient/lib/components/screens/dropbox-login.js
Normal file
83
ReactNativeClient/lib/components/screens/dropbox-login.js
Normal file
@ -0,0 +1,83 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { View, Button, Text, TextInput, TouchableOpacity, StyleSheet } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('lib/components/screen-header.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const { dialogs } = require('lib/dialogs.js');
|
||||
const Shared = require('lib/components/shared/dropbox-login-shared');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
|
||||
class DropboxLoginScreenComponent extends BaseScreenComponent {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.styles_ = {};
|
||||
|
||||
this.shared_ = new Shared(
|
||||
this,
|
||||
(msg) => dialogs.info(this, msg),
|
||||
(msg) => dialogs.error(this, msg)
|
||||
);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.shared_.refreshUrl();
|
||||
}
|
||||
|
||||
styles() {
|
||||
const themeId = this.props.theme;
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
container: {
|
||||
padding: theme.margin,
|
||||
},
|
||||
stepText: Object.assign({}, theme.normalText, { marginBottom: theme.margin }),
|
||||
urlText: Object.assign({}, theme.urlText, { marginBottom: theme.margin }),
|
||||
}
|
||||
|
||||
this.styles_[themeId] = StyleSheet.create(styles);
|
||||
return this.styles_[themeId];
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
return (
|
||||
<View style={this.styles().screen}>
|
||||
<ScreenHeader title={_('Login with Dropbox')}/>
|
||||
|
||||
<View style={this.styles().container}>
|
||||
<Text style={this.styles().stepText}>{_('To allow Joplin to synchronise with Dropbox, please follow the steps below:')}</Text>
|
||||
<Text style={this.styles().stepText}>{_('Step 1: Open this URL in your browser to authorise the application:')}</Text>
|
||||
<View>
|
||||
<TouchableOpacity onPress={this.shared_.loginUrl_click}>
|
||||
<Text style={this.styles().urlText}>{this.state.loginUrl}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={this.styles().stepText}>{_('Step 2: Enter the code provided by Dropbox:')}</Text>
|
||||
<TextInput value={this.state.authCode} onChangeText={this.shared_.authCodeInput_change} style={theme.lineInput}/>
|
||||
|
||||
<Button disabled={this.state.checkingAuthToken} title={_("Submit")} onPress={this.shared_.submit_click}></Button>
|
||||
</View>
|
||||
|
||||
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const DropboxLoginScreen = connect((state) => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
};
|
||||
})(DropboxLoginScreenComponent)
|
||||
|
||||
module.exports = { DropboxLoginScreen };
|
@ -0,0 +1,73 @@
|
||||
const { shim } = require('lib/shim');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Setting = require('lib/models/Setting');
|
||||
|
||||
class Shared {
|
||||
|
||||
constructor(comp, showInfoMessageBox, showErrorMessageBox) {
|
||||
this.comp_ = comp;
|
||||
|
||||
this.dropboxApi_ = null;
|
||||
|
||||
this.comp_.state = {
|
||||
loginUrl: '',
|
||||
authCode: '',
|
||||
checkingAuthToken: false,
|
||||
};
|
||||
|
||||
this.loginUrl_click = () => {
|
||||
if (!this.comp_.state.loginUrl) return;
|
||||
shim.openUrl(this.comp_.state.loginUrl);
|
||||
}
|
||||
|
||||
this.authCodeInput_change = (event) => {
|
||||
this.comp_.setState({
|
||||
authCode: typeof event === 'object' ? event.target.value : event
|
||||
});
|
||||
}
|
||||
|
||||
this.submit_click = async () => {
|
||||
this.comp_.setState({ checkingAuthToken: true });
|
||||
|
||||
const api = await this.dropboxApi();
|
||||
try {
|
||||
const response = await api.execAuthToken(this.comp_.state.authCode);
|
||||
|
||||
Setting.setValue('sync.' + this.syncTargetId() + '.auth', response.access_token);
|
||||
api.setAuthToken(response.access_token);
|
||||
await showInfoMessageBox(_('The application has been authorised!'));
|
||||
this.comp_.props.dispatch({ type: 'NAV_BACK' });
|
||||
reg.scheduleSync();
|
||||
} catch (error) {
|
||||
await showErrorMessageBox(_('Could not authorise application:\n\n%s\n\nPlease try again.', error.message));
|
||||
} finally {
|
||||
this.comp_.setState({ checkingAuthToken: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncTargetId() {
|
||||
return SyncTargetRegistry.nameToId('dropbox');
|
||||
}
|
||||
|
||||
async dropboxApi() {
|
||||
if (this.dropboxApi_) return this.dropboxApi_;
|
||||
|
||||
const syncTarget = reg.syncTarget(this.syncTargetId());
|
||||
this.dropboxApi_ = await syncTarget.api();
|
||||
return this.dropboxApi_;
|
||||
}
|
||||
|
||||
async refreshUrl() {
|
||||
const api = await this.dropboxApi();
|
||||
|
||||
this.comp_.setState({
|
||||
loginUrl: api.loginUrl(),
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Shared;
|
@ -36,7 +36,7 @@ shared.synchronize_press = async function(comp) {
|
||||
|
||||
const action = comp.props.syncStarted ? 'cancel' : 'start';
|
||||
|
||||
if (!reg.syncTarget().isAuthenticated()) {
|
||||
if (!await reg.syncTarget().isAuthenticated()) {
|
||||
if (reg.syncTarget().authRouteName()) {
|
||||
comp.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
|
@ -67,6 +67,11 @@ dialogs.error = (parentComponent, message) => {
|
||||
return parentComponent.dialogbox.alert(message);
|
||||
}
|
||||
|
||||
dialogs.info = (parentComponent, message) => {
|
||||
Keyboard.dismiss();
|
||||
return parentComponent.dialogbox.alert(message);
|
||||
}
|
||||
|
||||
dialogs.DialogBox = DialogBox
|
||||
|
||||
module.exports = { dialogs };
|
209
ReactNativeClient/lib/file-api-driver-dropbox.js
Normal file
209
ReactNativeClient/lib/file-api-driver-dropbox.js
Normal file
@ -0,0 +1,209 @@
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { shim } = require('lib/shim');
|
||||
const JoplinError = require('lib/JoplinError');
|
||||
const { basicDelta } = require('lib/file-api');
|
||||
|
||||
class FileApiDriverDropbox {
|
||||
|
||||
constructor(api) {
|
||||
this.api_ = api;
|
||||
}
|
||||
|
||||
api() {
|
||||
return this.api_;
|
||||
}
|
||||
|
||||
requestRepeatCount() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
makePath_(path) {
|
||||
if (!path) return '';
|
||||
return '/' + path;
|
||||
}
|
||||
|
||||
async stat(path) {
|
||||
try {
|
||||
const metadata = await this.api().exec('POST', 'files/get_metadata', {
|
||||
path: this.makePath_(path),
|
||||
});
|
||||
|
||||
return this.metadataToStat_(metadata, path);
|
||||
} catch (error) {
|
||||
if (error.code.indexOf('not_found') >= 0) {
|
||||
// ignore
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metadataToStat_(md, path) {
|
||||
const output = {
|
||||
path: path,
|
||||
updated_time: md.server_modified ? new Date(md.server_modified) : new Date(),
|
||||
isDir: md['.tag'] === 'folder',
|
||||
};
|
||||
|
||||
if (md['.tag'] === 'deleted') output.isDeleted = true;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
metadataToStats_(mds) {
|
||||
const output = [];
|
||||
for (let i = 0; i < mds.length; i++) {
|
||||
output.push(this.metadataToStat_(mds[i], mds[i].name));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async setTimestamp(path, timestampMs) {
|
||||
throw new Error('Not implemented'); // Not needed anymore
|
||||
}
|
||||
|
||||
async delta(path, options) {
|
||||
const context = options ? options.context : null;
|
||||
let cursor = context ? context.cursor : null;
|
||||
|
||||
while (true) {
|
||||
const urlPath = cursor ? 'files/list_folder/continue' : 'files/list_folder';
|
||||
const body = cursor ? { cursor: cursor } : { path: this.makePath_(path), include_deleted: true };
|
||||
|
||||
try {
|
||||
const response = await this.api().exec('POST', urlPath, body);
|
||||
|
||||
const output = {
|
||||
items: this.metadataToStats_(response.entries),
|
||||
hasMore: response.has_more,
|
||||
context: { cursor: response.cursor },
|
||||
}
|
||||
|
||||
return output;
|
||||
} catch (error) {
|
||||
// If there's an error related to an invalid cursor, clear the cursor and retry.
|
||||
if (cursor) {
|
||||
if (error.httpStatus === 400 || error.code.indexOf('reset') >= 0) {
|
||||
// console.info('Clearing cursor and retrying', error);
|
||||
cursor = null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async list(path, options) {
|
||||
let response = await this.api().exec('POST', 'files/list_folder', {
|
||||
path: this.makePath_(path),
|
||||
});
|
||||
|
||||
let output = this.metadataToStats_(response.entries);
|
||||
|
||||
while (response.has_more) {
|
||||
response = await this.api().exec('POST', 'files/list_folder/continue', {
|
||||
cursor: response.cursor,
|
||||
});
|
||||
|
||||
output = output.concat(this.metadataToStats_(response.entries));
|
||||
}
|
||||
|
||||
return {
|
||||
items: output,
|
||||
hasMore: false,
|
||||
context: { cursor: response.cursor },
|
||||
};
|
||||
}
|
||||
|
||||
async get(path, options) {
|
||||
if (!options) options = {};
|
||||
if (!options.responseFormat) options.responseFormat = 'text';
|
||||
|
||||
try {
|
||||
const response = await this.api().exec('POST', 'files/download', null, {
|
||||
'Dropbox-API-Arg': JSON.stringify({ "path": this.makePath_(path) }),
|
||||
}, options);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error.code.indexOf('not_found') >= 0) {
|
||||
return null;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async mkdir(path) {
|
||||
try {
|
||||
await this.api().exec('POST', 'files/create_folder_v2', {
|
||||
path: this.makePath_(path),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code.indexOf('path/conflict') >= 0) {
|
||||
// Ignore
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async put(path, content, options = null) {
|
||||
// See https://github.com/facebook/react-native/issues/14445#issuecomment-352965210
|
||||
if (typeof content === 'string') content = shim.Buffer.from(content, 'utf8')
|
||||
|
||||
await this.api().exec('POST', 'files/upload', content, {
|
||||
'Dropbox-API-Arg': JSON.stringify({
|
||||
path: this.makePath_(path),
|
||||
mode: 'overwrite',
|
||||
mute: true, // Don't send a notification to user since there can be many of these updates
|
||||
})}, options);
|
||||
}
|
||||
|
||||
async delete(path) {
|
||||
try {
|
||||
await this.api().exec('POST', 'files/delete_v2', {
|
||||
path: this.makePath_(path),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code.indexOf('not_found') >= 0) {
|
||||
// ignore
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async move(oldPath, newPath) {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
format() {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
async clearRoot() {
|
||||
const entries = await this.list('');
|
||||
const batchDelete = [];
|
||||
for (let i = 0; i < entries.items.length; i++) {
|
||||
batchDelete.push({ path: this.makePath_(entries.items[i].path) });
|
||||
}
|
||||
|
||||
const response = await this.api().exec('POST', 'files/delete_batch', { entries: batchDelete });
|
||||
const jobId = response.async_job_id;
|
||||
|
||||
while (true) {
|
||||
const check = await this.api().exec('POST', 'files/delete_batch/check', { async_job_id: jobId });
|
||||
if (check['.tag'] === 'complete') break;
|
||||
|
||||
// It returns "failed" if it didn't work but anyway throw an error if it's anything other than complete or in_progress
|
||||
if (check['.tag'] !== 'in_progress') {
|
||||
throw new Error('Batch delete failed? ' + JSON.stringify(check));
|
||||
}
|
||||
await time.sleep(2);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = { FileApiDriverDropbox };
|
@ -293,6 +293,7 @@ class FileApiDriverWebDav {
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error.code !== 404) throw error;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ class Setting extends BaseModel {
|
||||
}},
|
||||
'noteVisiblePanes': { value: ['editor', 'viewer'], type: Setting.TYPE_ARRAY, public: false, appTypes: ['desktop'] },
|
||||
'showAdvancedOptions': { value: false, type: Setting.TYPE_BOOL, public: true, appTypes: ['mobile' ], label: () => _('Show advanced options') },
|
||||
'sync.target': { value: SyncTargetRegistry.nameToId('onedrive'), type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation target'), description: (appType) => { return appType !== 'cli' ? null : _('The target to synchonise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).') }, options: () => {
|
||||
'sync.target': { value: SyncTargetRegistry.nameToId('dropbox'), type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation target'), description: (appType) => { return appType !== 'cli' ? null : _('The target to synchonise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).') }, options: () => {
|
||||
return SyncTargetRegistry.idAndLabelPlainObject();
|
||||
}},
|
||||
|
||||
@ -121,12 +121,14 @@ class Setting extends BaseModel {
|
||||
|
||||
'sync.3.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.4.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.7.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.1.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.2.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.3.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.4.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.5.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.6.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.7.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
};
|
||||
|
||||
return this.metadata_;
|
||||
|
@ -11,6 +11,10 @@ parameters_.dev = {
|
||||
id: '606fd4d7-4dfb-4310-b8b7-a47d96aa22b6',
|
||||
secret: 'qabchuPYL7931$ePDEQ3~_$',
|
||||
},
|
||||
dropbox: {
|
||||
id: 'cx9li9ur8taq1z7',
|
||||
secret: 'i8f9a1mvx3bijrt',
|
||||
},
|
||||
};
|
||||
|
||||
parameters_.prod = {
|
||||
@ -22,6 +26,10 @@ parameters_.prod = {
|
||||
id: '606fd4d7-4dfb-4310-b8b7-a47d96aa22b6',
|
||||
secret: 'qabchuPYL7931$ePDEQ3~_$',
|
||||
},
|
||||
dropbox: {
|
||||
id: 'm044w3cvmxhzvop',
|
||||
secret: 'r298deqisz0od56',
|
||||
},
|
||||
};
|
||||
|
||||
function parameters(env = null) {
|
||||
|
@ -70,7 +70,7 @@ reg.scheduleSync = async (delay = null, syncOptions = null) => {
|
||||
|
||||
const syncTargetId = Setting.value('sync.target');
|
||||
|
||||
if (!reg.syncTarget(syncTargetId).isAuthenticated()) {
|
||||
if (!await reg.syncTarget(syncTargetId).isAuthenticated()) {
|
||||
reg.logger().info('Synchroniser is missing credentials - manual sync required to authenticate.');
|
||||
promiseResolve();
|
||||
return;
|
||||
|
@ -111,10 +111,9 @@ function shimInit() {
|
||||
const urlParse = require('url').parse;
|
||||
|
||||
url = urlParse(url.trim());
|
||||
const method = options.method ? options.method : 'GET';
|
||||
const http = url.protocol.toLowerCase() == 'http:' ? require('follow-redirects').http : require('follow-redirects').https;
|
||||
const headers = options.headers ? options.headers : {};
|
||||
const method = options.method ? options.method : 'GET';
|
||||
if (method != 'GET') throw new Error('Only GET is supported');
|
||||
const filePath = options.path;
|
||||
|
||||
function makeResponse(response) {
|
||||
@ -143,7 +142,7 @@ function shimInit() {
|
||||
// Note: relative paths aren't supported
|
||||
const file = fs.createWriteStream(filePath);
|
||||
|
||||
const request = http.get(requestOptions, function(response) {
|
||||
const request = http.request(requestOptions, function(response) {
|
||||
response.pipe(file);
|
||||
|
||||
file.on('finish', function() {
|
||||
@ -157,6 +156,8 @@ function shimInit() {
|
||||
fs.unlink(filePath);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
request.end();
|
||||
} catch(error) {
|
||||
fs.unlink(filePath);
|
||||
reject(error);
|
||||
@ -180,6 +181,13 @@ function shimInit() {
|
||||
return Buffer.byteLength(string, 'utf-8');
|
||||
}
|
||||
|
||||
shim.Buffer = Buffer;
|
||||
|
||||
shim.openUrl = (url) => {
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
bridge().openExternal(url)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = { shimInit };
|
@ -6,6 +6,7 @@ const { generateSecureRandom } = require('react-native-securerandom');
|
||||
const FsDriverRN = require('lib/fs-driver-rn.js').FsDriverRN;
|
||||
const urlValidator = require('valid-url');
|
||||
const { Buffer } = require('buffer');
|
||||
const { Linking } = require('react-native');
|
||||
|
||||
function shimInit() {
|
||||
shim.Geolocation = GeolocationReact;
|
||||
@ -116,6 +117,12 @@ function shimInit() {
|
||||
shim.stringByteLength = function(string) {
|
||||
return Buffer.byteLength(string, 'utf-8');
|
||||
}
|
||||
|
||||
shim.Buffer = Buffer;
|
||||
|
||||
shim.openUrl = (url) => {
|
||||
Linking.openURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { shimInit };
|
@ -85,6 +85,9 @@ shim.fetchRequestCanBeRetried = function(error) {
|
||||
// Code: ETIMEDOUT
|
||||
if (error.code === 'ETIMEDOUT') return true;
|
||||
|
||||
// ECONNREFUSED is generally temporary
|
||||
if (error.code === 'ECONNREFUSED') return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@ -129,5 +132,7 @@ shim.clearInterval = function(id) {
|
||||
shim.stringByteLength = function(string) { throw new Error('Not implemented'); }
|
||||
shim.detectAndSetLocale = null;
|
||||
shim.attachFileToNote = async (note, filePath) => {}
|
||||
shim.Buffer = null;
|
||||
shim.openUrl = () => { throw new Error('Not implemented'); }
|
||||
|
||||
module.exports = { shim };
|
@ -36,6 +36,7 @@ const { WelcomeScreen } = require('lib/components/screens/welcome.js');
|
||||
const { SearchScreen } = require('lib/components/screens/search.js');
|
||||
const { OneDriveLoginScreen } = require('lib/components/screens/onedrive-login.js');
|
||||
const { EncryptionConfigScreen } = require('lib/components/screens/encryption-config.js');
|
||||
const { DropboxLoginScreen } = require('lib/components/screens/dropbox-login.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { MenuContext } = require('react-native-popup-menu');
|
||||
const { SideMenu } = require('lib/components/side-menu.js');
|
||||
@ -55,10 +56,12 @@ const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
||||
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
|
||||
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
|
||||
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV.js');
|
||||
const SyncTargetDropbox = require('lib/SyncTargetDropbox.js');
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
|
||||
SyncTargetRegistry.addClass(SyncTargetNextcloud);
|
||||
SyncTargetRegistry.addClass(SyncTargetWebDAV);
|
||||
SyncTargetRegistry.addClass(SyncTargetDropbox);
|
||||
|
||||
// Disabled because not fully working
|
||||
//SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||
@ -365,16 +368,6 @@ async function initialize(dispatch) {
|
||||
await db.open({ name: 'joplin.sqlite' })
|
||||
} else {
|
||||
await db.open({ name: 'joplin-68.sqlite' })
|
||||
//await db.open({ name: 'joplin-67.sqlite' })
|
||||
|
||||
// await db.exec('DELETE FROM notes');
|
||||
// await db.exec('DELETE FROM folders');
|
||||
// await db.exec('DELETE FROM tags');
|
||||
// await db.exec('DELETE FROM note_tags');
|
||||
// await db.exec('DELETE FROM resources');
|
||||
// await db.exec('DELETE FROM deleted_items');
|
||||
|
||||
// await db.exec('UPDATE notes SET is_conflict = 1 where id like "546f%"');
|
||||
}
|
||||
|
||||
reg.logger().info('Database is ready.');
|
||||
@ -559,6 +552,7 @@ class AppComponent extends React.Component {
|
||||
Note: { screen: NoteScreen },
|
||||
Folder: { screen: FolderScreen },
|
||||
OneDriveLogin: { screen: OneDriveLoginScreen },
|
||||
DropboxLogin: { screen: DropboxLoginScreen },
|
||||
EncryptionConfig: { screen: EncryptionConfigScreen },
|
||||
Log: { screen: LogScreen },
|
||||
Status: { screen: StatusScreen },
|
||||
|
@ -226,7 +226,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Windows</td>
|
||||
<td>Windows (64-bit only)</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.0.79/Joplin-Setup-1.0.79.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -252,7 +252,7 @@
|
||||
<tr>
|
||||
<td>Android</td>
|
||||
<td><a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a></td>
|
||||
<td>or <a href="https://github.com/laurent22/joplin-android/releases/download/android-v1.0.112/joplin-v1.0.112.apk">Download APK File</a></td>
|
||||
<td>or <a href="https://github.com/laurent22/joplin-android/releases/download/android-v1.0.114/joplin-v1.0.114.apk">Download APK File</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>iOS</td>
|
||||
@ -416,14 +416,14 @@ $$
|
||||
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/hr.png" alt=""></td>
|
||||
<td>Croatian</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po">hr_HR</a></td>
|
||||
<td>Hrvoje Mandić <a href="mailto:trbuhom@net.hr">trbuhom@net.hr</a></td>
|
||||
<td>Hrvoje Mandić <a href="mailto:trbuhom@net.hr">trbuhom@net.hr</a></td>
|
||||
<td>64%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cz.png" alt=""></td>
|
||||
<td>Czech</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/cs_CZ.po">cs_CZ</a></td>
|
||||
<td>Lukas Helebrandt <a href="mailto:lukas@aiya.cz">lukas@aiya.cz</a></td>
|
||||
<td>Lukas Helebrandt <a href="mailto:lukas@aiya.cz">lukas@aiya.cz</a></td>
|
||||
<td>99%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -437,7 +437,7 @@ $$
|
||||
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/de.png" alt=""></td>
|
||||
<td>Deutsch</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po">de_DE</a></td>
|
||||
<td>Tobias Grasse <a href="mailto:mail@tobias-grasse.net">mail@tobias-grasse.net</a></td>
|
||||
<td>Tobias Grasse <a href="mailto:mail@tobias-grasse.net">mail@tobias-grasse.net</a></td>
|
||||
<td>98%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -451,7 +451,7 @@ $$
|
||||
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/es.png" alt=""></td>
|
||||
<td>Español</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po">es_ES</a></td>
|
||||
<td>Fernando Martín <a href="mailto:f@mrtn.es">f@mrtn.es</a></td>
|
||||
<td>Fernando Martín <a href="mailto:f@mrtn.es">f@mrtn.es</a></td>
|
||||
<td>98%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -479,21 +479,21 @@ $$
|
||||
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/br.png" alt=""></td>
|
||||
<td>Português (Brasil)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po">pt_BR</a></td>
|
||||
<td>Renato Nunes Bastos <a href="mailto:rnbastos@gmail.com">rnbastos@gmail.com</a></td>
|
||||
<td>Renato Nunes Bastos <a href="mailto:rnbastos@gmail.com">rnbastos@gmail.com</a></td>
|
||||
<td>97%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/ru.png" alt=""></td>
|
||||
<td>Русский</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po">ru_RU</a></td>
|
||||
<td>Artyom Karlov <a href="mailto:artyom.karlov@gmail.com">artyom.karlov@gmail.com</a></td>
|
||||
<td>Artyom Karlov <a href="mailto:artyom.karlov@gmail.com">artyom.karlov@gmail.com</a></td>
|
||||
<td>98%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cn.png" alt=""></td>
|
||||
<td>中文 (简体)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po">zh_CN</a></td>
|
||||
<td>RCJacH <a href="mailto:RCJacH@outlook.com">RCJacH@outlook.com</a></td>
|
||||
<td>RCJacH <a href="mailto:RCJacH@outlook.com">RCJacH@outlook.com</a></td>
|
||||
<td>66%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
Loading…
Reference in New Issue
Block a user