const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); const { OneDriveApiNodeUtils } = require('./onedrive-api-node-utils.js'); const { Setting } = require('lib/models/setting.js'); const { BaseItem } = require('lib/models/base-item.js'); const { Synchronizer } = require('lib/synchronizer.js'); const { reg } = require('lib/registry.js'); const { cliUtils } = require('./cli-utils.js'); const md5 = require('md5'); const locker = require('proper-lockfile'); const fs = require('fs-extra'); const osTmpdir = require('os-tmpdir'); class Command extends BaseCommand { constructor() { super(); this.syncTargetId_ = null; this.releaseLockFn_ = null; this.oneDriveApiUtils_ = null; } usage() { return 'sync'; } description() { return _('Synchronises with remote storage.'); } options() { return [ ['--target ', _('Sync to provided target (defaults to sync.target config value)')], ['--random-failures', 'For debugging purposes. Do not use.'], ]; } static lockFile(filePath) { return new Promise((resolve, reject) => { locker.lock(filePath, { stale: 1000 * 60 * 5 }, (error, release) => { if (error) { reject(error); return; } resolve(release); }); }); } static isLocked(filePath) { return new Promise((resolve, reject) => { locker.check(filePath, (error, isLocked) => { if (error) { reject(error); return; } resolve(isLocked); }); }); } async doAuth(syncTargetId) { const syncTarget = reg.syncTarget(this.syncTargetId_); this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.oneDriveApi()); const auth = await this.oneDriveApiUtils_.oauthDance({ log: (...s) => { return this.stdout(...s); } }); this.oneDriveApiUtils_ = null; return auth; } cancelAuth() { if (this.oneDriveApiUtils_) { this.oneDriveApiUtils_.cancelOAuthDance(); return; } } doingAuth() { return !!this.oneDriveApiUtils_; } async action(args) { this.releaseLockFn_ = null; // Lock is unique per profile/database const lockFilePath = osTmpdir() + '/synclock_' + md5(Setting.value('profileDir')); if (!await fs.pathExists(lockFilePath)) await fs.writeFile(lockFilePath, 'synclock'); 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 (syncTarget.isAuthenticated()) { app().gui().showConsole(); app().gui().maximizeConsole(); const auth = await this.doAuth(this.syncTargetId_); 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 cleanUp(); } } const sync = await syncTarget.synchronizer(); let options = { onProgress: (report) => { let lines = Synchronizer.reportToLines(report); if (lines.length) cliUtils.redraw(lines.join(' ')); }, onMessage: (msg) => { cliUtils.redrawDone(); this.stdout(msg); }, randomFailures: args.options['random-failures'] === true, }; this.stdout(_('Synchronisation target: %s (%s)', Setting.enumOptionLabel('sync.target', this.syncTargetId_), this.syncTargetId_)); if (!sync) throw new Error(_('Cannot initialize synchroniser.')); this.stdout(_('Starting synchronisation...')); const contextKey = 'sync.' + this.syncTargetId_ + '.context'; let context = Setting.value(contextKey); context = context ? JSON.parse(context) : {}; options.context = context; try { let newContext = await sync.start(options); Setting.setValue(contextKey, JSON.stringify(newContext)); } catch (error) { if (error.code == 'alreadyStarted') { this.stdout(error.message); } else { throw error; } } await app().refreshCurrentFolder(); } catch (error) { cleanUp(); throw error; } cleanUp(); } 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 (syncTarget.isAuthenticated()) { const sync = await syncTarget.synchronizer(); if (sync) await sync.cancel(); } else { if (this.releaseLockFn_) this.releaseLockFn_(); this.releaseLockFn_ = null; } this.syncTargetId_ = null; } cancellable() { return true; } } module.exports = Command;