From 21ea3253dbd9be2ae661d70061eab96e48853041 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Thu, 3 Jun 2021 17:12:07 +0200 Subject: [PATCH] Desktop: Add Joplin Cloud sync target --- .eslintignore | 3 ++ .gitignore | 3 ++ packages/app-mobile/root.tsx | 2 + packages/lib/BaseApplication.ts | 8 ++- packages/lib/JoplinServerApi.ts | 15 ++++-- packages/lib/SyncTargetJoplinCloud.ts | 57 +++++++++++++++++++++ packages/lib/SyncTargetJoplinServer.ts | 47 +++++++++-------- packages/lib/SyncTargetRegistry.js | 2 +- packages/lib/models/Setting.ts | 35 +++++++++++++ packages/lib/services/share/ShareService.ts | 10 ++-- packages/lib/testing/test-utils.ts | 2 + 11 files changed, 151 insertions(+), 33 deletions(-) create mode 100644 packages/lib/SyncTargetJoplinCloud.ts diff --git a/.eslintignore b/.eslintignore index 8c0b8fc62c..d683bb7ecc 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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 diff --git a/.gitignore b/.gitignore index 318a2be559..f74547c0a8 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx index 493574d52d..bbfe230eb3 100644 --- a/packages/app-mobile/root.tsx +++ b/packages/app-mobile/root.tsx @@ -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'); @@ -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'; diff --git a/packages/lib/BaseApplication.ts b/packages/lib/BaseApplication.ts index 586ea3bf03..c63768bb08 100644 --- a/packages/lib/BaseApplication.ts +++ b/packages/lib/BaseApplication.ts @@ -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'; @@ -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'); @@ -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); @@ -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 diff --git a/packages/lib/JoplinServerApi.ts b/packages/lib/JoplinServerApi.ts index 7cd099ba44..8a5a08488c 100644 --- a/packages/lib/JoplinServerApi.ts +++ b/packages/lib/JoplinServerApi.ts @@ -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() { diff --git a/packages/lib/SyncTargetJoplinCloud.ts b/packages/lib/SyncTargetJoplinCloud.ts new file mode 100644 index 0000000000..500627e5ea --- /dev/null +++ b/packages/lib/SyncTargetJoplinCloud.ts @@ -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 { + 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')); + } +} diff --git a/packages/lib/SyncTargetJoplinServer.ts b/packages/lib/SyncTargetJoplinServer.ts index 87b234dc40..b1792a8393 100644 --- a/packages/lib/SyncTargetJoplinServer.ts +++ b/packages/lib/SyncTargetJoplinServer.ts @@ -5,6 +5,7 @@ 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; @@ -12,6 +13,28 @@ interface FileApiOptions { 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() { @@ -38,22 +61,6 @@ 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, @@ -61,7 +68,7 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget { }; try { - const fileApi = await SyncTargetJoplinServer.newFileApi_(options); + const fileApi = await newFileApi(SyncTargetJoplinServer.id(), options); fileApi.requestRepeatCount_ = 0; await fileApi.put('testing.txt', 'testing'); @@ -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() { diff --git a/packages/lib/SyncTargetRegistry.js b/packages/lib/SyncTargetRegistry.js index b752698a18..21add8d7c9 100644 --- a/packages/lib/SyncTargetRegistry.js +++ b/packages/lib/SyncTargetRegistry.js @@ -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) { diff --git a/packages/lib/models/Setting.ts b/packages/lib/models/Setting.ts index 7eac22af7b..90a72ebecd 100644 --- a/packages/lib/models/Setting.ts +++ b/packages/lib/models/Setting.ts @@ -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': { @@ -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 }, @@ -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 }, diff --git a/packages/lib/services/share/ShareService.ts b/packages/lib/services/share/ShareService.ts index 0d4fe2a1a1..c8def1a765 100644 --- a/packages/lib/services/share/ShareService.ts +++ b/packages/lib/services/share/ShareService.ts @@ -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 { @@ -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_; diff --git a/packages/lib/testing/test-utils.ts b/packages/lib/testing/test-utils.ts index e8a82a8eaf..ff2f5ac14f 100644 --- a/packages/lib/testing/test-utils.ts +++ b/packages/lib/testing/test-utils.ts @@ -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'); @@ -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;