@mieweb/wikigdrive
Version:
Google Drive to MarkDown synchronization
965 lines (867 loc) • 33.5 kB
text/typescript
import os from 'node:os';
import fs from 'node:fs';
import path from 'node:path';
import { randomUUID } from 'node:crypto';
import Sandbox from '@nyariv/sandboxjs';
import {Container, ContainerConfig, ContainerEngine} from '../../ContainerEngine.ts';
import {FileId} from '../../model/model.ts';
import {GoogleFolderContainer} from '../google_folder/GoogleFolderContainer.ts';
import {UserConfigService} from '../google_folder/UserConfigService.ts';
import {MarkdownTreeProcessor} from '../transform/MarkdownTreeProcessor.ts';
import {WorkerPool} from './WorkerPool.ts';
import {GitScanner} from '../../git/GitScanner.ts';
import {FileContentService} from '../../utils/FileContentService.ts';
import {CACHE_PATH} from '../server/routes/FolderController.ts';
import {FolderRegistryContainer} from '../folder_registry/FolderRegistryContainer.ts';
import {ActionRunnerContainer, convertActionYaml} from '../action/ActionRunnerContainer.ts';
import {getContentFileService} from '../transform/utils.ts';
import {UploadContainer} from '../google_folder/UploadContainer.ts';
const __filename = import.meta.filename;
export type JobType = 'sync' | 'sync_all' | 'transform' | 'git_fetch' | 'git_pull' | 'git_push' | 'git_reset' | 'git_commit' | 'run_action' | 'upload';
export type JobState = 'waiting' | 'running' | 'failed' | 'done';
export function initJob(): { id: string, state: JobState } {
return {
id: randomUUID(),
state: 'waiting'
};
}
export interface Job {
id: string;
state: JobState;
progress?: { total: number; completed: number; warnings: number; failed?: number };
type: JobType;
title: string;
trigger?: string;
action_id?: string;
payload?: string;
access_token?: string;
ts?: number; // scheduled at
started?: number; // started at
finished?: number; // finished at
startAfter?: number;
user?: {
name: string,
email: string
}
}
export interface Toast {
title: string;
message: string;
}
export interface DriveJobs {
driveId: FileId;
jobs: Job[];
archive: Job[];
}
export interface DriveJobsMap {
[driveId: FileId]: DriveJobs;
}
export async function clearCachedChanges(googleFileSystem: FileContentService) {
await googleFileSystem.remove(CACHE_PATH);
}
function notCompletedJob(job: Job) {
return ['waiting', 'running'].includes(job.state);
}
function removeOldByType(type: JobType) {
return (job: Job) => {
if (job.type !== type) {
return true;
}
return !(job.state === 'failed' || job.state === 'done');
};
}
function removeOldById(id) {
return (job: Job) => {
return job.id !== id;
};
}
function removeOldSingleJobs(fileId) {
if (fileId) {
return (job: Job) => {
if (job.type !== 'sync') {
return true;
}
if (job.payload !== fileId) {
return true;
}
return !(job.state === 'failed' || job.state === 'done');
};
}
return (job: Job) => {
if (job.type !== 'sync') {
return true;
}
return !(job.state === 'failed' || job.state === 'done');
};
}
function filterSplit(driveJobs: 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 {
private driveJobsMap: DriveJobsMap = {};
private workerPool: WorkerPool;
constructor(public readonly params: ContainerConfig) {
super(params);
}
async init(engine: ContainerEngine): Promise<void> {
await super.init(engine);
this.workerPool = new WorkerPool(os.cpus().length);
}
async getDriveJobs(driveId: FileId): Promise<DriveJobs> {
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: FileId, driveJobs: 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: string, payload: unknown): Promise<unknown> {
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: FileId, job: 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(): Promise<DriveJobsMap> {
return this.driveJobsMap;
}
async inspect(driveId: FileId): Promise<DriveJobs> {
return await this.getDriveJobs(driveId);
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
async run() {
const folderRegistryContainer = <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 });
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);
}
private async upload(folderId: FileId, jobId: string, access_token: string) {
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);
}
}
private async sync(folderId: FileId, jobId: string, filesIds: FileId[] = []) {
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
})
});
}
}
private async runAction(folderId: FileId, jobId: string, action_id: string, payload: string, user?: { name: string, email: string }) {
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);
}
}
private async gitFetch(driveId: FileId, jobId: string) {
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) {
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;
}
}
private async gitPull(driveId: FileId, jobId: string) {
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) {
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;
}
}
private async gitPush(driveId: FileId, jobId: string) {
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) {
logger.error(err.message);
if (err.message.indexOf('Failed to retrieve list of SSH authentication methods') > -1) {
return { error: 'Failed to authenticate' };
}
throw err;
}
}
private async gitCommit(driveId: FileId, jobId: string, message: string, filePaths: string[], 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: string) => {
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: Set<string> = 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
});
}
private async gitReset(driveId: FileId, jobId: string, type: string) {
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.resetToLocal({
privateKeyFile: await userConfigService.getDeployPrivateKeyPath()
});
await markdownTreeProcessor.regenerateTree(driveId);
await markdownTreeProcessor.save();
break;
case 'remote':
{
await gitScanner.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) {
logger.error(err.message);
if (err.message.indexOf('Failed to retrieve list of SSH authentication methods') > -1) {
return { error: 'Failed to authenticate' };
}
throw err;
}
}
private async scheduleRetry(driveId: FileId, changesToFetch, markdownTreeProcessor: 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)
});
}
}
}
private async runJob(driveId: FileId, currentJob: Job, driveJobs: 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: FileId) {
const googleFileSystem = await this.filesService.getSubFileService(driveId, '');
await clearCachedChanges(googleFileSystem);
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
async destroy(): Promise<void> {
}
progressJob(folderId: FileId, jobId: string,{ 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]);
}
}
}