Skip to content

Commit

Permalink
Desktop: Add Joplin Cloud sync target
Browse files Browse the repository at this point in the history
  • Loading branch information
laurent22 committed Jun 3, 2021
1 parent 770af6a commit 21ea325
Show file tree
Hide file tree
Showing 11 changed files with 151 additions and 33 deletions.
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,9 @@ packages/lib/Logger.js.map
packages/lib/PoorManIntervals.d.ts
packages/lib/PoorManIntervals.js
packages/lib/PoorManIntervals.js.map
packages/lib/SyncTargetJoplinCloud.d.ts
packages/lib/SyncTargetJoplinCloud.js
packages/lib/SyncTargetJoplinCloud.js.map
packages/lib/SyncTargetJoplinServer.d.ts
packages/lib/SyncTargetJoplinServer.js
packages/lib/SyncTargetJoplinServer.js.map
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,9 @@ packages/lib/Logger.js.map
packages/lib/PoorManIntervals.d.ts
packages/lib/PoorManIntervals.js
packages/lib/PoorManIntervals.js.map
packages/lib/SyncTargetJoplinCloud.d.ts
packages/lib/SyncTargetJoplinCloud.js
packages/lib/SyncTargetJoplinCloud.js.map
packages/lib/SyncTargetJoplinServer.d.ts
packages/lib/SyncTargetJoplinServer.js
packages/lib/SyncTargetJoplinServer.js.map
Expand Down
2 changes: 2 additions & 0 deletions packages/app-mobile/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { loadKeychainServiceAndSettings } from '@joplin/lib/services/SettingUtil
import KeychainServiceDriverMobile from '@joplin/lib/services/keychain/KeychainServiceDriver.mobile';
import { setLocale, closestSupportedLocale, defaultLocale } from '@joplin/lib/locale';
import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer';
import SyncTargetJoplinCloud from '@joplin/lib/SyncTargetJoplinCloud';
import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';

const { AppState, Keyboard, NativeModules, BackHandler, Animated, View, StatusBar, Linking, Platform } = require('react-native');
Expand Down Expand Up @@ -90,6 +91,7 @@ SyncTargetRegistry.addClass(SyncTargetDropbox);
SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetAmazonS3);
SyncTargetRegistry.addClass(SyncTargetJoplinServer);
SyncTargetRegistry.addClass(SyncTargetJoplinCloud);

import FsDriverRN from './utils/fs-driver-rn';
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
Expand Down
8 changes: 7 additions & 1 deletion packages/lib/BaseApplication.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Setting from './models/Setting';
import Setting, { Env } from './models/Setting';
import Logger, { TargetType, LoggerWrapper } from './Logger';
import shim from './shim';
import BaseService from './services/BaseService';
Expand Down Expand Up @@ -46,6 +46,7 @@ const { loadKeychainServiceAndSettings } = require('./services/SettingUtils');
import MigrationService from './services/MigrationService';
import ShareService from './services/share/ShareService';
import handleSyncStartupOperation from './services/synchronizer/utils/handleSyncStartupOperation';
import SyncTargetJoplinCloud from './SyncTargetJoplinCloud';
const { toSystemSlashes } = require('./path-utils');
const { setAutoFreeze } = require('immer');

