mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
310 lines
10 KiB
TypeScript
310 lines
10 KiB
TypeScript
import { _ } from '@joplin/lib/locale';
|
|
import Setting from '@joplin/lib/models/Setting';
|
|
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
|
import MigrationHandler from '@joplin/lib/services/synchronizer/MigrationHandler';
|
|
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
|
|
import Synchronizer from '@joplin/lib/Synchronizer';
|
|
import { masterKeysWithoutPassword } from '@joplin/lib/services/e2ee/utils';
|
|
import { appTypeToLockType } from '@joplin/lib/services/synchronizer/LockHandler';
|
|
const BaseCommand = require('./base-command').default;
|
|
import app from './app';
|
|
const { OneDriveApiNodeUtils } = require('@joplin/lib/onedrive-api-node-utils.js');
|
|
import { reg } from '@joplin/lib/registry';
|
|
const { cliUtils } = require('./cli-utils.js');
|
|
const md5 = require('md5');
|
|
import * as locker from 'proper-lockfile';
|
|
import { pathExists, writeFile } from 'fs-extra';
|
|
import { checkIfLoginWasSuccessful, generateApplicationConfirmUrl } from '@joplin/lib/services/joplinCloudUtils';
|
|
import Logger from '@joplin/utils/Logger';
|
|
import { uuidgen } from '@joplin/lib/uuid';
|
|
|
|
const logger = Logger.create('command-sync');
|
|
|
|
class Command extends BaseCommand {
|
|
|
|
private syncTargetId_: number = null;
|
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
|
private releaseLockFn_: Function = null;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
private oneDriveApiUtils_: any = null;
|
|
|
|
public usage() {
|
|
return 'sync';
|
|
}
|
|
|
|
public description() {
|
|
return _('Synchronises with remote storage.');
|
|
}
|
|
|
|
public options() {
|
|
return [
|
|
['--target <target>', _('Sync to provided target (defaults to sync.target config value)')],
|
|
['--upgrade', _('Upgrade the sync target to the latest version.')],
|
|
['--use-lock <value>', 'Disable local locks that prevent multiple clients from synchronizing at the same time (Default = 1)'],
|
|
];
|
|
}
|
|
|
|
private static async lockFile(filePath: string) {
|
|
return locker.lock(filePath, { stale: 1000 * 60 * 5 });
|
|
}
|
|
|
|
private static async isLocked(filePath: string) {
|
|
return locker.check(filePath);
|
|
}
|
|
|
|
public async doAuth() {
|
|
const syncTarget = reg.syncTarget(this.syncTargetId_);
|
|
const syncTargetMd = SyncTargetRegistry.idToMetadata(this.syncTargetId_);
|
|
|
|
if (this.syncTargetId_ === 3 || this.syncTargetId_ === 4) {
|
|
// OneDrive
|
|
this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.api());
|
|
const auth = await this.oneDriveApiUtils_.oauthDance({
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
log: (...s: any[]) => {
|
|
return this.stdout(...s);
|
|
},
|
|
});
|
|
this.oneDriveApiUtils_ = null;
|
|
|
|
Setting.setValue(`sync.${this.syncTargetId_}.auth`, auth ? JSON.stringify(auth) : null);
|
|
if (!auth) {
|
|
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
|
|
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;
|
|
} else if (syncTargetMd.name === 'joplinCloud') {
|
|
const applicationAuthId = uuidgen();
|
|
const checkForCredentials = async () => {
|
|
try {
|
|
const applicationAuthUrl = `${Setting.value('sync.10.path')}/api/application_auth/${applicationAuthId}`;
|
|
const response = await checkIfLoginWasSuccessful(applicationAuthUrl);
|
|
if (response && response.success) {
|
|
return response;
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
logger.error(error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
this.stdout(_('To allow Joplin to synchronise with Joplin Cloud, please login using this URL:'));
|
|
|
|
const confirmUrl = `${Setting.value('sync.10.website')}/applications/${applicationAuthId}/confirm`;
|
|
const urlWithClient = await generateApplicationConfirmUrl(confirmUrl);
|
|
this.stdout(urlWithClient);
|
|
|
|
const authorized = await this.prompt(_('Have you authorised the application login in the above URL?'), { booleanAnswerDefault: 'y' });
|
|
if (!authorized) return false;
|
|
const result = await checkForCredentials();
|
|
if (!result) return false;
|
|
return true;
|
|
}
|
|
|
|
this.stdout(_('Not authenticated with %s. Please provide any missing credentials.', syncTargetMd.label));
|
|
return false;
|
|
}
|
|
|
|
public cancelAuth() {
|
|
if (this.oneDriveApiUtils_) {
|
|
this.oneDriveApiUtils_.cancelOAuthDance();
|
|
return;
|
|
}
|
|
}
|
|
|
|
public doingAuth() {
|
|
return !!this.oneDriveApiUtils_;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
public async action(args: any) {
|
|
this.releaseLockFn_ = null;
|
|
|
|
// Lock is unique per profile/database
|
|
const lockFilePath = `${require('os').tmpdir()}/synclock_${md5(escape(Setting.value('profileDir')))}`; // https://github.com/pvorb/node-md5/issues/41
|
|
if (!(await pathExists(lockFilePath))) await writeFile(lockFilePath, 'synclock');
|
|
|
|
const useLock = args.options.useLock !== 0;
|
|
|
|
if (useLock) {
|
|
try {
|
|
if (await Command.isLocked(lockFilePath)) throw new Error(_('Synchronisation is already in progress.'));
|
|
|
|
this.releaseLockFn_ = await Command.lockFile(lockFilePath);
|
|
} catch (error) {
|
|
if (error.code === 'ELOCKED') {
|
|
const msg = _('Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at "%s" and resume the operation.', error.file);
|
|
this.stdout(msg);
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const cleanUp = () => {
|
|
cliUtils.redrawDone();
|
|
if (this.releaseLockFn_) {
|
|
this.releaseLockFn_();
|
|
this.releaseLockFn_ = null;
|
|
}
|
|
};
|
|
|
|
try {
|
|
this.syncTargetId_ = Setting.value('sync.target');
|
|
if (args.options.target) this.syncTargetId_ = args.options.target;
|
|
|
|
const syncTarget = reg.syncTarget(this.syncTargetId_);
|
|
|
|
if (!(await syncTarget.isAuthenticated())) {
|
|
app().gui().showConsole();
|
|
app().gui().maximizeConsole();
|
|
|
|
const authDone = await this.doAuth();
|
|
if (!authDone) return cleanUp();
|
|
}
|
|
|
|
const sync = await syncTarget.synchronizer();
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
const options: any = {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
onProgress: (report: any) => {
|
|
const lines = Synchronizer.reportToLines(report);
|
|
if (lines.length) cliUtils.redraw(lines.join(' '));
|
|
},
|
|
onMessage: (msg: string) => {
|
|
cliUtils.redrawDone();
|
|
this.stdout(msg);
|
|
},
|
|
};
|
|
|
|
this.stdout(_('Synchronisation target: %s (%s)', Setting.enumOptionLabel('sync.target', this.syncTargetId_), this.syncTargetId_));
|
|
|
|
if (!sync) throw new Error(_('Cannot initialise synchroniser.'));
|
|
|
|
if (args.options.upgrade) {
|
|
let migrationError = null;
|
|
|
|
try {
|
|
const migrationHandler = new MigrationHandler(
|
|
sync.api(),
|
|
reg.db(),
|
|
sync.lockHandler(),
|
|
appTypeToLockType(Setting.value('appType')),
|
|
Setting.value('clientId'),
|
|
);
|
|
|
|
migrationHandler.setLogger(cliUtils.stdoutLogger(this.stdout.bind(this)));
|
|
|
|
await migrationHandler.upgrade();
|
|
} catch (error) {
|
|
migrationError = error;
|
|
}
|
|
|
|
if (!migrationError) {
|
|
Setting.setValue('sync.upgradeState', Setting.SYNC_UPGRADE_STATE_IDLE);
|
|
await Setting.saveAll();
|
|
}
|
|
|
|
if (migrationError) throw migrationError;
|
|
|
|
return cleanUp();
|
|
}
|
|
|
|
this.stdout(_('Starting synchronisation...'));
|
|
|
|
const contextKey = `sync.${this.syncTargetId_}.context`;
|
|
let context = Setting.value(contextKey);
|
|
|
|
context = context ? JSON.parse(context) : {};
|
|
options.context = context;
|
|
|
|
try {
|
|
const newContext = await sync.start(options);
|
|
Setting.setValue(contextKey, JSON.stringify(newContext));
|
|
} catch (error) {
|
|
if (error.code === 'alreadyStarted') {
|
|
this.stdout(error.message);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// When using the tool in command line mode, the ResourceFetcher service is
|
|
// not going to be running in the background, so the resources need to be
|
|
// explicitly downloaded below.
|
|
if (!app().hasGui()) {
|
|
this.stdout(_('Downloading resources...'));
|
|
await ResourceFetcher.instance().fetchAll();
|
|
await ResourceFetcher.instance().waitForAllFinished();
|
|
}
|
|
|
|
const noPasswordMkIds = await masterKeysWithoutPassword();
|
|
if (noPasswordMkIds.length) this.stdout(`/!\\ ${_('Your password is needed to decrypt some of your data. Type `:e2ee decrypt` to set it.')}`);
|
|
|
|
await app().refreshCurrentFolder();
|
|
} catch (error) {
|
|
cleanUp();
|
|
throw error;
|
|
}
|
|
|
|
if (Setting.value('sync.upgradeState') > Setting.SYNC_UPGRADE_STATE_IDLE) {
|
|
this.stdout(`/!\\ ${_('Sync target must be upgraded! Run `%s` to proceed.', 'sync --upgrade')}`);
|
|
app().gui().showConsole();
|
|
app().gui().maximizeConsole();
|
|
}
|
|
|
|
cleanUp();
|
|
}
|
|
|
|
public async cancel() {
|
|
if (this.doingAuth()) {
|
|
this.cancelAuth();
|
|
return;
|
|
}
|
|
|
|
const syncTargetId = this.syncTargetId_ ? this.syncTargetId_ : Setting.value('sync.target');
|
|
|
|
cliUtils.redrawDone();
|
|
|
|
this.stdout(_('Cancelling... Please wait.'));
|
|
|
|
const syncTarget = reg.syncTarget(syncTargetId);
|
|
|
|
if (await syncTarget.isAuthenticated()) {
|
|
const sync = await syncTarget.synchronizer();
|
|
if (sync) await sync.cancel();
|
|
} else {
|
|
if (this.releaseLockFn_) this.releaseLockFn_();
|
|
this.releaseLockFn_ = null;
|
|
}
|
|
|
|
this.syncTargetId_ = null;
|
|
}
|
|
|
|
public cancellable() {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
module.exports = Command;
|