UNPKG

@mieweb/wikigdrive

Version:

Google Drive to MarkDown synchronization

874 lines (873 loc) 39.5 kB
import os from 'node:os'; import fs from 'node:fs'; import path from 'node:path'; import Sandbox from '@nyariv/sandboxjs'; import { Container } from '../../ContainerEngine.js'; import { GoogleFolderContainer } from '../google_folder/GoogleFolderContainer.js'; import { UserConfigService } from '../google_folder/UserConfigService.js'; import { MarkdownTreeProcessor } from '../transform/MarkdownTreeProcessor.js'; import { WorkerPool } from './WorkerPool.js'; import { GitScanner } from '../../git/GitScanner.js'; import { FileContentService } from '../../utils/FileContentService.js'; import { CACHE_PATH } from '../server/routes/FolderController.js'; import { ActionRunnerContainer, convertActionYaml } from '../action/ActionRunnerContainer.js'; import { getContentFileService } from '../transform/utils.js'; import { UploadContainer } from '../google_folder/UploadContainer.js'; import { startDockerProxy } from '../action/dockerProxy.js'; const __filename = globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).filename; export function initJob() { return { id: crypto.randomUUID(), state: 'waiting' }; } export async function clearCachedChanges(googleFileSystem) { await googleFileSystem.remove(CACHE_PATH); } function notCompletedJob(job) { return ['waiting', 'running'].includes(job.state); } function removeOldByType(type) { return (job) => { if (job.type !== type) { return true; } return !(job.state === 'failed' || job.state === 'done'); }; } function removeOldById(id) { return (job) => { return job.id !== id; }; } function removeOldSingleJobs(fileId) { if (fileId) { return (job) => { if (job.type !== 'sync') { return true; } if (job.payload !== fileId) { return true; } return !(job.state === 'failed' || job.state === 'done'); }; } return (job) => { if (job.type !== 'sync') { return true; } return !(job.state === 'failed' || job.state === 'done'); }; } function filterSplit(driveJobs, filter) { driveJobs.archive = [].concat(driveJobs.archive).concat(driveJobs.jobs.filter(a => !filter(a))); driveJobs.archive = driveJobs.archive.slice(driveJobs.archive.length - 100, driveJobs.archive.length); driveJobs.jobs = driveJobs.jobs.filter(a => filter(a)); } export class JobManagerContainer extends Container { constructor(params) { super(params); Object.defineProperty(this, "params", { enumerable: true, configurable: true, writable: true, value: params }); Object.defineProperty(this, "driveJobsMap", { enumerable: true, configurable: true, writable: true, value: {} }); Object.defineProperty(this, "workerPool", { enumerable: true, configurable: true, writable: true, value: void 0 }); } async init(engine) { await super.init(engine); startDockerProxy(5000, '/var/run/docker.sock'); startDockerProxy(5001, '/var/run/podman/podman.sock'); this.workerPool = new WorkerPool(os.cpus().length); } async getDriveJobs(driveId) { if (!this.driveJobsMap[driveId]) { const driveFileSystem = await this.filesService.getSubFileService(driveId, ''); const driveJobs = await driveFileSystem.readJson('.jobs.json'); this.driveJobsMap[driveId] = driveJobs || { driveId, jobs: [] }; } if (!this.driveJobsMap[driveId].archive) { this.driveJobsMap[driveId].archive = []; } return this.driveJobsMap[driveId]; } async setDriveJobs(driveId, driveJobs) { if (driveJobs) { this.driveJobsMap[driveId] = driveJobs; } this.engine.emit(driveId, 'jobs:changed', driveJobs); const driveFileSystem = await this.filesService.getSubFileService(driveId, ''); await driveFileSystem.writeJson('.jobs.json', driveJobs); } async scheduleWorker(type, payload) { this.engine.logger.info(`scheduleWorker: ${type}`); try { return await this.workerPool.schedule(type, payload); } catch (err) { this.engine.logger.error('Worker error ' + err); throw err; } } async schedule(driveId, job) { job.state = 'waiting'; job.ts = +new Date(); const driveJobs = await this.getDriveJobs(driveId); switch (job.type) { case 'sync': if (driveJobs.jobs.find(subJob => subJob.type === 'sync_all' && notCompletedJob(subJob))) { return; } if (driveJobs.jobs.find(subJob => subJob.type === 'sync' && subJob.payload === job.payload && notCompletedJob(subJob))) { return; } driveJobs.jobs.push(job); break; case 'sync_all': if (driveJobs.jobs.find(subJob => subJob.type === 'sync_all' && notCompletedJob(subJob))) { return; } driveJobs.jobs = driveJobs.jobs.filter(subJob => subJob.state === 'running'); driveJobs.jobs.push(job); break; case 'run_action': { const googleFileSystem = await this.filesService.getSubFileService(driveId, '/'); const userConfigService = new UserConfigService(googleFileSystem); await userConfigService.load(); const config = userConfigService.config; const workflow = await convertActionYaml(config.actions_yaml); const actionId = job.action_id ? job.action_id : workflow.on[job.trigger]; const workflowJob = workflow.jobs[actionId]; if (workflowJob && workflowJob.name) { job.title = workflowJob.name; job.action_id = actionId; driveJobs.jobs.push(job); this.engine.emit(driveId, 'toasts:added', { title: 'Scheduled: ' + workflowJob.name, message: JSON.stringify(job, null, 2), type: 'action:scheduled', payload: job.payload ? job.payload : 'all' }); } } break; case 'git_fetch': if (driveJobs.jobs.find(subJob => subJob.type === 'git_fetch' && notCompletedJob(subJob))) { return; } driveJobs.jobs.push(job); break; case 'git_pull': if (driveJobs.jobs.find(subJob => subJob.type === 'git_pull' && notCompletedJob(subJob))) { return; } driveJobs.jobs.push(job); break; case 'git_push': if (driveJobs.jobs.find(subJob => subJob.type === 'git_push' && notCompletedJob(subJob))) { return; } driveJobs.jobs.push(job); break; case 'git_commit': if (driveJobs.jobs.find(subJob => subJob.type === 'git_commit' && notCompletedJob(subJob))) { return; } driveJobs.jobs.push(job); break; case 'git_reset': if (driveJobs.jobs.find(subJob => subJob.type === 'git_reset' && notCompletedJob(subJob))) { return; } driveJobs.jobs.push(job); break; case 'upload': if (driveJobs.jobs.find(subJob => subJob.type === 'upload' && notCompletedJob(subJob))) { return; } driveJobs.jobs.push(job); break; } await this.setDriveJobs(driveId, driveJobs); } async ps() { return this.driveJobsMap; } async inspect(driveId) { return await this.getDriveJobs(driveId); } // eslint-disable-next-line @typescript-eslint/no-empty-function async run() { const folderRegistryContainer = this.engine.getContainer('folder_registry'); const folders = await folderRegistryContainer.getFolders(); for (const driveId in folders) { const driveJobs = await this.getDriveJobs(driveId); if (driveJobs.jobs) { driveJobs.jobs = []; await this.setDriveJobs(driveId, { driveId, jobs: [], archive: driveJobs.archive }); } } setInterval(async () => { try { const now = +new Date(); for (const driveId in this.driveJobsMap) { const driveJobs = await this.getDriveJobs(driveId); if (driveJobs.jobs.length === 0 && driveJobs.archive.length === 0) { delete this.driveJobsMap[driveId]; await this.setDriveJobs(driveId, this.driveJobsMap[driveId]); } if (driveJobs.jobs.length === 0) { delete this.driveJobsMap[driveId]; continue; } const lastTs = driveJobs.jobs[driveJobs.jobs.length - 1].ts; if (now - lastTs < 1000) { continue; } if (driveJobs.jobs.find(job => job.state === 'running')) { continue; } const currentJob = driveJobs.jobs.find(job => job.state === 'waiting' && (!job.startAfter || job.startAfter > now)); if (!currentJob) { continue; } currentJob.state = 'running'; currentJob.started = now; this.engine.emit(driveId, 'jobs:changed', driveJobs); this.runJob(driveId, currentJob, driveJobs) .then(async () => { filterSplit(driveJobs, removeOldById(currentJob.id)); if (currentJob.type === 'upload') { filterSplit(driveJobs, removeOldByType('upload')); this.engine.emit(driveId, 'toasts:added', { title: 'Google Drive upload done', type: 'upload:done', links: {} }); } if (currentJob.type === 'git_reset') { filterSplit(driveJobs, removeOldByType('git_reset')); this.engine.emit(driveId, 'toasts:added', { title: 'Git reset done', type: 'git_reset:done', links: { '#git_log': 'View git history' }, }); } if (currentJob.type === 'git_commit') { filterSplit(driveJobs, removeOldByType('git_commit')); this.engine.emit(driveId, 'toasts:added', { title: 'Git commit done', type: 'git_commit:done', links: { '#git_log': 'View git history' }, }); } if (currentJob.type === 'git_push') { filterSplit(driveJobs, removeOldByType('git_push')); this.engine.emit(driveId, 'toasts:added', { title: 'Git push done', type: 'git_push:done', links: { '#git_log': 'View git history' }, }); } if (currentJob.type === 'sync_all') { filterSplit(driveJobs, removeOldByType('sync_all')); filterSplit(driveJobs, removeOldSingleJobs(null)); this.engine.emit(driveId, 'toasts:added', { title: 'Sync all done', type: 'sync:done', payload: 'all' }); } if (currentJob.type === 'sync') { filterSplit(driveJobs, removeOldSingleJobs(currentJob.payload)); this.engine.emit(driveId, 'toasts:added', { title: 'Sync done', type: 'sync:done', payload: currentJob.payload }); } currentJob.state = 'done'; currentJob.finished = +new Date(); await this.setDriveJobs(driveId, driveJobs); }) .catch(err => { filterSplit(driveJobs, removeOldById(currentJob.id)); if (currentJob.type === 'upload') { filterSplit(driveJobs, removeOldByType('upload')); this.engine.emit(driveId, 'toasts:added', { title: 'Google Drive upload failed', type: 'upload:failed', err: err.message, links: { ['#drive_logs:job-' + currentJob.id]: 'View logs' }, }); } if (currentJob.type === 'git_reset') { filterSplit(driveJobs, removeOldByType('git_reset')); this.engine.emit(driveId, 'toasts:added', { title: 'Git reset failed', type: 'git_reset:failed', err: err.message, links: { ['#drive_logs:job-' + currentJob.id]: 'View logs' }, }); } if (currentJob.type === 'git_commit') { filterSplit(driveJobs, removeOldByType('git_commit')); this.engine.emit(driveId, 'toasts:added', { title: 'Git commit failed', type: 'git_commit:failed', err: err.message, links: { ['#drive_logs:job-' + currentJob.id]: 'View logs' }, }); } if (currentJob.type === 'git_push') { filterSplit(driveJobs, removeOldByType('git_push')); this.engine.emit(driveId, 'toasts:added', { title: 'Git push failed', type: 'git_push:failed', err: err.message, links: { ['#drive_logs:job-' + currentJob.id]: 'View logs' }, }); } if (currentJob.type === 'sync_all') { filterSplit(driveJobs, removeOldByType('sync_all')); filterSplit(driveJobs, removeOldSingleJobs(null)); this.engine.emit(driveId, 'toasts:added', { title: 'Sync all failed', type: 'sync:failed', err: err.message, links: { ['#drive_logs:job-' + currentJob.id]: 'View logs' }, payload: 'all' }); } if (currentJob.type === 'sync') { filterSplit(driveJobs, removeOldSingleJobs(currentJob.payload)); this.engine.emit(driveId, 'toasts:added', { title: 'Sync failed', type: 'sync:failed', err: err.message, payload: currentJob.payload }); } const logger = this.engine.logger.child({ filename: __filename, driveId: driveId, jobId: currentJob.id }); if (err instanceof AggregateError) { for (const subErr of err.errors) { logger.error(subErr.stack ? subErr.stack : subErr.message); } } else { if (err.message.indexOf('Process exited') === -1) { // Already in log logger.error(err.stack ? err.stack : err.message); } } currentJob.state = 'failed'; currentJob.finished = +new Date(); this.setDriveJobs(driveId, driveJobs); }); } } catch (err) { this.engine.logger.error(err.stack ? err.stack : err.message); } }, 100); } async upload(folderId, jobId, access_token) { const uploadContainer = new UploadContainer({ cmd: 'pull', name: folderId, folderId: folderId, jobId, apiContainer: 'google_api', access_token }); const generatedFileService = await this.filesService.getSubFileService(folderId + '_transform', '/'); const googleFileSystem = await this.filesService.getSubFileService(folderId, '/'); await uploadContainer.mount2(googleFileSystem, generatedFileService); uploadContainer.onProgressNotify(({ completed, total, warnings }) => { if (!this.driveJobsMap[folderId]) { return; } const jobs = this.driveJobsMap[folderId].jobs || []; const job = jobs.find(j => j.state === 'running' && j.type === 'upload'); if (job) { job.progress = { completed: completed, total: total, warnings }; this.engine.emit(folderId, 'jobs:changed', this.driveJobsMap[folderId]); } }); await this.engine.registerContainer(uploadContainer); try { await uploadContainer.run(); } finally { await this.engine.unregisterContainer(uploadContainer.params.name); } } async sync(folderId, jobId, filesIds = []) { const downloadContainer = new GoogleFolderContainer({ cmd: 'pull', name: folderId, folderId: folderId, jobId, apiContainer: 'google_api' }, { filesIds }); downloadContainer.setForceDownloadFilters(filesIds.length === 1); await downloadContainer.mount(await this.filesService.getSubFileService(folderId, '/')); downloadContainer.onProgressNotify(({ completed, total, warnings, failed }) => { if (!this.driveJobsMap[folderId]) { return; } const jobs = this.driveJobsMap[folderId].jobs || []; const job = jobs.find(j => j.state === 'running' && j.type === 'sync_all'); if (job) { job.progress = { completed: completed, total: total, failed: failed, warnings }; this.engine.emit(folderId, 'jobs:changed', this.driveJobsMap[folderId]); } }); await this.engine.registerContainer(downloadContainer); try { await downloadContainer.run(); } finally { await this.engine.unregisterContainer(downloadContainer.params.name); } const jobs = this.driveJobsMap[folderId].jobs || []; const job = jobs.find(j => j.state === 'running' && j.type === 'sync_all'); if (job?.progress?.failed) { throw new Error('Sync failed'); } else { await this.schedule(folderId, { ...initJob(), type: 'run_action', title: 'Run action: on sync', trigger: 'internal/sync', payload: JSON.stringify({ selectedFileId: filesIds.length === 1 ? filesIds[0] : null }) }); } } async runAction(folderId, jobId, action_id, payload, user) { const runActionContainer = new ActionRunnerContainer({ name: folderId, jobId, action_id, payload, user_name: user?.name || 'WikiGDrive', user_email: user?.email || 'wikigdrive@wikigdrive.com' }); const generatedFileService = await this.filesService.getSubFileService(folderId + '_transform', '/'); const googleFileSystem = await this.filesService.getSubFileService(folderId, '/'); const tempPath = fs.mkdtempSync(path.join(this.filesService.getRealPath(), 'temp-')); const tempFileService = new FileContentService(tempPath); await runActionContainer.mount3(googleFileSystem, generatedFileService, tempFileService); await this.engine.registerContainer(runActionContainer); try { await runActionContainer.run(folderId); if (runActionContainer.failed()) { throw new Error('One on action steps has failed'); } } finally { fs.rmSync(tempPath, { recursive: true, force: true }); await this.engine.unregisterContainer(runActionContainer.params.name); } } async gitFetch(driveId, jobId) { const logger = this.engine.logger.child({ filename: __filename, driveId, jobId }); try { const transformedFileSystem = await this.filesService.getSubFileService(driveId + '_transform', ''); const gitScanner = new GitScanner(logger, transformedFileSystem.getRealPath(), 'wikigdrive@wikigdrive.com'); await gitScanner.initialize(); const googleFileSystem = await this.filesService.getSubFileService(driveId, ''); const userConfigService = new UserConfigService(googleFileSystem); await gitScanner.fetch({ privateKeyFile: await userConfigService.getDeployPrivateKeyPath() }); await this.schedule(driveId, { ...initJob(), type: 'run_action', title: 'Run action: on git_fetch', trigger: 'git_fetch' }); return {}; } catch (err) { if (err.message.indexOf('Process exited') === -1) { // Already in log logger.error(err.stack ? err.stack : err.message); } if (err.message.indexOf('Failed to retrieve list of SSH authentication methods') > -1) { return { error: 'Failed to authenticate' }; } throw err; } } async gitPull(driveId, jobId) { const logger = this.engine.logger.child({ filename: __filename, driveId, jobId }); try { const transformedFileSystem = await this.filesService.getSubFileService(driveId + '_transform', ''); const gitScanner = new GitScanner(logger, transformedFileSystem.getRealPath(), 'wikigdrive@wikigdrive.com'); await gitScanner.initialize(); const googleFileSystem = await this.filesService.getSubFileService(driveId, ''); const userConfigService = new UserConfigService(googleFileSystem); const userConfig = await userConfigService.load(); await gitScanner.pullBranch(userConfig.remote_branch, { privateKeyFile: await userConfigService.getDeployPrivateKeyPath() }); await this.schedule(driveId, { ...initJob(), type: 'run_action', title: 'Run action: on git_pull', trigger: 'git_pull' }); return {}; } catch (err) { if (err.message.indexOf('Process exited') === -1) { // Already in log logger.error(err.stack ? err.stack : err.message); } if (err.message.indexOf('Failed to retrieve list of SSH authentication methods') > -1) { return { error: 'Failed to authenticate' }; } throw err; } } async gitPush(driveId, jobId) { const logger = this.engine.logger.child({ filename: __filename, driveId, jobId }); try { const transformedFileSystem = await this.filesService.getSubFileService(driveId + '_transform', ''); const gitScanner = new GitScanner(logger, transformedFileSystem.getRealPath(), 'wikigdrive@wikigdrive.com'); await gitScanner.initialize(); const googleFileSystem = await this.filesService.getSubFileService(driveId, ''); const userConfigService = new UserConfigService(googleFileSystem); const userConfig = await userConfigService.load(); await gitScanner.pushBranch(userConfig.remote_branch, { privateKeyFile: await userConfigService.getDeployPrivateKeyPath() }); return {}; } catch (err) { if (err.message.indexOf('Process exited') === -1) { // Already in log logger.error(err.stack ? err.stack : err.message); } if (err.message.indexOf('Failed to retrieve list of SSH authentication methods') > -1) { return { error: 'Failed to authenticate' }; } throw err; } } async gitCommit(driveId, jobId, message, filePaths, user) { const logger = this.engine.logger.child({ filename: __filename, driveId, jobId }); const transformedFileSystem = await this.filesService.getSubFileService(driveId + '_transform', ''); const gitScanner = new GitScanner(logger, transformedFileSystem.getRealPath(), 'wikigdrive@wikigdrive.com'); await gitScanner.initialize(); const googleFileSystem = await this.filesService.getSubFileService(driveId, ''); const userConfigService = new UserConfigService(googleFileSystem); const userConfig = await userConfigService.load(); const contentFileService = await getContentFileService(transformedFileSystem, userConfigService); const markdownTreeProcessor = new MarkdownTreeProcessor(contentFileService); await markdownTreeProcessor.load(); if (userConfig.companion_files_rule) { gitScanner.setCompanionFileResolver(async (filePath) => { if (!filePath.endsWith('.md')) { return []; } let subdir = (userConfigService.config.transform_subdir || '') .replace(/^\//, '') .replace(/\/$/, ''); if (subdir.length > 0) { subdir += '/'; } filePath = filePath .replace(/^\//, '') .substring(subdir.length); const tuple = await markdownTreeProcessor.findByPath('/' + filePath); const treeItem = tuple[0]; if (!treeItem) { return []; } const retVal = new Set(); const sandbox = new Sandbox.default(); const exec = sandbox.compile('return ' + (userConfig.companion_files_rule || 'false')); await markdownTreeProcessor.walkTree((treeNode) => { const commit = { path: subdir + treeItem.path.replace(/^\//, ''), id: treeItem.id, fileName: treeItem.fileName, mimeType: treeItem.mimeType, redirectTo: treeItem.redirectTo }; const file = { path: subdir + treeNode.path.replace(/^\//, ''), id: treeNode.id, fileName: treeNode.fileName, mimeType: treeNode.mimeType, redirectTo: treeNode.redirectTo }; const result = exec({ commit, file }).run(); if (result) { retVal.add(file.path); } return false; }); return Array.from(retVal); }); } await gitScanner.commit(message, filePaths, user); await this.schedule(driveId, { ...initJob(), type: 'run_action', title: 'Run action: on commit', trigger: 'commit', user }); } async gitReset(driveId, jobId, type) { const logger = this.engine.logger.child({ filename: __filename, driveId, jobId }); try { const transformedFileSystem = await this.filesService.getSubFileService(driveId + '_transform', ''); const gitScanner = new GitScanner(logger, transformedFileSystem.getRealPath(), 'wikigdrive@wikigdrive.com'); await gitScanner.initialize(); const googleFileSystem = await this.filesService.getSubFileService(driveId, ''); const userConfigService = new UserConfigService(googleFileSystem); const userConfig = await userConfigService.load(); const contentFileService = await getContentFileService(transformedFileSystem, userConfigService); const markdownTreeProcessor = new MarkdownTreeProcessor(contentFileService); switch (type) { case 'local': await gitScanner.reset.resetToLocal({ privateKeyFile: await userConfigService.getDeployPrivateKeyPath() }); await markdownTreeProcessor.regenerateTree(driveId); await markdownTreeProcessor.save(); break; case 'remote': { await gitScanner.reset.resetToRemote(userConfig.remote_branch, { privateKeyFile: await userConfigService.getDeployPrivateKeyPath() }); await markdownTreeProcessor.regenerateTree(driveId); await markdownTreeProcessor.save(); } break; } await this.schedule(driveId, { ...initJob(), type: 'run_action', title: 'Run action: on git_reset', trigger: 'git_reset' }); } catch (err) { if (err.message.indexOf('Process exited') === -1) { // Already in log logger.error(err.stack ? err.stack : err.message); } if (err.message.indexOf('Failed to retrieve list of SSH authentication methods') > -1) { return { error: 'Failed to authenticate' }; } throw err; } } async scheduleRetry(driveId, changesToFetch, markdownTreeProcessor) { if (changesToFetch.length === 0) { return; } if (markdownTreeProcessor.isEmpty()) { return; } const filesToRetry = []; for (const change of changesToFetch) { const [treeItem] = await markdownTreeProcessor.findById(change.id); if (treeItem?.modifiedTime && treeItem.modifiedTime < change.modifiedTime) { filesToRetry.push(change); } } const now = +new Date(); if (filesToRetry.length > 0) { for (const change of filesToRetry) { await this.schedule(driveId, { ...initJob(), type: 'sync', startAfter: now + 10 * 1000, payload: change.id, title: 'Retry syncing file: ' + (change.title || change.name) }); } } } async runJob(driveId, currentJob, driveJobs) { const logger = this.engine.logger.child({ filename: __filename, driveId: driveId, jobId: currentJob.id }); logger.info('runJob ' + driveId + ' ' + JSON.stringify(currentJob)); switch (currentJob.type) { case 'sync': await this.sync(driveId, currentJob.id, currentJob.payload.split(',')); break; case 'sync_all': await this.sync(driveId, currentJob.id); break; case 'run_action': try { await this.runAction(driveId, currentJob.id, currentJob.action_id, currentJob.payload, currentJob.user); await this.clearGitCache(driveId); // TODO: check if necessary? await this.schedule(driveId, { ...initJob(), type: 'run_action', title: 'Run action:', trigger: currentJob.action_id }); this.engine.emit(driveId, 'toasts:added', { title: 'Done: ' + currentJob.title, type: 'run_action:done', payload: this.params.payload }); } catch (err) { this.engine.emit(driveId, 'toasts:added', { title: 'Failed: ' + currentJob.title, type: 'run_action:failed', err: err.message, links: { ['#drive_logs:job-' + currentJob.id]: 'View logs' }, payload: this.params.payload }); throw err; } break; case 'git_fetch': try { await this.gitFetch(driveId, currentJob.id); await this.clearGitCache(driveId); driveJobs.jobs = driveJobs.jobs.filter(removeOldByType('git_fetch')); this.engine.emit(driveId, 'toasts:added', { title: 'Git fetch done', type: 'git_fetch:done', links: { '#git_log': 'View git history' }, }); } catch (err) { driveJobs.jobs = driveJobs.jobs.filter(removeOldByType('git_fetch')); this.engine.emit(driveId, 'toasts:added', { title: 'Git fetch failed', type: 'git_fetch:failed', err: err.message, links: { ['#drive_logs:job-' + currentJob.id]: 'View logs' }, }); throw err; } break; case 'git_pull': try { await this.gitPull(driveId, currentJob.id); await this.clearGitCache(driveId); driveJobs.jobs = driveJobs.jobs.filter(removeOldByType('git_pull')); this.engine.emit(driveId, 'toasts:added', { title: 'Git pull done', type: 'git_pull:done', links: { '#git_log': 'View git history' }, }); } catch (err) { driveJobs.jobs = driveJobs.jobs.filter(removeOldByType('git_pull')); this.engine.emit(driveId, 'toasts:added', { title: 'Git pull failed', type: 'git_pull:failed', err: err.message, links: { ['#drive_logs:job-' + currentJob.id]: 'View logs' }, }); throw err; } break; case 'git_push': await this.gitPush(driveId, currentJob.id); await this.clearGitCache(driveId); break; case 'git_commit': { const { message, filePaths, user } = JSON.parse(currentJob.payload); await this.gitCommit(driveId, currentJob.id, message, filePaths, user); await this.clearGitCache(driveId); } break; case 'git_reset': await this.gitReset(driveId, currentJob.id, currentJob.payload); await this.clearGitCache(driveId); break; case 'upload': await this.upload(driveId, currentJob.id, currentJob.access_token); break; } } async clearGitCache(driveId) { const googleFileSystem = await this.filesService.getSubFileService(driveId, ''); await clearCachedChanges(googleFileSystem); } // eslint-disable-next-line @typescript-eslint/no-empty-function async destroy() { } progressJob(folderId, jobId, { completed, total, warnings, failed }) { if (!this.driveJobsMap[folderId]) { return; } const jobs = this.driveJobsMap[folderId].jobs || []; const job = jobs.find(j => j.id === jobId); if (job) { job.progress = { completed: completed, total: total, failed: failed, warnings }; this.engine.emit(folderId, 'jobs:changed', this.driveJobsMap[folderId]); } } }