Expand Down Expand Up @@ -691,6 +692,7 @@ export default class BaseApplication {
SyncTargetRegistry.addClass(SyncTargetDropbox);
SyncTargetRegistry.addClass(SyncTargetAmazonS3);
SyncTargetRegistry.addClass(SyncTargetJoplinServer);
SyncTargetRegistry.addClass(SyncTargetJoplinCloud);

try {
await shim.fsDriver().remove(tempDir);
Expand Down Expand Up @@ -763,6 +765,10 @@ export default class BaseApplication {
setLocale(Setting.value('locale'));
}

if (Setting.value('env') === Env.Dev) {
Setting.setValue('sync.10.path', 'http://api-joplincloud.local:22300');
}

// For now always disable fuzzy search due to performance issues:
// https://discourse.joplinapp.org/t/1-1-4-keyboard-locks-up-while-typing/11231/11
// https://discourse.joplinapp.org/t/serious-lagging-when-there-are-tens-of-thousands-of-notes/11215/23
Expand Down
15 changes: 10 additions & 5 deletions packages/lib/JoplinServerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,17 @@ export default class JoplinServerApi {
private async session() {
if (this.session_) return this.session_;

this.session_ = await this.exec('POST', 'api/sessions', null, {
email: this.options_.username(),
password: this.options_.password(),
});
try {
this.session_ = await this.exec('POST', 'api/sessions', null, {
email: this.options_.username(),
password: this.options_.password(),
});

return this.session_;
return this.session_;
} catch (error) {
logger.error('Could not acquire session:', error);
throw error;
}
}

private async sessionId() {
Expand Down
57 changes: 57 additions & 0 deletions packages/lib/SyncTargetJoplinCloud.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Setting from './models/Setting';
import Synchronizer from './Synchronizer';
import { _ } from './locale.js';
import BaseSyncTarget from './BaseSyncTarget';
import { FileApi } from './file-api';
import SyncTargetJoplinServer, { initFileApi } from './SyncTargetJoplinServer';

interface FileApiOptions {
path(): string;
username(): string;
password(): string;
}

export default class SyncTargetJoplinCloud extends BaseSyncTarget {

public static id() {
return 10;
}

public static supportsConfigCheck() {
return SyncTargetJoplinServer.supportsConfigCheck();
}

public static targetName() {
return 'joplinCloud';
}

public static label() {
return _('Joplin Cloud');
}

public async isAuthenticated() {
return true;
}

public async fileApi(): Promise<FileApi> {
return super.fileApi();
}

public static async checkConfig(options: FileApiOptions) {
return SyncTargetJoplinServer.checkConfig({
...options,
});
}

protected async initFileApi() {
return initFileApi(this.logger(), {
path: () => Setting.value('sync.10.path'),
username: () => Setting.value('sync.10.username'),
password: () => Setting.value('sync.10.password'),
});
}

protected async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
47 changes: 25 additions & 22 deletions packages/lib/SyncTargetJoplinServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,36 @@ import { _ } from './locale.js';
import JoplinServerApi from './JoplinServerApi';
import BaseSyncTarget from './BaseSyncTarget';
import { FileApi } from './file-api';
import Logger from './Logger';

interface FileApiOptions {
path(): string;
username(): string;
password(): string;
}

export async function newFileApi(id: number, options: FileApiOptions) {
const apiOptions = {
baseUrl: () => options.path(),
username: () => options.username(),
password: () => options.password(),
env: Setting.value('env'),
};

const api = new JoplinServerApi(apiOptions);
const driver = new FileApiDriverJoplinServer(api);
const fileApi = new FileApi('', driver);
fileApi.setSyncTargetId(id);
await fileApi.initialize();
return fileApi;
}

export async function initFileApi(logger: Logger, options: FileApiOptions) {
const fileApi = await newFileApi(SyncTargetJoplinServer.id(), options);
fileApi.setLogger(logger);
return fileApi;
}

export default class SyncTargetJoplinServer extends BaseSyncTarget {

public static id() {
Expand All @@ -38,30 +61,14 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
return super.fileApi();
}

private static async newFileApi_(options: FileApiOptions) {
const apiOptions = {
baseUrl: () => options.path(),
username: () => options.username(),
password: () => options.password(),
env: Setting.value('env'),
};

const api = new JoplinServerApi(apiOptions);
const driver = new FileApiDriverJoplinServer(api);
const fileApi = new FileApi('', driver);
fileApi.setSyncTargetId(this.id());
await fileApi.initialize();
return fileApi;
}

public static async checkConfig(options: FileApiOptions) {
const output = {
ok: false,
errorMessage: '',
};

try {
const fileApi = await SyncTargetJoplinServer.newFileApi_(options);
const fileApi = await newFileApi(SyncTargetJoplinServer.id(), options);
fileApi.requestRepeatCount_ = 0;

await fileApi.put('testing.txt', 'testing');
Expand All @@ -78,15 +85,11 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
}

protected async initFileApi() {
const fileApi = await SyncTargetJoplinServer.newFileApi_({
return initFileApi(this.logger(), {
path: () => Setting.value('sync.9.path'),
username: () => Setting.value('sync.9.username'),
password: () => Setting.value('sync.9.password'),
});

fileApi.setLogger(this.logger());

return fileApi;
}

protected async initSynchronizer() {
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/SyncTargetRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class SyncTargetRegistry {
if (!this.reg_.hasOwnProperty(n)) continue;
if (this.reg_[n].name === name) return this.reg_[n].id;
}
throw new Error(`Name not found: ${name}`);
throw new Error(`Name not found: ${name}. Was the sync target registered?`);
}

static idToMetadata(id) {
Expand Down
35 changes: 35 additions & 0 deletions packages/lib/models/Setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,39 @@ class Setting extends BaseModel {
secure: true,
},

// Although sync.10.path is essentially a constant, we still define
// it here so that both Joplin Server and Joplin Cloud can be
// handled in the same consistent way. Also having it a setting
// means it can be set to something else for development.
'sync.10.path': {
value: 'https://api.joplincloud.com',
type: SettingItemType.String,
public: false,
storage: SettingStorage.Database,
},
'sync.10.username': {
value: '',
type: SettingItemType.String,
section: 'sync',
show: (settings: any) => {
return settings['sync.target'] == SyncTargetRegistry.nameToId('joplinCloud');
},
public: true,
label: () => _('Joplin Cloud email'),
storage: SettingStorage.File,
},
'sync.10.password': {
value: '',
type: SettingItemType.String,
section: 'sync',
show: (settings: any) => {
return settings['sync.target'] == SyncTargetRegistry.nameToId('joplinCloud');
},
public: true,
label: () => _('Joplin Cloud password'),
secure: true,
},

'sync.5.syncTargets': { value: {}, type: SettingItemType.Object, public: false },

'sync.resourceDownloadMode': {
Expand All @@ -525,6 +558,7 @@ class Setting extends BaseModel {
'sync.4.auth': { value: '', type: SettingItemType.String, public: false },
'sync.7.auth': { value: '', type: SettingItemType.String, public: false },
'sync.9.auth': { value: '', type: SettingItemType.String, public: false },
'sync.10.auth': { value: '', type: SettingItemType.String, public: false },
'sync.1.context': { value: '', type: SettingItemType.String, public: false },
'sync.2.context': { value: '', type: SettingItemType.String, public: false },
'sync.3.context': { value: '', type: SettingItemType.String, public: false },
Expand All @@ -534,6 +568,7 @@ class Setting extends BaseModel {
'sync.7.context': { value: '', type: SettingItemType.String, public: false },
'sync.8.context': { value: '', type: SettingItemType.String, public: false },
'sync.9.context': { value: '', type: SettingItemType.String, public: false },
'sync.10.context': { value: '', type: SettingItemType.String, public: false },

'sync.maxConcurrentConnections': { value: 5, type: SettingItemType.Int, storage: SettingStorage.File, public: true, advanced: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },

Expand Down
10 changes: 6 additions & 4 deletions packages/lib/services/share/ShareService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default class ShareService {
}

public get enabled(): boolean {
return Setting.value('sync.target') === 9; // Joplin Server target
return [9, 10].includes(Setting.value('sync.target')); // Joplin Server, Joplin Cloud targets
}

private get store(): Store<any> {
Expand All @@ -36,10 +36,12 @@ export default class ShareService {
private api(): JoplinServerApi {
if (this.api_) return this.api_;

const syncTargetId = Setting.value('sync.target');

this.api_ = new JoplinServerApi({
baseUrl: () => Setting.value('sync.9.path'),
username: () => Setting.value('sync.9.username'),
password: () => Setting.value('sync.9.password'),
baseUrl: () => Setting.value(`sync.${syncTargetId}.path`),
username: () => Setting.value(`sync.${syncTargetId}.username`),
password: () => Setting.value(`sync.${syncTargetId}.password`),
});

return this.api_;
Expand Down
2 changes: 2 additions & 0 deletions packages/lib/testing/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const DropboxApi = require('../DropboxApi');
import JoplinServerApi from '../JoplinServerApi';
import { FolderEntity } from '../services/database/types';
import { credentialFile } from '../utils/credentialFiles';
import SyncTargetJoplinCloud from '../SyncTargetJoplinCloud';
const { loadKeychainServiceAndSettings } = require('../services/SettingUtils');
const md5 = require('md5');
const S3 = require('aws-sdk/clients/s3');
Expand Down Expand Up @@ -112,6 +113,7 @@ SyncTargetRegistry.addClass(SyncTargetNextcloud);
SyncTargetRegistry.addClass(SyncTargetDropbox);
SyncTargetRegistry.addClass(SyncTargetAmazonS3);
SyncTargetRegistry.addClass(SyncTargetJoplinServer);
SyncTargetRegistry.addClass(SyncTargetJoplinCloud);

let syncTargetName_ = '';
let syncTargetId_: number = null;
Expand Down

3 comments on commit 21ea325

@tessus
Copy link
Collaborator

@tessus tessus commented on 21ea325 Jun 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference between Joplin Server and Joplin Cloud?

@laurent22
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Joplin Server is the server that can be hosted anywhere, and Joplin Cloud is the service that will be a hosted on a specific domain.

@tessus
Copy link
Collaborator

@tessus tessus commented on 21ea325 Jun 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the info.

Please sign in to comment.