diff --git a/src/backend/middlewares/admin/AdminMWs.ts b/src/backend/middlewares/admin/AdminMWs.ts index ee68fca2..d229534f 100644 --- a/src/backend/middlewares/admin/AdminMWs.ts +++ b/src/backend/middlewares/admin/AdminMWs.ts @@ -50,19 +50,22 @@ export class AdminMWs { } } - public static async startJob(req: Request, res: Response, next: NextFunction) { - try { - const id = req.params.id; - const JobConfig: any = req.body.config; - await ObjectManagers.getInstance().JobManager.run(id, JobConfig); - req.resultPipe = 'ok'; - return next(); - } catch (err) { - if (err instanceof Error) { - return next(new ErrorDTO(ErrorCodes.JOB_ERROR, 'Job error: ' + err.toString(), err)); + + public static startJob(soloRun: boolean) { + return async (req: Request, res: Response, next: NextFunction) => { + try { + const id = req.params.id; + const JobConfig: any = req.body.config; + await ObjectManagers.getInstance().JobManager.run(id, JobConfig, soloRun); + req.resultPipe = 'ok'; + return next(); + } catch (err) { + if (err instanceof Error) { + return next(new ErrorDTO(ErrorCodes.JOB_ERROR, 'Job error: ' + err.toString(), err)); + } + return next(new ErrorDTO(ErrorCodes.JOB_ERROR, 'Job error: ' + JSON.stringify(err, null, ' '), err)); } - return next(new ErrorDTO(ErrorCodes.JOB_ERROR, 'Job error: ' + JSON.stringify(err, null, ' '), err)); - } + }; } public static stopJob(req: Request, res: Response, next: NextFunction) { @@ -102,15 +105,4 @@ export class AdminMWs { return next(new ErrorDTO(ErrorCodes.JOB_ERROR, 'Job error: ' + JSON.stringify(err, null, ' '), err)); } } - public static getJobLastRuns(req: Request, res: Response, next: NextFunction) { - try { - req.resultPipe = ObjectManagers.getInstance().JobManager.getJobLastRuns(); - return next(); - } catch (err) { - if (err instanceof Error) { - return next(new ErrorDTO(ErrorCodes.JOB_ERROR, 'Job error: ' + err.toString(), err)); - } - return next(new ErrorDTO(ErrorCodes.JOB_ERROR, 'Job error: ' + JSON.stringify(err, null, ' '), err)); - } - } } diff --git a/src/backend/model/database/interfaces/IJobManager.ts b/src/backend/model/database/interfaces/IJobManager.ts index 792fd185..ac882dc9 100644 --- a/src/backend/model/database/interfaces/IJobManager.ts +++ b/src/backend/model/database/interfaces/IJobManager.ts @@ -3,18 +3,17 @@ import {JobDTO} from '../../../../common/entities/job/JobDTO'; export interface IJobManager { - run(jobId: string, config: any): Promise; + + run(jobId: string, config: any, soloRun: boolean): Promise; stop(jobId: string): void; getProgresses(): { [key: string]: JobProgressDTO }; - getAvailableJobs(): JobDTO[]; stopSchedules(): void; runSchedules(): void; - getJobLastRuns(): { [key: string]: JobProgressDTO }; } diff --git a/src/backend/model/jobs/JobManager.ts b/src/backend/model/jobs/JobManager.ts index 5740a2d3..8c885cfc 100644 --- a/src/backend/model/jobs/JobManager.ts +++ b/src/backend/model/jobs/JobManager.ts @@ -23,18 +23,14 @@ export class JobManager implements IJobManager, IJobListener { } getProgresses(): { [id: string]: JobProgressDTO } { - return this.progressManager.Running; + return this.progressManager.Progresses; } - getJobLastRuns(): { [key: string]: JobProgressDTO } { - return this.progressManager.Finished; - } - - async run(jobName: string, config: T): Promise { + async run(jobName: string, config: T, soloRun: boolean): Promise { const t = this.findJob(jobName); if (t) { t.JobListener = this; - await t.start(config); + await t.start(config, soloRun); } else { Logger.warn(LOG_TAG, 'cannot find job to start:' + jobName); } @@ -54,8 +50,9 @@ export class JobManager implements IJobManager, IJobListener { }; - onJobFinished = async (job: IJob, state: JobProgressStates): Promise => { - if (state !== JobProgressStates.finished) { // if it was not finished peacefully, do not start the next one + onJobFinished = async (job: IJob, state: JobProgressStates, soloRun: boolean): Promise => { + // if it was not finished peacefully or was a soloRun, do not start the next one + if (state !== JobProgressStates.finished || soloRun === true) { return; } const sch = Config.Server.Jobs.scheduled.find(s => s.jobName === job.Name); @@ -64,7 +61,7 @@ export class JobManager implements IJobManager, IJobListener { (s.trigger).afterScheduleName === sch.name); for (let i = 0; i < children.length; ++i) { try { - await this.run(children[i].jobName, children[i].config); + await this.run(children[i].jobName, children[i].config, false); } catch (e) { NotificationManager.warning('Job running error:' + children[i].name, e.toString()); } @@ -99,7 +96,7 @@ export class JobManager implements IJobManager, IJobListener { const timer: NodeJS.Timeout = setTimeout(async () => { this.timers = this.timers.filter(t => t.timer !== timer); - await this.run(schedule.jobName, schedule.config); + await this.run(schedule.jobName, schedule.config, false); this.runSchedule(schedule); }, nextDate.getTime() - Date.now()); this.timers.push({schedule: schedule, timer: timer}); diff --git a/src/backend/model/jobs/JobProgressManager.ts b/src/backend/model/jobs/JobProgressManager.ts index cd665cec..3e558cc7 100644 --- a/src/backend/model/jobs/JobProgressManager.ts +++ b/src/backend/model/jobs/JobProgressManager.ts @@ -5,13 +5,13 @@ import {Config} from '../../../common/config/private/Config'; import {JobProgressDTO, JobProgressStates} from '../../../common/entities/job/JobProgressDTO'; export class JobProgressManager { - private static readonly VERSION = 1; - db: { + private static readonly VERSION = 2; + private db: { version: number, - db: { [key: string]: { progress: JobProgressDTO, timestamp: number } } + progresses: { [key: string]: { progress: JobProgressDTO, timestamp: number } } } = { version: JobProgressManager.VERSION, - db: {} + progresses: {} }; private readonly dbPath: string; private timer: NodeJS.Timeout = null; @@ -21,29 +21,20 @@ export class JobProgressManager { this.loadDB().catch(console.error); } - get Running(): { [key: string]: JobProgressDTO } { + get Progresses(): { [key: string]: JobProgressDTO } { const m: { [key: string]: JobProgressDTO } = {}; - for (const key of Object.keys(this.db.db)) { - if (this.db.db[key].progress.state === JobProgressStates.running) { - m[key] = this.db.db[key].progress; + for (const key of Object.keys(this.db.progresses)) { + m[key] = this.db.progresses[key].progress; + if (this.db.progresses[key].progress.state === JobProgressStates.running) { m[key].time.end = Date.now(); } } return m; } - get Finished(): { [key: string]: JobProgressDTO } { - const m: { [key: string]: JobProgressDTO } = {}; - for (const key of Object.keys(this.db.db)) { - if (this.db.db[key].progress.state !== JobProgressStates.running) { - m[key] = this.db.db[key].progress; - } - } - return m; - } onJobProgressUpdate(progress: JobProgressDTO) { - this.db.db[progress.HashName] = {progress: progress, timestamp: Date.now()}; + this.db.progresses[progress.HashName] = {progress: progress, timestamp: Date.now()}; this.delayedSave(); } @@ -60,19 +51,20 @@ export class JobProgressManager { } this.db = db; - while (Object.keys(this.db.db).length > Config.Server.Jobs.maxSavedProgress) { + while (Object.keys(this.db.progresses).length > Config.Server.Jobs.maxSavedProgress) { let min: string = null; - for (const key of Object.keys(this.db.db)) { - if (min === null || this.db.db[min].timestamp > this.db.db[key].timestamp) { + for (const key of Object.keys(this.db.progresses)) { + if (min === null || this.db.progresses[min].timestamp > this.db.progresses[key].timestamp) { min = key; } } - delete this.db.db[min]; + delete this.db.progresses[min]; } - for (const key of Object.keys(this.db.db)) { - if (this.db.db[key].progress.state === JobProgressStates.running) { - this.db.db[key].progress.state = JobProgressStates.interrupted; + for (const key of Object.keys(this.db.progresses)) { + if (this.db.progresses[key].progress.state === JobProgressStates.running || + this.db.progresses[key].progress.state === JobProgressStates.cancelling) { + this.db.progresses[key].progress.state = JobProgressStates.interrupted; } } } @@ -88,7 +80,7 @@ export class JobProgressManager { this.timer = setTimeout(async () => { this.saveDB().catch(console.error); this.timer = null; - }, 1000); + }, 5000); } } diff --git a/src/backend/model/jobs/jobs/IJob.ts b/src/backend/model/jobs/jobs/IJob.ts index 13e61bb6..6ca30d7c 100644 --- a/src/backend/model/jobs/jobs/IJob.ts +++ b/src/backend/model/jobs/jobs/IJob.ts @@ -8,7 +8,7 @@ export interface IJob extends JobDTO { Progress: JobProgress; JobListener: IJobListener; - start(config: T): Promise; + start(config: T, soloRun?: boolean): Promise; cancel(): void; diff --git a/src/backend/model/jobs/jobs/IJobListener.ts b/src/backend/model/jobs/jobs/IJobListener.ts index f0e49c85..9dc978e8 100644 --- a/src/backend/model/jobs/jobs/IJobListener.ts +++ b/src/backend/model/jobs/jobs/IJobListener.ts @@ -3,7 +3,7 @@ import {IJob} from './IJob'; import {JobProgressStates} from '../../../../common/entities/job/JobProgressDTO'; export interface IJobListener { - onJobFinished(job: IJob, state: JobProgressStates): void; + onJobFinished(job: IJob, state: JobProgressStates, soloRun: boolean): void; onProgressUpdate(progress: JobProgress): void; } diff --git a/src/backend/model/jobs/jobs/Job.ts b/src/backend/model/jobs/jobs/Job.ts index 5ac0c78e..081d7af1 100644 --- a/src/backend/model/jobs/jobs/Job.ts +++ b/src/backend/model/jobs/jobs/Job.ts @@ -16,6 +16,7 @@ export abstract class Job implements IJob { protected prResolve: () => void; protected IsInstant = false; private jobListener: IJobListener; + private soloRun: boolean; public set JobListener(value: IJobListener) { this.jobListener = value; @@ -32,13 +33,15 @@ export abstract class Job implements IJob { return this.progress; } - public get CanRun() { - return this.Progress == null && this.Supported; + protected get InProgress(): boolean { + return this.Progress !== null && (this.Progress.State === JobProgressStates.running || + this.Progress.State === JobProgressStates.cancelling); } - public start(config: T): Promise { - if (this.CanRun) { - Logger.info(LOG_TAG, 'Running job: ' + this.Name); + public start(config: T, soloRun = false): Promise { + if (this.InProgress === false && this.Supported === true) { + Logger.info(LOG_TAG, 'Running job ' + (soloRun === true ? 'solo' : '') + ': ' + this.Name); + this.soloRun = soloRun; this.config = config; this.progress = new JobProgress(JobDTO.getHashName(this.Name, this.config)); this.progress.OnChange = this.jobListener.onProgressUpdate; @@ -52,12 +55,15 @@ export abstract class Job implements IJob { } return pr; } else { - Logger.info(LOG_TAG, 'Job already running: ' + this.Name); - return Promise.reject(); + Logger.info(LOG_TAG, 'Job already running or not supported: ' + this.Name); + return Promise.reject('Job already running or not supported: ' + this.Name); } } public cancel(): void { + if (this.InProgress === false) { + return; + } Logger.info(LOG_TAG, 'Stopping job: ' + this.Name); this.Progress.State = JobProgressStates.cancelling; } @@ -69,18 +75,20 @@ export abstract class Job implements IJob { }; } - protected abstract async step(): Promise; protected abstract async init(): Promise; private onFinish(): void { + if (this.InProgress === false) { + return; + } if (this.Progress.State === JobProgressStates.running) { this.Progress.State = JobProgressStates.finished; - } - if (this.Progress.State === JobProgressStates.cancelling) { + } else if (this.Progress.State === JobProgressStates.cancelling) { this.Progress.State = JobProgressStates.canceled; } + const finishState = this.Progress.State; this.progress = null; if (global.gc) { @@ -90,13 +98,14 @@ export abstract class Job implements IJob { if (this.IsInstant) { this.prResolve(); } - this.jobListener.onJobFinished(this, finishState); + this.jobListener.onJobFinished(this, finishState, this.soloRun); } private run() { process.nextTick(async () => { try { if (this.Progress == null || this.Progress.State !== JobProgressStates.running) { + this.onFinish(); return; } if (await this.step() === false) { // finished diff --git a/src/backend/model/jobs/jobs/ThumbnailGenerationJob.ts b/src/backend/model/jobs/jobs/ThumbnailGenerationJob.ts index 5cb29bb6..e22c4b4e 100644 --- a/src/backend/model/jobs/jobs/ThumbnailGenerationJob.ts +++ b/src/backend/model/jobs/jobs/ThumbnailGenerationJob.ts @@ -31,14 +31,14 @@ export class ThumbnailGenerationJob extends FileJob<{ sizes: number[], indexedOn return true; } - start(config: { sizes: number[], indexedOnly: boolean }): Promise { + start(config: { sizes: number[], indexedOnly: boolean }, soloRun = false): Promise { for (let i = 0; i < config.sizes.length; ++i) { if (Config.Client.Media.Thumbnail.thumbnailSizes.indexOf(config.sizes[i]) === -1) { throw new Error('unknown thumbnails size: ' + config.sizes[i] + '. Add it to the possible thumbnail sizes.'); } } - return super.start(config); + return super.start(config, soloRun); } protected async filterMediaFiles(files: FileDTO[]): Promise { diff --git a/src/backend/routes/admin/AdminRouter.ts b/src/backend/routes/admin/AdminRouter.ts index 3f7b2aca..9bb77fe5 100644 --- a/src/backend/routes/admin/AdminRouter.ts +++ b/src/backend/routes/admin/AdminRouter.ts @@ -43,16 +43,16 @@ export class AdminRouter { AdminMWs.getJobProgresses, RenderingMWs.renderResult ); - app.get('/api/admin/jobs/scheduled/lastRun', - AuthenticationMWs.authenticate, - AuthenticationMWs.authorise(UserRoles.Admin), - AdminMWs.getJobLastRuns, - RenderingMWs.renderResult - ); app.post('/api/admin/jobs/scheduled/:id/start', AuthenticationMWs.authenticate, AuthenticationMWs.authorise(UserRoles.Admin), - AdminMWs.startJob, + AdminMWs.startJob(false), + RenderingMWs.renderResult + ); + app.post('/api/admin/jobs/scheduled/:id/soloStart', + AuthenticationMWs.authenticate, + AuthenticationMWs.authorise(UserRoles.Admin), + AdminMWs.startJob(true), RenderingMWs.renderResult ); app.post('/api/admin/jobs/scheduled/:id/stop', diff --git a/src/frontend/app/app.module.ts b/src/frontend/app/app.module.ts index 3151d895..00daa115 100644 --- a/src/frontend/app/app.module.ts +++ b/src/frontend/app/app.module.ts @@ -89,6 +89,7 @@ import {JobProgressComponent} from './ui/settings/jobs/progress/job-progress.set import {JobsSettingsComponent} from './ui/settings/jobs/jobs.settings.component'; import {ScheduledJobsService} from './ui/settings/scheduled-jobs.service'; import {BackendtextService} from './model/backendtext.service'; +import {JobButtonComponent} from './ui/settings/jobs/button/job-button.settings.component'; @Injectable() @@ -200,6 +201,8 @@ export function translationsFactory(locale: string) { IndexingSettingsComponent, JobProgressComponent, JobsSettingsComponent, + JobButtonComponent, + // Pipes StringifyRole, IconizeSortingMethod, diff --git a/src/frontend/app/ui/settings/indexing/indexing.settings.component.html b/src/frontend/app/ui/settings/indexing/indexing.settings.component.html index c565a51d..d412f5aa 100644 --- a/src/frontend/app/ui/settings/indexing/indexing.settings.component.html +++ b/src/frontend/app/ui/settings/indexing/indexing.settings.component.html @@ -103,33 +103,26 @@ If you add a new folder to your gallery, the site indexes it automatically.  If you would like to trigger indexing manually, click index button.
- (Note: search only works among the indexed directories) + ( + Note: search only works among the indexed directories + ) -
- -
+ + + - - - +
diff --git a/src/frontend/app/ui/settings/indexing/indexing.settings.component.ts b/src/frontend/app/ui/settings/indexing/indexing.settings.component.ts index a8dfac0d..7867b320 100644 --- a/src/frontend/app/ui/settings/indexing/indexing.settings.component.ts +++ b/src/frontend/app/ui/settings/indexing/indexing.settings.component.ts @@ -25,6 +25,8 @@ export class IndexingSettingsComponent extends SettingsComponent + Run {{jobName}} now + + + diff --git a/src/frontend/app/ui/settings/jobs/button/job-button.settings.component.ts b/src/frontend/app/ui/settings/jobs/button/job-button.settings.component.ts new file mode 100644 index 00000000..d4902457 --- /dev/null +++ b/src/frontend/app/ui/settings/jobs/button/job-button.settings.component.ts @@ -0,0 +1,73 @@ +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {JobProgressStates} from '../../../../../../common/entities/job/JobProgressDTO'; +import {ErrorDTO} from '../../../../../../common/entities/Error'; +import {ScheduledJobsService} from '../../scheduled-jobs.service'; +import {NotificationService} from '../../../../model/notification.service'; +import {I18n} from '@ngx-translate/i18n-polyfill'; +import {JobDTO} from '../../../../../../common/entities/job/JobDTO'; + +@Component({ + selector: 'app-settings-job-button', + templateUrl: './job-button.settings.component.html', + styleUrls: ['./job-button.settings.component.css'] +}) +export class JobButtonComponent { + @Input() jobName: string; + @Input() config: any = {}; + @Input() shortName = false; + @Input() disabled = false; + @Input() soloRun = false; + @Input() danger = false; + JobProgressStates = JobProgressStates; + @Output() error = new EventEmitter(); + + constructor(private notification: NotificationService, + public jobsService: ScheduledJobsService, + private i18n: I18n) { + } + + public get Running() { + return this.Progress && (this.Progress.state === JobProgressStates.running || this.Progress.state === JobProgressStates.cancelling); + } + + get Progress() { + return this.jobsService.progress.value[JobDTO.getHashName(this.jobName, this.config)]; + } + + + public async start() { + this.error.emit(''); + try { + await this.jobsService.start(this.jobName, this.config, this.soloRun); + this.notification.info(this.i18n('Job') + ' ' + this.jobName + ' ' + this.i18n('started')); + return true; + } catch (err) { + console.log(err); + if (err.message) { + this.error.emit((err).message); + } + } + + return false; + } + + public async stop() { + this.error.emit(''); + try { + await this.jobsService.stop(this.jobName); + this.notification.info(this.i18n('Job') + ' ' + this.jobName + ' ' + this.i18n('stopped')); + return true; + } catch (err) { + console.log(err); + if (err.message) { + this.error.emit((err).message); + } + } + return false; + } + + +} + + + diff --git a/src/frontend/app/ui/settings/jobs/jobs.settings.component.html b/src/frontend/app/ui/settings/jobs/jobs.settings.component.html index 014340b8..7315852a 100644 --- a/src/frontend/app/ui/settings/jobs/jobs.settings.component.html +++ b/src/frontend/app/ui/settings/jobs/jobs.settings.component.html @@ -8,37 +8,34 @@
-
+
- {{schedule.name}} @ - - - every - {{periods[schedule.trigger.periodicity]}} {{schedule.trigger.atTime | date:"HH:mm":"+0"}} + + + every + {{periods[schedule.trigger.periodicity]}} {{schedule.trigger.atTime | date:"HH:mm":"+0"}} + + {{schedule.trigger.time | date:"medium"}} + never + + after + {{schedule.trigger.afterScheduleName}} + - {{schedule.trigger.time | date:"medium"}} - never - - after - {{schedule.trigger.afterScheduleName}} - - +
- - - +
@@ -120,18 +117,10 @@
- - +
@@ -194,8 +183,8 @@ + *ngIf="getProgress(schedule)" + [progress]="getProgress(schedule)"> diff --git a/src/frontend/app/ui/settings/jobs/jobs.settings.component.ts b/src/frontend/app/ui/settings/jobs/jobs.settings.component.ts index e64321dc..784720e4 100644 --- a/src/frontend/app/ui/settings/jobs/jobs.settings.component.ts +++ b/src/frontend/app/ui/settings/jobs/jobs.settings.component.ts @@ -5,7 +5,6 @@ import {NavigationService} from '../../../model/navigation.service'; import {NotificationService} from '../../../model/notification.service'; import {SettingsComponent} from '../_abstract/abstract.settings.component'; import {I18n} from '@ngx-translate/i18n-polyfill'; -import {ErrorDTO} from '../../../../../common/entities/Error'; import {ScheduledJobsService} from '../scheduled-jobs.service'; import { AfterJobTrigger, @@ -18,7 +17,6 @@ import { import {Utils} from '../../../../../common/Utils'; import {ServerConfig} from '../../../../../common/config/private/IPrivateConfig'; import {ConfigTemplateEntry} from '../../../../../common/entities/job/JobDTO'; -import {Job} from '../../../../../backend/model/jobs/jobs/Job'; import {ModalDirective} from 'ngx-bootstrap/modal'; import {JobProgressDTO, JobProgressStates} from '../../../../../common/entities/job/JobProgressDTO'; import {BackendtextService} from '../../../model/backendtext.service'; @@ -96,46 +94,8 @@ export class JobsSettingsComponent extends SettingsComponenterr).message; - } - } finally { - this.disableButtons = false; - } - - return false; - } - - public async stop(schedule: JobScheduleDTO) { - this.error = ''; - try { - this.disableButtons = true; - await this.jobsService.stop(schedule.jobName); - this.notification.info(this.i18n('Job') + ' ' + schedule.jobName + ' ' + this.i18n('stopped')); - return true; - } catch (err) { - console.log(err); - if (err.message) { - this.error = (err).message; - } - } finally { - this.disableButtons = false; - } - - return false; - } - - remove(index: number) { - this.settings.scheduled.splice(index, 1); + remove(schedule: JobScheduleDTO) { + this.settings.scheduled.splice(this.settings.scheduled.indexOf(schedule), 1); } jobTypeChanged(schedule: JobScheduleDTO) { @@ -211,18 +171,11 @@ export class JobsSettingsComponent extends SettingsComponent list.length) { return 0; diff --git a/src/frontend/app/ui/settings/jobs/progress/job-progress.settings.component.html b/src/frontend/app/ui/settings/jobs/progress/job-progress.settings.component.html index 8acfd6b8..503f241d 100644 --- a/src/frontend/app/ui/settings/jobs/progress/job-progress.settings.component.html +++ b/src/frontend/app/ui/settings/jobs/progress/job-progress.settings.component.html @@ -1,5 +1,4 @@ -
+
Last run:
@@ -7,26 +6,35 @@ {{progress.time.start | date:'medium'}} - {{progress.time.end | date:'mediumTime'}}
-
+
- {{progress.steps.processed}}+{{progress.steps.skipped}}/{{progress.steps.all}} + {{progress.steps.processed + progress.steps.skipped}}/{{progress.steps.all}}
-
+
{{JobProgressStates[progress.state]}}
+
+ +
-
+
-
-
- - +
+ + +
+
@@ -47,7 +55,7 @@ aria-valuemax="100" style="min-width: 2em;" [style.width.%]="((progress.steps.processed+progress.steps.skipped)/progress.steps.all)*100"> - {{progress.steps.processed}}+{{progress.steps.skipped}}/{{progress.steps.all}} + {{progress.steps.processed + progress.steps.skipped}}/{{progress.steps.all}}