You've already forked pigallery2
							
							
				mirror of
				https://github.com/bpatrik/pigallery2.git
				synced 2025-10-30 23:57:43 +02:00 
			
		
		
		
	implementing basic task scheduling
This commit is contained in:
		| @@ -51,7 +51,7 @@ export class AdminMWs { | |||||||
|  |  | ||||||
|     const galleryManager = <ISQLGalleryManager>ObjectManagers.getInstance().GalleryManager; |     const galleryManager = <ISQLGalleryManager>ObjectManagers.getInstance().GalleryManager; | ||||||
|     try { |     try { | ||||||
|       req.resultPipe =  await galleryManager.getPossibleDuplicates(); |       req.resultPipe = await galleryManager.getPossibleDuplicates(); | ||||||
|       return next(); |       return next(); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       if (err instanceof Error) { |       if (err instanceof Error) { | ||||||
| @@ -253,6 +253,7 @@ export class AdminMWs { | |||||||
|       return next(new ErrorDTO(ErrorCodes.SETTINGS_ERROR, 'Settings error: ' + JSON.stringify(err, null, '  '), err)); |       return next(new ErrorDTO(ErrorCodes.SETTINGS_ERROR, 'Settings error: ' + JSON.stringify(err, null, '  '), err)); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public static async updateFacesSettings(req: Request, res: Response, next: NextFunction) { |   public static async updateFacesSettings(req: Request, res: Response, next: NextFunction) { | ||||||
|     if ((typeof req.body === 'undefined') || (typeof req.body.settings === 'undefined')) { |     if ((typeof req.body === 'undefined') || (typeof req.body.settings === 'undefined')) { | ||||||
|       return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'settings is needed')); |       return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'settings is needed')); | ||||||
| @@ -439,6 +440,60 @@ export class AdminMWs { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   public static startTask(req: Request, res: Response, next: NextFunction) { | ||||||
|  |     try { | ||||||
|  |       const id = req.params.id; | ||||||
|  |       const taskConfig: any = req.body.config; | ||||||
|  |       ObjectManagers.getInstance().TaskManager.start(id, taskConfig); | ||||||
|  |       req.resultPipe = 'ok'; | ||||||
|  |       return next(); | ||||||
|  |     } catch (err) { | ||||||
|  |       if (err instanceof Error) { | ||||||
|  |         return next(new ErrorDTO(ErrorCodes.TASK_ERROR, 'Task error: ' + err.toString(), err)); | ||||||
|  |       } | ||||||
|  |       return next(new ErrorDTO(ErrorCodes.TASK_ERROR, 'Task error: ' + JSON.stringify(err, null, '  '), err)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static stopTask(req: Request, res: Response, next: NextFunction) { | ||||||
|  |     try { | ||||||
|  |       const id = req.params.id; | ||||||
|  |       ObjectManagers.getInstance().TaskManager.stop(id); | ||||||
|  |       req.resultPipe = 'ok'; | ||||||
|  |       return next(); | ||||||
|  |     } catch (err) { | ||||||
|  |       if (err instanceof Error) { | ||||||
|  |         return next(new ErrorDTO(ErrorCodes.TASK_ERROR, 'Task error: ' + err.toString(), err)); | ||||||
|  |       } | ||||||
|  |       return next(new ErrorDTO(ErrorCodes.TASK_ERROR, 'Task error: ' + JSON.stringify(err, null, '  '), err)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   public static getAvailableTasks(req: Request, res: Response, next: NextFunction) { | ||||||
|  |     try { | ||||||
|  |       req.resultPipe = ObjectManagers.getInstance().TaskManager.getAvailableTasks(); | ||||||
|  |       return next(); | ||||||
|  |     } catch (err) { | ||||||
|  |       if (err instanceof Error) { | ||||||
|  |         return next(new ErrorDTO(ErrorCodes.TASK_ERROR, 'Task error: ' + err.toString(), err)); | ||||||
|  |       } | ||||||
|  |       return next(new ErrorDTO(ErrorCodes.TASK_ERROR, 'Task error: ' + JSON.stringify(err, null, '  '), err)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static getTaskProgresses(req: Request, res: Response, next: NextFunction) { | ||||||
|  |     try { | ||||||
|  |       req.resultPipe = ObjectManagers.getInstance().TaskManager.getProgresses(); | ||||||
|  |       return next(); | ||||||
|  |     } catch (err) { | ||||||
|  |       if (err instanceof Error) { | ||||||
|  |         return next(new ErrorDTO(ErrorCodes.TASK_ERROR, 'Task error: ' + err.toString(), err)); | ||||||
|  |       } | ||||||
|  |       return next(new ErrorDTO(ErrorCodes.TASK_ERROR, 'Task error: ' + JSON.stringify(err, null, '  '), err)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public static startIndexing(req: Request, res: Response, next: NextFunction) { |   public static startIndexing(req: Request, res: Response, next: NextFunction) { | ||||||
|     try { |     try { | ||||||
|       const createThumbnails: boolean = (<IndexingDTO>req.body).createThumbnails || false; |       const createThumbnails: boolean = (<IndexingDTO>req.body).createThumbnails || false; | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import {IIndexingTaskManager} from './interfaces/IIndexingTaskManager'; | |||||||
| import {IIndexingManager} from './interfaces/IIndexingManager'; | import {IIndexingManager} from './interfaces/IIndexingManager'; | ||||||
| import {IPersonManager} from './interfaces/IPersonManager'; | import {IPersonManager} from './interfaces/IPersonManager'; | ||||||
| import {IVersionManager} from './interfaces/IVersionManager'; | import {IVersionManager} from './interfaces/IVersionManager'; | ||||||
|  | import {ITaskManager} from './interfaces/ITaskManager'; | ||||||
|  |  | ||||||
| export class ObjectManagers { | export class ObjectManagers { | ||||||
|  |  | ||||||
| @@ -21,7 +22,7 @@ export class ObjectManagers { | |||||||
|   private _indexingTaskManager: IIndexingTaskManager; |   private _indexingTaskManager: IIndexingTaskManager; | ||||||
|   private _personManager: IPersonManager; |   private _personManager: IPersonManager; | ||||||
|   private _versionManager: IVersionManager; |   private _versionManager: IVersionManager; | ||||||
|  |   private _taskManager: ITaskManager; | ||||||
|  |  | ||||||
|  |  | ||||||
|   get VersionManager(): IVersionManager { |   get VersionManager(): IVersionManager { | ||||||
| @@ -88,6 +89,14 @@ export class ObjectManagers { | |||||||
|     this._sharingManager = value; |     this._sharingManager = value; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   get TaskManager(): ITaskManager { | ||||||
|  |     return this._taskManager; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   set TaskManager(value: ITaskManager) { | ||||||
|  |     this._taskManager = value; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public static getInstance() { |   public static getInstance() { | ||||||
|     if (this._instance === null) { |     if (this._instance === null) { | ||||||
|       this._instance = new ObjectManagers(); |       this._instance = new ObjectManagers(); | ||||||
| @@ -110,6 +119,7 @@ export class ObjectManagers { | |||||||
|     const IndexingManager = require('./memory/IndexingManager').IndexingManager; |     const IndexingManager = require('./memory/IndexingManager').IndexingManager; | ||||||
|     const PersonManager = require('./memory/PersonManager').PersonManager; |     const PersonManager = require('./memory/PersonManager').PersonManager; | ||||||
|     const VersionManager = require('./memory/VersionManager').VersionManager; |     const VersionManager = require('./memory/VersionManager').VersionManager; | ||||||
|  |     const TaskManager = require('./tasks/TaskManager').TaskManager; | ||||||
|     ObjectManagers.getInstance().GalleryManager = new GalleryManager(); |     ObjectManagers.getInstance().GalleryManager = new GalleryManager(); | ||||||
|     ObjectManagers.getInstance().UserManager = new UserManager(); |     ObjectManagers.getInstance().UserManager = new UserManager(); | ||||||
|     ObjectManagers.getInstance().SearchManager = new SearchManager(); |     ObjectManagers.getInstance().SearchManager = new SearchManager(); | ||||||
| @@ -118,6 +128,7 @@ export class ObjectManagers { | |||||||
|     ObjectManagers.getInstance().IndexingManager = new IndexingManager(); |     ObjectManagers.getInstance().IndexingManager = new IndexingManager(); | ||||||
|     ObjectManagers.getInstance().PersonManager = new PersonManager(); |     ObjectManagers.getInstance().PersonManager = new PersonManager(); | ||||||
|     ObjectManagers.getInstance().VersionManager = new VersionManager(); |     ObjectManagers.getInstance().VersionManager = new VersionManager(); | ||||||
|  |     ObjectManagers.getInstance().TaskManager = new TaskManager(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public static async InitSQLManagers() { |   public static async InitSQLManagers() { | ||||||
| @@ -131,6 +142,7 @@ export class ObjectManagers { | |||||||
|     const IndexingManager = require('./sql/IndexingManager').IndexingManager; |     const IndexingManager = require('./sql/IndexingManager').IndexingManager; | ||||||
|     const PersonManager = require('./sql/PersonManager').PersonManager; |     const PersonManager = require('./sql/PersonManager').PersonManager; | ||||||
|     const VersionManager = require('./sql/VersionManager').VersionManager; |     const VersionManager = require('./sql/VersionManager').VersionManager; | ||||||
|  |     const TaskManager = require('./tasks/TaskManager').TaskManager; | ||||||
|     ObjectManagers.getInstance().GalleryManager = new GalleryManager(); |     ObjectManagers.getInstance().GalleryManager = new GalleryManager(); | ||||||
|     ObjectManagers.getInstance().UserManager = new UserManager(); |     ObjectManagers.getInstance().UserManager = new UserManager(); | ||||||
|     ObjectManagers.getInstance().SearchManager = new SearchManager(); |     ObjectManagers.getInstance().SearchManager = new SearchManager(); | ||||||
| @@ -139,6 +151,7 @@ export class ObjectManagers { | |||||||
|     ObjectManagers.getInstance().IndexingManager = new IndexingManager(); |     ObjectManagers.getInstance().IndexingManager = new IndexingManager(); | ||||||
|     ObjectManagers.getInstance().PersonManager = new PersonManager(); |     ObjectManagers.getInstance().PersonManager = new PersonManager(); | ||||||
|     ObjectManagers.getInstance().VersionManager = new VersionManager(); |     ObjectManagers.getInstance().VersionManager = new VersionManager(); | ||||||
|  |     ObjectManagers.getInstance().TaskManager = new TaskManager(); | ||||||
|     Logger.debug('SQL DB inited'); |     Logger.debug('SQL DB inited'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,4 +2,6 @@ import {DirectoryDTO} from '../../../common/entities/DirectoryDTO'; | |||||||
|  |  | ||||||
| export interface IIndexingManager { | export interface IIndexingManager { | ||||||
|   indexDirectory(relativeDirectoryName: string): Promise<DirectoryDTO>; |   indexDirectory(relativeDirectoryName: string): Promise<DirectoryDTO>; | ||||||
|  |  | ||||||
|  |   resetDB(): Promise<void>; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| import {IndexingProgressDTO} from '../../../common/entities/settings/IndexingProgressDTO'; | import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO'; | ||||||
|  |  | ||||||
| export interface IIndexingTaskManager { | export interface IIndexingTaskManager { | ||||||
|   startIndexing(createThumbnails?: boolean): void; |   startIndexing(createThumbnails?: boolean): void; | ||||||
|  |  | ||||||
|   getProgress(): IndexingProgressDTO; |   getProgress(): TaskProgressDTO; | ||||||
|  |  | ||||||
|   cancelIndexing(): void; |   cancelIndexing(): void; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								backend/model/interfaces/ITaskManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								backend/model/interfaces/ITaskManager.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO'; | ||||||
|  | import {TaskScheduleDTO} from '../../../common/entities/task/TaskScheduleDTO'; | ||||||
|  | import {TaskDTO} from '../../../common/entities/task/TaskDTO'; | ||||||
|  |  | ||||||
|  | export interface ITaskManager { | ||||||
|  |  | ||||||
|  |   start(taskId: string, config: any): void; | ||||||
|  |  | ||||||
|  |   stop(taskId: string): void; | ||||||
|  |  | ||||||
|  |   getProgresses(): { [key: string]: TaskProgressDTO }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   getAvailableTasks(): TaskDTO[]; | ||||||
|  | } | ||||||
| @@ -7,5 +7,9 @@ export class IndexingManager implements IIndexingManager { | |||||||
|     throw new Error('not supported by memory DB'); |     throw new Error('not supported by memory DB'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   resetDB(): Promise<void> { | ||||||
|  |     throw new Error('not supported by memory DB'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import {IIndexingTaskManager} from '../interfaces/IIndexingTaskManager'; | import {IIndexingTaskManager} from '../interfaces/IIndexingTaskManager'; | ||||||
| import {IndexingProgressDTO} from '../../../common/entities/settings/IndexingProgressDTO'; | import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO'; | ||||||
|  |  | ||||||
| export class IndexingTaskManager implements IIndexingTaskManager { | export class IndexingTaskManager implements IIndexingTaskManager { | ||||||
|  |  | ||||||
| @@ -7,7 +7,7 @@ export class IndexingTaskManager implements IIndexingTaskManager { | |||||||
|     throw new Error('not supported by memory DB'); |     throw new Error('not supported by memory DB'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getProgress(): IndexingProgressDTO { |   getProgress(): TaskProgressDTO { | ||||||
|     throw new Error('not supported by memory DB'); |     throw new Error('not supported by memory DB'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ import {FaceRegionEntry} from './enitites/FaceRegionEntry'; | |||||||
| import {ObjectManagers} from '../ObjectManagers'; | import {ObjectManagers} from '../ObjectManagers'; | ||||||
| import {IIndexingManager} from '../interfaces/IIndexingManager'; | import {IIndexingManager} from '../interfaces/IIndexingManager'; | ||||||
| import {DiskMangerWorker} from '../threading/DiskMangerWorker'; | import {DiskMangerWorker} from '../threading/DiskMangerWorker'; | ||||||
|  | import {Logger} from '../../Logger'; | ||||||
|  |  | ||||||
| const LOG_TAG = '[IndexingManager]'; | const LOG_TAG = '[IndexingManager]'; | ||||||
|  |  | ||||||
| @@ -44,6 +45,17 @@ export class IndexingManager implements IIndexingManager { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   async resetDB(): Promise<void> { | ||||||
|  |     Logger.info(LOG_TAG, 'Resetting DB'); | ||||||
|  |     const connection = await SQLConnection.getConnection(); | ||||||
|  |     return connection | ||||||
|  |       .getRepository(DirectoryEntity) | ||||||
|  |       .createQueryBuilder('directory') | ||||||
|  |       .delete() | ||||||
|  |       .execute().then(() => { | ||||||
|  |       }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // Todo fix it, once typeorm support connection pools for sqlite |   // Todo fix it, once typeorm support connection pools for sqlite | ||||||
|   protected async queueForSave(scannedDirectory: DirectoryDTO) { |   protected async queueForSave(scannedDirectory: DirectoryDTO) { | ||||||
|     if (this.savingQueue.findIndex(dir => dir.name === scannedDirectory.name && |     if (this.savingQueue.findIndex(dir => dir.name === scannedDirectory.name && | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import {IIndexingTaskManager} from '../interfaces/IIndexingTaskManager'; | import {IIndexingTaskManager} from '../interfaces/IIndexingTaskManager'; | ||||||
| import {IndexingProgressDTO} from '../../../common/entities/settings/IndexingProgressDTO'; | import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO'; | ||||||
| import {ObjectManagers} from '../ObjectManagers'; | import {ObjectManagers} from '../ObjectManagers'; | ||||||
| import * as path from 'path'; | import * as path from 'path'; | ||||||
| import * as fs from 'fs'; | import * as fs from 'fs'; | ||||||
| @@ -16,7 +16,7 @@ const LOG_TAG = '[IndexingTaskManager]'; | |||||||
|  |  | ||||||
| export class IndexingTaskManager implements IIndexingTaskManager { | export class IndexingTaskManager implements IIndexingTaskManager { | ||||||
|   directoriesToIndex: string[] = []; |   directoriesToIndex: string[] = []; | ||||||
|   indexingProgress: IndexingProgressDTO = null; |   indexingProgress: TaskProgressDTO = null; | ||||||
|   enabled = false; |   enabled = false; | ||||||
|   private indexNewDirectory = async (createThumbnails: boolean = false) => { |   private indexNewDirectory = async (createThumbnails: boolean = false) => { | ||||||
|     if (this.directoriesToIndex.length === 0) { |     if (this.directoriesToIndex.length === 0) { | ||||||
| @@ -27,13 +27,13 @@ export class IndexingTaskManager implements IIndexingTaskManager { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     const directory = this.directoriesToIndex.shift(); |     const directory = this.directoriesToIndex.shift(); | ||||||
|     this.indexingProgress.current = directory; |     this.indexingProgress.comment = directory; | ||||||
|     this.indexingProgress.left = this.directoriesToIndex.length; |     this.indexingProgress.left = this.directoriesToIndex.length; | ||||||
|     const scanned = await ObjectManagers.getInstance().IndexingManager.indexDirectory(directory); |     const scanned = await ObjectManagers.getInstance().IndexingManager.indexDirectory(directory); | ||||||
|     if (this.enabled === false) { |     if (this.enabled === false) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     this.indexingProgress.indexed++; |     this.indexingProgress.progress++; | ||||||
|     this.indexingProgress.time.current = Date.now(); |     this.indexingProgress.time.current = Date.now(); | ||||||
|     for (let i = 0; i < scanned.directories.length; i++) { |     for (let i = 0; i < scanned.directories.length; i++) { | ||||||
|       this.directoriesToIndex.push(path.join(scanned.directories[i].path, scanned.directories[i].name)); |       this.directoriesToIndex.push(path.join(scanned.directories[i].path, scanned.directories[i].name)); | ||||||
| @@ -72,9 +72,9 @@ export class IndexingTaskManager implements IIndexingTaskManager { | |||||||
|     if (this.directoriesToIndex.length === 0 && this.enabled === false) { |     if (this.directoriesToIndex.length === 0 && this.enabled === false) { | ||||||
|       Logger.info(LOG_TAG, 'Starting indexing'); |       Logger.info(LOG_TAG, 'Starting indexing'); | ||||||
|       this.indexingProgress = { |       this.indexingProgress = { | ||||||
|         indexed: 0, |         progress: 0, | ||||||
|         left: 0, |         left: 0, | ||||||
|         current: '', |         comment: '', | ||||||
|         time: { |         time: { | ||||||
|           start: Date.now(), |           start: Date.now(), | ||||||
|           current: Date.now() |           current: Date.now() | ||||||
| @@ -88,7 +88,7 @@ export class IndexingTaskManager implements IIndexingTaskManager { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getProgress(): IndexingProgressDTO { |   getProgress(): TaskProgressDTO { | ||||||
|     return this.indexingProgress; |     return this.indexingProgress; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								backend/model/tasks/DBResetTask.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								backend/model/tasks/DBResetTask.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO'; | ||||||
|  | import {ObjectManagers} from '../ObjectManagers'; | ||||||
|  | import {Config} from '../../../common/config/private/Config'; | ||||||
|  | import {DatabaseType} from '../../../common/config/private/IPrivateConfig'; | ||||||
|  | import {ConfigTemplateEntry, DefaultsTasks} from '../../../common/entities/task/TaskDTO'; | ||||||
|  | import {Task} from './Task'; | ||||||
|  |  | ||||||
|  | const LOG_TAG = '[DBRestTask]'; | ||||||
|  |  | ||||||
|  | export class DBRestTask extends Task { | ||||||
|  |   public readonly Name = DefaultsTasks[DefaultsTasks['Database Reset']]; | ||||||
|  |   public readonly ConfigTemplate: ConfigTemplateEntry[] = null; | ||||||
|  |  | ||||||
|  |   public get Supported(): boolean { | ||||||
|  |     return Config.Server.database.type !== DatabaseType.memory; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected async init() { | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected async step(): Promise<TaskProgressDTO> { | ||||||
|  |     await ObjectManagers.getInstance().IndexingManager.resetDB(); | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								backend/model/tasks/DummyTask.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								backend/model/tasks/DummyTask.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO'; | ||||||
|  | import {ConfigTemplateEntry, DefaultsTasks} from '../../../common/entities/task/TaskDTO'; | ||||||
|  | import {Utils} from '../../../common/Utils'; | ||||||
|  | import {Task} from './Task'; | ||||||
|  |  | ||||||
|  | const LOG_TAG = '[DummyTask]'; | ||||||
|  |  | ||||||
|  | export class DummyTask extends Task { | ||||||
|  |   public readonly Name = DefaultsTasks[DefaultsTasks.Dummy]; | ||||||
|  |   counter = 0; | ||||||
|  |  | ||||||
|  |   public readonly ConfigTemplate: ConfigTemplateEntry[] = null; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   public get Supported(): boolean { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected async init() { | ||||||
|  |     this.counter = 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected async step(): Promise<TaskProgressDTO> { | ||||||
|  |     await Utils.wait(1000); | ||||||
|  |     if (!this.running) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     this.counter++; | ||||||
|  |     this.progress.progress = this.counter; | ||||||
|  |     this.progress.left = Math.pow(10, Math.floor(Math.log10(this.counter)) + 1) - this.counter; | ||||||
|  |     return this.progress; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								backend/model/tasks/ITask.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/model/tasks/ITask.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO'; | ||||||
|  | import {TaskDTO} from '../../../common/entities/task/TaskDTO'; | ||||||
|  |  | ||||||
|  | export interface ITask<T> extends TaskDTO { | ||||||
|  |   Name: string; | ||||||
|  |   Supported: boolean; | ||||||
|  |   Progress: TaskProgressDTO; | ||||||
|  |  | ||||||
|  |   start(config: T): void; | ||||||
|  |  | ||||||
|  |   stop(): void; | ||||||
|  | } | ||||||
							
								
								
									
										84
									
								
								backend/model/tasks/IndexingTask.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								backend/model/tasks/IndexingTask.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | |||||||
|  | import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO'; | ||||||
|  | import {ObjectManagers} from '../ObjectManagers'; | ||||||
|  | import * as path from 'path'; | ||||||
|  | import * as fs from 'fs'; | ||||||
|  | import {Logger} from '../../Logger'; | ||||||
|  | import {RendererInput, ThumbnailSourceType, ThumbnailWorker} from '../threading/ThumbnailWorker'; | ||||||
|  | import {Config} from '../../../common/config/private/Config'; | ||||||
|  | import {MediaDTO} from '../../../common/entities/MediaDTO'; | ||||||
|  | import {ProjectPath} from '../../ProjectPath'; | ||||||
|  | import {ThumbnailGeneratorMWs} from '../../middlewares/thumbnail/ThumbnailGeneratorMWs'; | ||||||
|  | import {Task} from './Task'; | ||||||
|  | import {DatabaseType} from '../../../common/config/private/IPrivateConfig'; | ||||||
|  | import {ConfigTemplateEntry, DefaultsTasks} from '../../../common/entities/task/TaskDTO'; | ||||||
|  |  | ||||||
|  | declare const global: any; | ||||||
|  | const LOG_TAG = '[IndexingTask]'; | ||||||
|  |  | ||||||
|  | export class IndexingTask extends Task<{ createThumbnails: boolean }> { | ||||||
|  |   public readonly Name = DefaultsTasks[DefaultsTasks.Indexing]; | ||||||
|  |   directoriesToIndex: string[] = []; | ||||||
|  |   public readonly ConfigTemplate: ConfigTemplateEntry[] = [{ | ||||||
|  |     id: 'createThumbnails', | ||||||
|  |     type: 'boolean', | ||||||
|  |     name: 'With thumbnails', | ||||||
|  |     defaultValue: false | ||||||
|  |   }]; | ||||||
|  |  | ||||||
|  |   public get Supported(): boolean { | ||||||
|  |     return Config.Server.database.type !== DatabaseType.memory; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected async init() { | ||||||
|  |     this.directoriesToIndex.push('/'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected async step(): Promise<TaskProgressDTO> { | ||||||
|  |     if (this.directoriesToIndex.length === 0) { | ||||||
|  |       if (global.gc) { | ||||||
|  |         global.gc(); | ||||||
|  |       } | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     const directory = this.directoriesToIndex.shift(); | ||||||
|  |     this.progress.comment = directory; | ||||||
|  |     this.progress.left = this.directoriesToIndex.length; | ||||||
|  |     const scanned = await ObjectManagers.getInstance().IndexingManager.indexDirectory(directory); | ||||||
|  |     if (this.running === false) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     this.progress.progress++; | ||||||
|  |     this.progress.time.current = Date.now(); | ||||||
|  |     for (let i = 0; i < scanned.directories.length; i++) { | ||||||
|  |       this.directoriesToIndex.push(path.join(scanned.directories[i].path, scanned.directories[i].name)); | ||||||
|  |     } | ||||||
|  |     if (this.config.createThumbnails) { | ||||||
|  |       for (let i = 0; i < scanned.media.length; i++) { | ||||||
|  |         try { | ||||||
|  |           const media = scanned.media[i]; | ||||||
|  |           const mPath = path.join(ProjectPath.ImageFolder, media.directory.path, media.directory.name, media.name); | ||||||
|  |           const thPath = path.join(ProjectPath.ThumbnailFolder, | ||||||
|  |             ThumbnailGeneratorMWs.generateThumbnailName(mPath, Config.Client.Thumbnail.thumbnailSizes[0])); | ||||||
|  |           if (fs.existsSync(thPath)) { // skip existing thumbnails | ||||||
|  |             continue; | ||||||
|  |           } | ||||||
|  |           await ThumbnailWorker.render(<RendererInput>{ | ||||||
|  |             type: MediaDTO.isVideo(media) ? ThumbnailSourceType.Video : ThumbnailSourceType.Image, | ||||||
|  |             mediaPath: mPath, | ||||||
|  |             size: Config.Client.Thumbnail.thumbnailSizes[0], | ||||||
|  |             thPath: thPath, | ||||||
|  |             makeSquare: false, | ||||||
|  |             qualityPriority: Config.Server.thumbnail.qualityPriority | ||||||
|  |           }, Config.Server.thumbnail.processingLibrary); | ||||||
|  |         } catch (e) { | ||||||
|  |           console.error(e); | ||||||
|  |           Logger.error(LOG_TAG, 'Error during indexing job: ' + e.toString()); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |     return this.progress; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										75
									
								
								backend/model/tasks/Task.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								backend/model/tasks/Task.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | |||||||
|  | import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO'; | ||||||
|  | import {Logger} from '../../Logger'; | ||||||
|  | import {ITask} from './ITask'; | ||||||
|  | import {ConfigTemplateEntry} from '../../../common/entities/task/TaskDTO'; | ||||||
|  |  | ||||||
|  | declare const process: any; | ||||||
|  |  | ||||||
|  | export abstract class Task<T = void> implements ITask<T> { | ||||||
|  |  | ||||||
|  |   protected progress: TaskProgressDTO; | ||||||
|  |   protected running = false; | ||||||
|  |   protected config: T; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   public abstract get Supported(): boolean; | ||||||
|  |  | ||||||
|  |   public abstract get Name(): string; | ||||||
|  |  | ||||||
|  |   public abstract get ConfigTemplate(): ConfigTemplateEntry[]; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   public get Progress(): TaskProgressDTO { | ||||||
|  |     return this.progress; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public start(config: T): void { | ||||||
|  |     if (this.running === false && this.Supported) { | ||||||
|  |       Logger.info('[Task]', 'running task:' + this.Name); | ||||||
|  |       this.config = config; | ||||||
|  |       this.progress = { | ||||||
|  |         progress: 0, | ||||||
|  |         left: 0, | ||||||
|  |         comment: '', | ||||||
|  |         time: { | ||||||
|  |           start: Date.now(), | ||||||
|  |           current: Date.now() | ||||||
|  |         } | ||||||
|  |       }; | ||||||
|  |       this.running = true; | ||||||
|  |       this.init().catch(console.error); | ||||||
|  |       this.run(); | ||||||
|  |     } else { | ||||||
|  |       Logger.info('[Task]', 'Task already running: ' + this.Name); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public stop(): void { | ||||||
|  |     Logger.info('[Task]', 'stopping task' + this.Name); | ||||||
|  |     this.progress = null; | ||||||
|  |     this.running = false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected abstract async step(): Promise<TaskProgressDTO>; | ||||||
|  |  | ||||||
|  |   protected abstract async init(): Promise<void>; | ||||||
|  |  | ||||||
|  |   private run() { | ||||||
|  |     process.nextTick(async () => { | ||||||
|  |       try { | ||||||
|  |         if (!this.running) { | ||||||
|  |           this.progress = null; | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         this.progress = await this.step(); | ||||||
|  |         if (this.progress == null) { // finished | ||||||
|  |           this.running = false; | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         this.run(); | ||||||
|  |       } catch (e) { | ||||||
|  |         Logger.error('[Task]', e); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								backend/model/tasks/TaskManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								backend/model/tasks/TaskManager.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | import {ITaskManager} from '../interfaces/ITaskManager'; | ||||||
|  | import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO'; | ||||||
|  | import {ITask} from './ITask'; | ||||||
|  | import {TaskRepository} from './TaskRepository'; | ||||||
|  | import {Config} from '../../../common/config/private/Config'; | ||||||
|  |  | ||||||
|  | export class TaskManager implements ITaskManager { | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   getProgresses(): { [id: string]: TaskProgressDTO } { | ||||||
|  |     const m: { [id: string]: TaskProgressDTO } = {}; | ||||||
|  |     TaskRepository.Instance.getAvailableTasks().forEach(t => m[t.Name] = t.Progress); | ||||||
|  |     return m; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   start(taskName: string, config: any): void { | ||||||
|  |     const t = this.findTask(taskName); | ||||||
|  |     if (t) { | ||||||
|  |       t.start(config); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   stop(taskName: string): void { | ||||||
|  |     const t = this.findTask(taskName); | ||||||
|  |     if (t) { | ||||||
|  |       t.stop(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getAvailableTasks(): ITask<any>[] { | ||||||
|  |     return TaskRepository.Instance.getAvailableTasks(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   protected findTask(taskName: string): ITask<any> { | ||||||
|  |     return this.getAvailableTasks().find(t => t.Name === taskName); | ||||||
|  |  | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										30
									
								
								backend/model/tasks/TaskRepository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								backend/model/tasks/TaskRepository.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | import {ITask} from './ITask'; | ||||||
|  | import {IndexingTask} from './IndexingTask'; | ||||||
|  | import {DBRestTask} from './DBResetTask'; | ||||||
|  | import {DummyTask} from './DummyTask'; | ||||||
|  |  | ||||||
|  | export class TaskRepository { | ||||||
|  |  | ||||||
|  |   private static instance: TaskRepository = null; | ||||||
|  |   availableTasks: { [key: string]: ITask<any> } = {}; | ||||||
|  |  | ||||||
|  |   public static get Instance(): TaskRepository { | ||||||
|  |     if (TaskRepository.instance == null) { | ||||||
|  |       TaskRepository.instance = new TaskRepository(); | ||||||
|  |     } | ||||||
|  |     return TaskRepository.instance; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getAvailableTasks(): ITask<any>[] { | ||||||
|  |     return Object.values(this.availableTasks).filter(t => t.Supported); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   register(task: ITask<any>) { | ||||||
|  |     this.availableTasks[task.Name] = task; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | TaskRepository.Instance.register(new IndexingTask()); | ||||||
|  | TaskRepository.Instance.register(new DBRestTask()); | ||||||
|  | TaskRepository.Instance.register(new DummyTask()); | ||||||
| @@ -9,6 +9,7 @@ export class AdminRouter { | |||||||
|  |  | ||||||
|     this.addGetStatistic(app); |     this.addGetStatistic(app); | ||||||
|     this.addGetDuplicates(app); |     this.addGetDuplicates(app); | ||||||
|  |     this.addTasks(app); | ||||||
|     this.addIndexGallery(app); |     this.addIndexGallery(app); | ||||||
|     this.addSettings(app); |     this.addSettings(app); | ||||||
|   } |   } | ||||||
| @@ -21,6 +22,7 @@ export class AdminRouter { | |||||||
|       RenderingMWs.renderResult |       RenderingMWs.renderResult | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private static addGetDuplicates(app: Express) { |   private static addGetDuplicates(app: Express) { | ||||||
|     app.get('/api/admin/duplicates', |     app.get('/api/admin/duplicates', | ||||||
|       AuthenticationMWs.authenticate, |       AuthenticationMWs.authenticate, | ||||||
| @@ -57,6 +59,33 @@ export class AdminRouter { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private static addTasks(app: Express) { | ||||||
|  |     app.get('/api/admin/tasks/available', | ||||||
|  |       AuthenticationMWs.authenticate, | ||||||
|  |       AuthenticationMWs.authorise(UserRoles.Admin), | ||||||
|  |       AdminMWs.getAvailableTasks, | ||||||
|  |       RenderingMWs.renderResult | ||||||
|  |     ); | ||||||
|  |     app.get('/api/admin/tasks/scheduled/progress', | ||||||
|  |       AuthenticationMWs.authenticate, | ||||||
|  |       AuthenticationMWs.authorise(UserRoles.Admin), | ||||||
|  |       AdminMWs.getTaskProgresses, | ||||||
|  |       RenderingMWs.renderResult | ||||||
|  |     ); | ||||||
|  |     app.post('/api/admin/tasks/scheduled/:id/start', | ||||||
|  |       AuthenticationMWs.authenticate, | ||||||
|  |       AuthenticationMWs.authorise(UserRoles.Admin), | ||||||
|  |       AdminMWs.startTask, | ||||||
|  |       RenderingMWs.renderResult | ||||||
|  |     ); | ||||||
|  |     app.post('/api/admin/tasks/scheduled/:id/stop', | ||||||
|  |       AuthenticationMWs.authenticate, | ||||||
|  |       AuthenticationMWs.authorise(UserRoles.Admin), | ||||||
|  |       AdminMWs.stopTask, | ||||||
|  |       RenderingMWs.renderResult | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private static addSettings(app: Express) { |   private static addSettings(app: Express) { | ||||||
|     app.get('/api/settings', |     app.get('/api/settings', | ||||||
|       AuthenticationMWs.authenticate, |       AuthenticationMWs.authenticate, | ||||||
|   | |||||||
| @@ -1,17 +0,0 @@ | |||||||
| { |  | ||||||
|   "compileOnSave": true, |  | ||||||
|   "compilerOptions": { |  | ||||||
|     "noImplicitAny": true, |  | ||||||
|     "module": "commonjs", |  | ||||||
|     "skipLibCheck": true, |  | ||||||
|     "sourceMap": false, |  | ||||||
|     "declaration": false, |  | ||||||
|     "moduleResolution": "node", |  | ||||||
|     "emitDecoratorMetadata": true, |  | ||||||
|     "experimentalDecorators": true, |  | ||||||
|     "target": "es6", |  | ||||||
|     "typeRoots": [ |  | ||||||
|       "../node_modules/@types" |  | ||||||
|     ] |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,4 +1,14 @@ | |||||||
| export class Utils { | export class Utils { | ||||||
|  |   static GUID() { | ||||||
|  |     const s4 = function () { | ||||||
|  |       return Math.floor((1 + Math.random()) * 0x10000) | ||||||
|  |         .toString(16) | ||||||
|  |         .substring(1); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     return s4() + s4() + '-' + s4() + s4(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   static chunkArrays<T>(arr: T[], chunkSize: number): T[][] { |   static chunkArrays<T>(arr: T[], chunkSize: number): T[][] { | ||||||
|     const R = []; |     const R = []; | ||||||
|     for (let i = 0; i < arr.length; i += chunkSize) { |     for (let i = 0; i < arr.length; i += chunkSize) { | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import {ClientConfig} from '../public/ConfigClass'; | import {ClientConfig} from '../public/ConfigClass'; | ||||||
|  | import {TaskScheduleDTO} from '../../entities/task/TaskScheduleDTO'; | ||||||
|  |  | ||||||
| export enum DatabaseType { | export enum DatabaseType { | ||||||
|   memory = 1, mysql = 2, sqlite = 3 |   memory = 1, mysql = 2, sqlite = 3 | ||||||
| @@ -70,6 +71,10 @@ export interface LogConfig { | |||||||
|   sqlLevel: SQLLogLevel; |   sqlLevel: SQLLogLevel; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export interface TaskConfig { | ||||||
|  |   scheduled: TaskScheduleDTO[]; | ||||||
|  | } | ||||||
|  |  | ||||||
| export interface ServerConfig { | export interface ServerConfig { | ||||||
|   port: number; |   port: number; | ||||||
|   host: string; |   host: string; | ||||||
| @@ -83,6 +88,7 @@ export interface ServerConfig { | |||||||
|   photoMetadataSize: number; |   photoMetadataSize: number; | ||||||
|   duplicates: DuplicatesConfig; |   duplicates: DuplicatesConfig; | ||||||
|   log: LogConfig; |   log: LogConfig; | ||||||
|  |   tasks: TaskConfig; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface IPrivateConfig { | export interface IPrivateConfig { | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ import * as path from 'path'; | |||||||
| import {ConfigLoader} from 'typeconfig'; | import {ConfigLoader} from 'typeconfig'; | ||||||
| import {Utils} from '../../Utils'; | import {Utils} from '../../Utils'; | ||||||
| import {UserRoles} from '../../entities/UserDTO'; | import {UserRoles} from '../../entities/UserDTO'; | ||||||
|  | import {TaskScheduleDTO, TaskTriggerType} from '../../entities/task/TaskScheduleDTO'; | ||||||
|  | import {Config} from './Config'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * This configuration will be only at backend |  * This configuration will be only at backend | ||||||
| @@ -61,6 +63,30 @@ export class PrivateConfigClass extends PublicConfigClass implements IPrivateCon | |||||||
|     }, |     }, | ||||||
|     duplicates: { |     duplicates: { | ||||||
|       listingLimit: 1000 |       listingLimit: 1000 | ||||||
|  |     }, | ||||||
|  |     tasks: { | ||||||
|  |       scheduled: [ | ||||||
|  |         { | ||||||
|  |           priority: 1, | ||||||
|  |           taskName: 'indexing', | ||||||
|  |           config: null, | ||||||
|  |           trigger: { | ||||||
|  |             type: TaskTriggerType.periodic, | ||||||
|  |             time: { | ||||||
|  |               offset: 0, | ||||||
|  |               repeat: 10 | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           priority: 2, | ||||||
|  |           taskName: 'Database reset', | ||||||
|  |           config: null, | ||||||
|  |           trigger: { | ||||||
|  |             type: TaskTriggerType.never | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|   private ConfigLoader: any; |   private ConfigLoader: any; | ||||||
| @@ -94,6 +120,20 @@ export class PrivateConfigClass extends PublicConfigClass implements IPrivateCon | |||||||
|       throw new Error('Unknown Server.log.level, found: ' + this.Server.log.sqlLevel); |       throw new Error('Unknown Server.log.level, found: ' + this.Server.log.sqlLevel); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     let updated = false; | ||||||
|  |     Config.Server.tasks.scheduled.forEach((task: TaskScheduleDTO, i: number) => { | ||||||
|  |       if (!task.id) { | ||||||
|  |         task.id = Utils.GUID(); | ||||||
|  |         updated = true; | ||||||
|  |       } | ||||||
|  |       if (!task.name) { | ||||||
|  |         task.name = task.taskName; | ||||||
|  |         updated = true; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     if (updated) { | ||||||
|  |       this.save(); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public save() { |   public save() { | ||||||
|   | |||||||
| @@ -18,7 +18,8 @@ export enum ErrorCodes { | |||||||
|  |  | ||||||
|   INPUT_ERROR = 12, |   INPUT_ERROR = 12, | ||||||
|  |  | ||||||
|   SETTINGS_ERROR = 13 |   SETTINGS_ERROR = 13, | ||||||
|  |   TASK_ERROR = 14 | ||||||
| } | } | ||||||
|  |  | ||||||
| export class ErrorDTO { | export class ErrorDTO { | ||||||
|   | |||||||
| @@ -1,28 +0,0 @@ | |||||||
| export interface TaskType { |  | ||||||
|   name: string; |  | ||||||
|   parameter: any; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export enum TaskTriggerType { |  | ||||||
|   scheduled, periodic |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface TaskTrigger { |  | ||||||
|   type: TaskTriggerType; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface ScheduledTaskTrigger extends TaskTrigger { |  | ||||||
|   type: TaskTriggerType.scheduled; |  | ||||||
|   time: number; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface PeriodicTaskTrigger extends TaskTrigger { |  | ||||||
|   type: TaskTriggerType.periodic; |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface TaskDTO { |  | ||||||
|   priority: number; |  | ||||||
|   type: TaskType; |  | ||||||
|   trigger: TaskTrigger; |  | ||||||
| } |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| export interface IndexingProgressDTO { |  | ||||||
|   indexed: number; |  | ||||||
|   left: number; |  | ||||||
|   current: string; |  | ||||||
|   time: { |  | ||||||
|     start: number, |  | ||||||
|     current: number |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
							
								
								
									
										9
									
								
								common/entities/settings/TaskProgressDTO.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								common/entities/settings/TaskProgressDTO.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | export interface TaskProgressDTO { | ||||||
|  |   progress: number; | ||||||
|  |   left: number; | ||||||
|  |   comment: string; | ||||||
|  |   time: { | ||||||
|  |     start: number, | ||||||
|  |     current: number | ||||||
|  |   }; | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								common/entities/task/TaskDTO.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								common/entities/task/TaskDTO.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | export  type fieldType = 'string' | 'number' | 'boolean'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | export enum DefaultsTasks { | ||||||
|  |   Indexing, 'Database Reset', Dummy | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface ConfigTemplateEntry { | ||||||
|  |   id: string; | ||||||
|  |   name: string; | ||||||
|  |   type: fieldType; | ||||||
|  |   defaultValue: any; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* | ||||||
|  | export interface NestedFieldType { | ||||||
|  |   [key: string]: fieldType | NestedFieldType; | ||||||
|  | }*/ | ||||||
|  |  | ||||||
|  | export interface TaskDTO { | ||||||
|  |   Name: string; | ||||||
|  |   ConfigTemplate: ConfigTemplateEntry[]; | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								common/entities/task/TaskScheduleDTO.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								common/entities/task/TaskScheduleDTO.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | export enum TaskTriggerType { | ||||||
|  |   never = 1, scheduled = 2, periodic = 3 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface TaskTrigger { | ||||||
|  |   type: TaskTriggerType; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface NeverTaskTrigger { | ||||||
|  |   type: TaskTriggerType.never; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface ScheduledTaskTrigger extends TaskTrigger { | ||||||
|  |   type: TaskTriggerType.scheduled; | ||||||
|  |   time: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface PeriodicTaskTrigger extends TaskTrigger { | ||||||
|  |   type: TaskTriggerType.periodic; | ||||||
|  |   time: { | ||||||
|  |     offset: number, | ||||||
|  |     repeat: number | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface TaskScheduleDTO { | ||||||
|  |   priority: number; | ||||||
|  |   name?: string; | ||||||
|  |   id?: string; | ||||||
|  |   taskName: string; | ||||||
|  |   config: any; | ||||||
|  |   trigger: NeverTaskTrigger | ScheduledTaskTrigger | PeriodicTaskTrigger; | ||||||
|  | } | ||||||
| @@ -78,9 +78,11 @@ import {FacesComponent} from './ui/faces/faces.component'; | |||||||
| import {FacesService} from './ui/faces/faces.service'; | import {FacesService} from './ui/faces/faces.service'; | ||||||
| import {FaceComponent} from './ui/faces/face/face.component'; | import {FaceComponent} from './ui/faces/face/face.component'; | ||||||
| import {VersionService} from './model/version.service'; | import {VersionService} from './model/version.service'; | ||||||
| import { DirectoriesComponent } from './ui/gallery/directories/directories.component'; | import {DirectoriesComponent} from './ui/gallery/directories/directories.component'; | ||||||
| import {ControlsLightboxComponent} from './ui/gallery/lightbox/controls/controls.lightbox.gallery.component'; | import {ControlsLightboxComponent} from './ui/gallery/lightbox/controls/controls.lightbox.gallery.component'; | ||||||
| import {FacesSettingsComponent} from './ui/settings/faces/faces.settings.component'; | import {FacesSettingsComponent} from './ui/settings/faces/faces.settings.component'; | ||||||
|  | import {TasksSettingsComponent} from './ui/settings/tasks/tasks.settings.component'; | ||||||
|  | import {ScheduledTasksService} from './ui/settings/scheduled-tasks.service'; | ||||||
|  |  | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| @@ -166,8 +168,12 @@ export function translationsFactory(locale: string) { | |||||||
|     InfoPanelLightboxComponent, |     InfoPanelLightboxComponent, | ||||||
|     ControlsLightboxComponent, |     ControlsLightboxComponent, | ||||||
|     RandomQueryBuilderGalleryComponent, |     RandomQueryBuilderGalleryComponent, | ||||||
|  |     DirectoriesComponent, | ||||||
|     // Face |     // Face | ||||||
|     FaceComponent, |     FaceComponent, | ||||||
|  |     // Duplicates | ||||||
|  |     DuplicateComponent, | ||||||
|  |     DuplicatesPhotoComponent, | ||||||
|     // Settings |     // Settings | ||||||
|     UserMangerSettingsComponent, |     UserMangerSettingsComponent, | ||||||
|     DatabaseSettingsComponent, |     DatabaseSettingsComponent, | ||||||
| @@ -182,15 +188,14 @@ export function translationsFactory(locale: string) { | |||||||
|     FacesSettingsComponent, |     FacesSettingsComponent, | ||||||
|     OtherSettingsComponent, |     OtherSettingsComponent, | ||||||
|     IndexingSettingsComponent, |     IndexingSettingsComponent, | ||||||
|     DuplicateComponent, |     TasksSettingsComponent, | ||||||
|     DuplicatesPhotoComponent, |     // Pipes | ||||||
|     StringifyRole, |     StringifyRole, | ||||||
|     IconizeSortingMethod, |     IconizeSortingMethod, | ||||||
|     StringifySortingMethod, |     StringifySortingMethod, | ||||||
|     FixOrientationPipe, |     FixOrientationPipe, | ||||||
|     DurationPipe, |     DurationPipe, | ||||||
|     FileSizePipe, |     FileSizePipe | ||||||
|     DirectoriesComponent |  | ||||||
|   ], |   ], | ||||||
|   providers: [ |   providers: [ | ||||||
|     {provide: UrlSerializer, useClass: CustomUrlSerializer}, |     {provide: UrlSerializer, useClass: CustomUrlSerializer}, | ||||||
| @@ -214,6 +219,7 @@ export function translationsFactory(locale: string) { | |||||||
|     DuplicateService, |     DuplicateService, | ||||||
|     FacesService, |     FacesService, | ||||||
|     VersionService, |     VersionService, | ||||||
|  |     ScheduledTasksService, | ||||||
|     { |     { | ||||||
|       provide: TRANSLATIONS, |       provide: TRANSLATIONS, | ||||||
|       useFactory: translationsFactory, |       useFactory: translationsFactory, | ||||||
|   | |||||||
| @@ -66,5 +66,7 @@ | |||||||
|                                [simplifiedMode]="simplifiedMode"></app-settings-faces> |                                [simplifiedMode]="simplifiedMode"></app-settings-faces> | ||||||
|     <app-settings-indexing #indexing [hidden]="!indexing.hasAvailableSettings" |     <app-settings-indexing #indexing [hidden]="!indexing.hasAvailableSettings" | ||||||
|                            [simplifiedMode]="simplifiedMode"></app-settings-indexing> |                            [simplifiedMode]="simplifiedMode"></app-settings-indexing> | ||||||
|  |     <app-settings-tasks #tasks [hidden]="!tasks.hasAvailableSettings" | ||||||
|  |                            [simplifiedMode]="simplifiedMode"></app-settings-tasks> | ||||||
|   </div> |   </div> | ||||||
| </app-frame> | </app-frame> | ||||||
|   | |||||||
| @@ -69,8 +69,8 @@ | |||||||
|       )<br/> |       )<br/> | ||||||
|  |  | ||||||
|  |  | ||||||
|       <div *ngIf="_settingsService.progress.value != null"> |       <div *ngIf="Progress != null"> | ||||||
|         <span class="progress-details" i18n>indexing</span>: {{_settingsService.progress.value.current}} <br/> |         <span class="progress-details" i18n>indexing</span>: {{Progress.comment}} <br/> | ||||||
|         <span class="progress-details" i18n>elapsed</span>: {{TimeElapsed | duration}}<br/> |         <span class="progress-details" i18n>elapsed</span>: {{TimeElapsed | duration}}<br/> | ||||||
|         <span class="progress-details" i18n>left</span>: {{TimeLeft | duration}} |         <span class="progress-details" i18n>left</span>: {{TimeLeft | duration}} | ||||||
|         <div class="progress"> |         <div class="progress"> | ||||||
| @@ -80,16 +80,16 @@ | |||||||
|                aria-valuemin="0" |                aria-valuemin="0" | ||||||
|                aria-valuemax="100" |                aria-valuemax="100" | ||||||
|                style="min-width: 2em;" |                style="min-width: 2em;" | ||||||
|                [style.width.%]="(_settingsService.progress.value.indexed/(_settingsService.progress.value.left+_settingsService.progress.value.indexed))*100"> |                [style.width.%]="(Progress.progress/(Progress.left+Progress.progress))*100"> | ||||||
|             {{_settingsService.progress.value.indexed}} |             {{Progress.progress}} | ||||||
|             /{{_settingsService.progress.value.indexed + _settingsService.progress.value.left}} |             /{{Progress.progress + Progress.left}} | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div class="row justify-content-center buttons-row"> |       <div class="row justify-content-center buttons-row"> | ||||||
|         <button class="btn btn-success" |         <button class="btn btn-success" | ||||||
|                 *ngIf="_settingsService.progress.value == null" |                 *ngIf="Progress == null" | ||||||
|                 [disabled]="inProgress" |                 [disabled]="inProgress" | ||||||
|                 title="Indexes the folders" |                 title="Indexes the folders" | ||||||
|                 i18n-title |                 i18n-title | ||||||
| @@ -98,12 +98,12 @@ | |||||||
|         <button class="btn btn-primary" |         <button class="btn btn-primary" | ||||||
|                 title="Indexes the folders and also creates the thumbnails" |                 title="Indexes the folders and also creates the thumbnails" | ||||||
|                 i18n-title |                 i18n-title | ||||||
|                 *ngIf="_settingsService.progress.value == null" |                 *ngIf="Progress == null" | ||||||
|                 [disabled]="inProgress" |                 [disabled]="inProgress" | ||||||
|                 (click)="index(true)" i18n>Index with Thumbnails |                 (click)="index(true)" i18n>Index with Thumbnails | ||||||
|         </button> |         </button> | ||||||
|         <button class="btn btn-secondary" |         <button class="btn btn-secondary" | ||||||
|                 *ngIf="_settingsService.progress.value != null" |                 *ngIf="Progress != null" | ||||||
|                 [disabled]="inProgress" |                 [disabled]="inProgress" | ||||||
|                 (click)="cancelIndexing()" i18n>Cancel |                 (click)="cancelIndexing()" i18n>Cancel | ||||||
|         </button> |         </button> | ||||||
|   | |||||||
| @@ -4,12 +4,13 @@ import {AuthenticationService} from '../../../model/network/authentication.servi | |||||||
| import {NavigationService} from '../../../model/navigation.service'; | import {NavigationService} from '../../../model/navigation.service'; | ||||||
| import {NotificationService} from '../../../model/notification.service'; | import {NotificationService} from '../../../model/notification.service'; | ||||||
| import {ErrorDTO} from '../../../../../common/entities/Error'; | import {ErrorDTO} from '../../../../../common/entities/Error'; | ||||||
| import {interval, Observable} from 'rxjs'; |  | ||||||
| import {IndexingConfig, ReIndexingSensitivity} from '../../../../../common/config/private/IPrivateConfig'; | import {IndexingConfig, ReIndexingSensitivity} from '../../../../../common/config/private/IPrivateConfig'; | ||||||
| import {SettingsComponent} from '../_abstract/abstract.settings.component'; | import {SettingsComponent} from '../_abstract/abstract.settings.component'; | ||||||
| import {Utils} from '../../../../../common/Utils'; | import {Utils} from '../../../../../common/Utils'; | ||||||
| import {I18n} from '@ngx-translate/i18n-polyfill'; | import {I18n} from '@ngx-translate/i18n-polyfill'; | ||||||
| import {StatisticDTO} from '../../../../../common/entities/settings/StatisticDTO'; | import {StatisticDTO} from '../../../../../common/entities/settings/StatisticDTO'; | ||||||
|  | import {ScheduledTasksService} from '../scheduled-tasks.service'; | ||||||
|  | import {DefaultsTasks} from '../../../../../common/entities/task/TaskDTO'; | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-settings-indexing', |   selector: 'app-settings-indexing', | ||||||
| @@ -24,15 +25,11 @@ export class IndexingSettingsComponent extends SettingsComponent<IndexingConfig, | |||||||
|  |  | ||||||
|   types: { key: number; value: string }[] = []; |   types: { key: number; value: string }[] = []; | ||||||
|   statistic: StatisticDTO; |   statistic: StatisticDTO; | ||||||
|   private subscription: { timer: any, settings: any } = { |  | ||||||
|     timer: null, |  | ||||||
|     settings: null |  | ||||||
|   }; |  | ||||||
|   private $counter: Observable<number> = null; |  | ||||||
|  |  | ||||||
|   constructor(_authService: AuthenticationService, |   constructor(_authService: AuthenticationService, | ||||||
|               _navigation: NavigationService, |               _navigation: NavigationService, | ||||||
|               _settingsService: IndexingSettingsService, |               _settingsService: IndexingSettingsService, | ||||||
|  |               public tasksService: ScheduledTasksService, | ||||||
|               notification: NotificationService, |               notification: NotificationService, | ||||||
|               i18n: I18n) { |               i18n: I18n) { | ||||||
|  |  | ||||||
| @@ -46,43 +43,30 @@ export class IndexingSettingsComponent extends SettingsComponent<IndexingConfig, | |||||||
|  |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get TimeLeft() { |   get Progress() { | ||||||
|     const prg = this._settingsService.progress.value; |     return this.tasksService.progress.value[DefaultsTasks[DefaultsTasks.Indexing]]; | ||||||
|     return (prg.time.current - prg.time.start) / prg.indexed * prg.left; |   } | ||||||
|  |  | ||||||
|  |   get TimeLeft(): number { | ||||||
|  |     if (this.Progress) { | ||||||
|  |       return (this.Progress.time.current - this.Progress.time.start) / this.Progress.progress * this.Progress.left; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get TimeElapsed() { |   get TimeElapsed() { | ||||||
|     const prg = this._settingsService.progress.value; |     if (this.Progress) { | ||||||
|     return (prg.time.current - prg.time.start); |       return (this.Progress.time.current - this.Progress.time.start); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   updateProgress = async () => { |   ngOnDestroy() { | ||||||
|     try { |     super.ngOnDestroy(); | ||||||
|       const wasRunning = this._settingsService.progress.value !== null; |     this.tasksService.unsubscribeFromProgress(); | ||||||
|       await (<IndexingSettingsService>this._settingsService).getProgress(); |   } | ||||||
|       if (wasRunning && this._settingsService.progress.value === null) { |  | ||||||
|         this.notification.success(this.i18n('Folder indexed'), this.i18n('Success')); |  | ||||||
|       } |  | ||||||
|     } catch (err) { |  | ||||||
|       if (this.subscription.timer != null) { |  | ||||||
|         this.subscription.timer.unsubscribe(); |  | ||||||
|         this.subscription.timer = null; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     if ((<IndexingSettingsService>this._settingsService).progress.value != null && this.subscription.timer == null) { |  | ||||||
|       if (!this.$counter) { |  | ||||||
|         this.$counter = interval(5000); |  | ||||||
|       } |  | ||||||
|       this.subscription.timer = this.$counter.subscribe((x) => this.updateProgress()); |  | ||||||
|     } |  | ||||||
|     if ((<IndexingSettingsService>this._settingsService).progress.value == null && this.subscription.timer != null) { |  | ||||||
|       this.subscription.timer.unsubscribe(); |  | ||||||
|       this.subscription.timer = null; |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   async ngOnInit() { |   async ngOnInit() { | ||||||
|     super.ngOnInit(); |     super.ngOnInit(); | ||||||
|  |     this.tasksService.subscribeToProgress(); | ||||||
|     this.types = Utils |     this.types = Utils | ||||||
|       .enumToArray(ReIndexingSensitivity); |       .enumToArray(ReIndexingSensitivity); | ||||||
|     this.types.forEach(v => { |     this.types.forEach(v => { | ||||||
| @@ -98,30 +82,18 @@ export class IndexingSettingsComponent extends SettingsComponent<IndexingConfig, | |||||||
|           break; |           break; | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|     this.updateProgress(); |  | ||||||
|     if (this._settingsService.isSupported()) { |     if (this._settingsService.isSupported()) { | ||||||
|       this.statistic = await this._settingsService.getStatistic(); |       this.statistic = await this._settingsService.getStatistic(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   ngOnDestroy() { |  | ||||||
|     super.ngOnDestroy(); |  | ||||||
|     if (this.subscription.timer != null) { |  | ||||||
|       this.subscription.timer.unsubscribe(); |  | ||||||
|       this.subscription.timer = null; |  | ||||||
|     } |  | ||||||
|     if (this.subscription.settings != null) { |  | ||||||
|       this.subscription.settings.unsubscribe(); |  | ||||||
|       this.subscription.settings = null; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async index(createThumbnails: boolean) { |   async index(createThumbnails: boolean) { | ||||||
|     this.inProgress = true; |     this.inProgress = true; | ||||||
|     this.error = ''; |     this.error = ''; | ||||||
|     try { |     try { | ||||||
|       await this._settingsService.index(createThumbnails); |       await this.tasksService.start(DefaultsTasks[DefaultsTasks.Indexing], {createThumbnails: !!createThumbnails}); | ||||||
|       this.updateProgress(); |       await this.tasksService.forceUpdate(); | ||||||
|       this.notification.info(this.i18n('Folder indexing started')); |       this.notification.info(this.i18n('Folder indexing started')); | ||||||
|       this.inProgress = false; |       this.inProgress = false; | ||||||
|       return true; |       return true; | ||||||
| @@ -140,8 +112,8 @@ export class IndexingSettingsComponent extends SettingsComponent<IndexingConfig, | |||||||
|     this.inProgress = true; |     this.inProgress = true; | ||||||
|     this.error = ''; |     this.error = ''; | ||||||
|     try { |     try { | ||||||
|       await (<IndexingSettingsService>this._settingsService).cancel(); |       await this.tasksService.stop(DefaultsTasks[DefaultsTasks.Indexing]); | ||||||
|       this._settingsService.progress.next(null); |       await this.tasksService.forceUpdate(); | ||||||
|       this.notification.info(this.i18n('Folder indexing interrupted')); |       this.notification.info(this.i18n('Folder indexing interrupted')); | ||||||
|       this.inProgress = false; |       this.inProgress = false; | ||||||
|       return true; |       return true; | ||||||
| @@ -160,7 +132,8 @@ export class IndexingSettingsComponent extends SettingsComponent<IndexingConfig, | |||||||
|     this.inProgress = true; |     this.inProgress = true; | ||||||
|     this.error = ''; |     this.error = ''; | ||||||
|     try { |     try { | ||||||
|       await (<IndexingSettingsService>this._settingsService).reset(); |       await this.tasksService.start(DefaultsTasks[DefaultsTasks['Database Reset']]); | ||||||
|  |       await this.tasksService.forceUpdate(); | ||||||
|       this.notification.success(this.i18n('Database reset'), this.i18n('Success')); |       this.notification.success(this.i18n('Database reset'), this.i18n('Success')); | ||||||
|       this.inProgress = false; |       this.inProgress = false; | ||||||
|       return true; |       return true; | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import {NetworkService} from '../../../model/network/network.service'; | |||||||
| import {SettingsService} from '../settings.service'; | import {SettingsService} from '../settings.service'; | ||||||
| import {AbstractSettingsService} from '../_abstract/abstract.settings.service'; | import {AbstractSettingsService} from '../_abstract/abstract.settings.service'; | ||||||
| import {DatabaseType, IndexingConfig} from '../../../../../common/config/private/IPrivateConfig'; | import {DatabaseType, IndexingConfig} from '../../../../../common/config/private/IPrivateConfig'; | ||||||
| import {IndexingProgressDTO} from '../../../../../common/entities/settings/IndexingProgressDTO'; | import {TaskProgressDTO} from '../../../../../common/entities/settings/TaskProgressDTO'; | ||||||
| import {BehaviorSubject} from 'rxjs'; | import {BehaviorSubject} from 'rxjs'; | ||||||
| import {IndexingDTO} from '../../../../../common/entities/settings/IndexingDTO'; | import {IndexingDTO} from '../../../../../common/entities/settings/IndexingDTO'; | ||||||
| import {StatisticDTO} from '../../../../../common/entities/settings/StatisticDTO'; | import {StatisticDTO} from '../../../../../common/entities/settings/StatisticDTO'; | ||||||
| @@ -12,12 +12,10 @@ import {StatisticDTO} from '../../../../../common/entities/settings/StatisticDTO | |||||||
| export class IndexingSettingsService extends AbstractSettingsService<IndexingConfig> { | export class IndexingSettingsService extends AbstractSettingsService<IndexingConfig> { | ||||||
|  |  | ||||||
|  |  | ||||||
|   public progress: BehaviorSubject<IndexingProgressDTO>; |  | ||||||
|  |  | ||||||
|   constructor(private _networkService: NetworkService, |   constructor(private _networkService: NetworkService, | ||||||
|               _settingsService: SettingsService) { |               _settingsService: SettingsService) { | ||||||
|     super(_settingsService); |     super(_settingsService); | ||||||
|     this.progress = new BehaviorSubject(null); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public updateSettings(settings: IndexingConfig): Promise<void> { |   public updateSettings(settings: IndexingConfig): Promise<void> { | ||||||
| @@ -29,22 +27,6 @@ export class IndexingSettingsService extends AbstractSettingsService<IndexingCon | |||||||
|     return this._settingsService.settings.value.Server.database.type !== DatabaseType.memory; |     return this._settingsService.settings.value.Server.database.type !== DatabaseType.memory; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public index(createThumbnails: boolean) { |  | ||||||
|     return this._networkService.postJson('/admin/indexes/job', <IndexingDTO>{createThumbnails: createThumbnails}); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public cancel() { |  | ||||||
|     return this._networkService.deleteJson('/admin/indexes/job'); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public async getProgress() { |  | ||||||
|     this.progress.next(await this._networkService.getJson<IndexingProgressDTO>('/admin/indexes/job/progress')); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public reset() { |  | ||||||
|     return this._networkService.deleteJson('/admin/indexes'); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   getStatistic() { |   getStatistic() { | ||||||
|     return this._networkService.getJson<StatisticDTO>('/admin/statistic'); |     return this._networkService.getJson<StatisticDTO>('/admin/statistic'); | ||||||
|   | |||||||
							
								
								
									
										63
									
								
								frontend/app/ui/settings/scheduled-tasks.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								frontend/app/ui/settings/scheduled-tasks.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | import {Injectable} from '@angular/core'; | ||||||
|  | import {BehaviorSubject} from 'rxjs'; | ||||||
|  | import {TaskProgressDTO} from '../../../../common/entities/settings/TaskProgressDTO'; | ||||||
|  | import {NetworkService} from '../../model/network/network.service'; | ||||||
|  |  | ||||||
|  | @Injectable() | ||||||
|  | export class ScheduledTasksService { | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   public progress: BehaviorSubject<{ [key: string]: TaskProgressDTO }>; | ||||||
|  |   timer: number = null; | ||||||
|  |   private subscribers = 0; | ||||||
|  |  | ||||||
|  |   constructor(private _networkService: NetworkService) { | ||||||
|  |     this.progress = new BehaviorSubject({}); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   subscribeToProgress(): void { | ||||||
|  |     this.incSubscribers(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   unsubscribeFromProgress(): void { | ||||||
|  |     this.decSubscribers(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public forceUpdate() { | ||||||
|  |     return this.getProgress(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async start(id: string, config?: any) { | ||||||
|  |     return await this._networkService.postJson('/admin/tasks/scheduled/' + id + '/start', {config: config}); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async stop(id: string) { | ||||||
|  |     return await this._networkService.postJson('/admin/tasks/scheduled/' + id + '/stop'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected async getProgress() { | ||||||
|  |     return this.progress.next(await this._networkService.getJson<{ [key: string]: TaskProgressDTO }>('/admin/tasks/scheduled/progress')); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected getProgressPeriodically() { | ||||||
|  |     if (this.timer != null || this.subscribers === 0) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this.timer = window.setTimeout(async () => { | ||||||
|  |       await this.getProgress(); | ||||||
|  |       this.timer = null; | ||||||
|  |       this.getProgressPeriodically(); | ||||||
|  |     }, 5000); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private incSubscribers() { | ||||||
|  |     this.subscribers++; | ||||||
|  |     this.getProgressPeriodically(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private decSubscribers() { | ||||||
|  |     this.subscribers--; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -114,6 +114,9 @@ export class SettingsService { | |||||||
|         photoMetadataSize: 512 * 1024, |         photoMetadataSize: 512 * 1024, | ||||||
|         duplicates: { |         duplicates: { | ||||||
|           listingLimit: 1000 |           listingLimit: 1000 | ||||||
|  |         }, | ||||||
|  |         tasks: { | ||||||
|  |           scheduled: [] | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								frontend/app/ui/settings/tasks/tasks.settings.component.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/app/ui/settings/tasks/tasks.settings.component.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  |  | ||||||
|  | .progress-details { | ||||||
|  |   font-weight: bold; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .card { | ||||||
|  |   margin-bottom: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .button-delete { | ||||||
|  |   margin-top: -5px; | ||||||
|  |   margin-bottom: -5px; | ||||||
|  | } | ||||||
							
								
								
									
										117
									
								
								frontend/app/ui/settings/tasks/tasks.settings.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								frontend/app/ui/settings/tasks/tasks.settings.component.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | |||||||
|  | <form #settingsForm="ngForm" class="form-horizontal"> | ||||||
|  |   <div class="card mb-4"> | ||||||
|  |     <h5 class="card-header" i18n> | ||||||
|  |       Tasks | ||||||
|  |     </h5> | ||||||
|  |     <div class="card-body"> | ||||||
|  |       <div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div> | ||||||
|  |  | ||||||
|  |       <div *ngFor="let schedule of settings.scheduled;let i= index"> | ||||||
|  |         <div class="card bg-light"> | ||||||
|  |           <div class="card-header "> | ||||||
|  |             <div class="d-flex justify-content-between"> | ||||||
|  |               {{schedule.name}} | ||||||
|  |               <button class="btn btn-danger button-delete" | ||||||
|  |                       (click)="remove(schedule.taskName)" i18n><span class="oi oi-trash"></span> | ||||||
|  |               </button> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |           <div class="card-body"> | ||||||
|  |             <div class="d-flex justify-content-between"> | ||||||
|  |               <div> | ||||||
|  |                 <select class="form-control" [(ngModel)]="schedule.taskName" [name]="'taskName'+i" required> | ||||||
|  |                   <option *ngFor="let availableTask of _settingsService.availableTasks | async" | ||||||
|  |                           [ngValue]="availableTask.Name">{{availableTask.Name}} | ||||||
|  |                   </option> | ||||||
|  |                 </select> | ||||||
|  |               </div> | ||||||
|  |               <div> | ||||||
|  |                 <button class="btn btn-success" | ||||||
|  |                         *ngIf="!tasksService.progress.value[schedule.taskName]" | ||||||
|  |                         [disabled]="disableButtons" | ||||||
|  |                         title="Trigger task run manually" | ||||||
|  |                         i18n-title | ||||||
|  |                         (click)="start(schedule)" i18n>Start | ||||||
|  |                 </button> | ||||||
|  |                 <button class="btn btn-secondary" | ||||||
|  |                         *ngIf="tasksService.progress.value[schedule.taskName]" | ||||||
|  |                         [disabled]="disableButtons" | ||||||
|  |                         (click)="stop(schedule)" i18n>Stop | ||||||
|  |                 </button> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|  |             <ng-container *ngIf="getConfigTemplate(schedule.taskName) "> | ||||||
|  |               <hr/> | ||||||
|  |               <div *ngFor="let configEntry of  getConfigTemplate(schedule.taskName)"> | ||||||
|  |                 <ng-container [ngSwitch]="configEntry.type"> | ||||||
|  |                   <ng-container *ngSwitchCase="'boolean'"> | ||||||
|  |                     <div class="form-group row"> | ||||||
|  |                       <label class="col-md-2 control-label" [for]="configEntry.id" | ||||||
|  |                              i18n>{{configEntry.name}}</label> | ||||||
|  |                       <div class="col-md-10"> | ||||||
|  |                         <bSwitch | ||||||
|  |                           id="enableThreading" | ||||||
|  |                           class="switch" | ||||||
|  |                           [name]="configEntry.id" | ||||||
|  |                           [id]="configEntry.id" | ||||||
|  |                           [switch-on-color]="'primary'" | ||||||
|  |                           [switch-inverse]="true" | ||||||
|  |                           [switch-off-text]="text.Disabled" | ||||||
|  |                           [switch-on-text]="text.Enabled" | ||||||
|  |                           [switch-handle-width]="100" | ||||||
|  |                           [switch-label-width]="20" | ||||||
|  |                           [(ngModel)]="schedule.config[configEntry.id]"> | ||||||
|  |                         </bSwitch> | ||||||
|  |                       </div> | ||||||
|  |                     </div> | ||||||
|  |                   </ng-container> | ||||||
|  |                   <ng-container *ngSwitchCase="'string'"> | ||||||
|  |                     <div class="form-group row" [hidden]="simplifiedMode"> | ||||||
|  |                       <label class="col-md-2 control-label" [for]="configEntry.id" | ||||||
|  |                              i18n>{{configEntry.name}}</label> | ||||||
|  |                       <div class="col-md-10"> | ||||||
|  |                         <input type="text" class="form-control" [name]="configEntry.id" [id]="configEntry.id" | ||||||
|  |                                [(ngModel)]="schedule.config[configEntry.id]" required> | ||||||
|  |                       </div> | ||||||
|  |                     </div> | ||||||
|  |                   </ng-container> | ||||||
|  |                   <ng-container *ngSwitchCase="'number'"> | ||||||
|  |                     <div class="form-group row" [hidden]="simplifiedMode"> | ||||||
|  |                       <label class="col-md-2 control-label" [for]="configEntry.id" | ||||||
|  |                              i18n>{{configEntry.name}}</label> | ||||||
|  |                       <div class="col-md-10"> | ||||||
|  |                         <input type="number" class="form-control" [name]="configEntry.id" [id]="configEntry.id" | ||||||
|  |                                [(ngModel)]="schedule.config[configEntry.id]" required> | ||||||
|  |                       </div> | ||||||
|  |                     </div> | ||||||
|  |                   </ng-container> | ||||||
|  |                 </ng-container> | ||||||
|  |               </div> | ||||||
|  |             </ng-container> | ||||||
|  |           </div> | ||||||
|  |           <div class="card-footer bg-transparent" *ngIf="tasksService.progress.value[schedule.taskName]"> | ||||||
|  |             <span class="progress-details" | ||||||
|  |                   i18n>status</span>: {{tasksService.progress.value[schedule.taskName].comment}} <br/> | ||||||
|  |             <span class="progress-details" i18n>elapsed</span>: {{getTimeElapsed(schedule.taskName) | duration}}<br/> | ||||||
|  |             <span class="progress-details" i18n>left</span>: {{getTimeLeft(schedule.taskName) | duration}} | ||||||
|  |             <div class="progress"> | ||||||
|  |               <div class="progress-bar progress-bar-success" | ||||||
|  |                    role="progressbar" | ||||||
|  |                    aria-valuenow="2" | ||||||
|  |                    aria-valuemin="0" | ||||||
|  |                    aria-valuemax="100" | ||||||
|  |                    style="min-width: 2em;" | ||||||
|  |                    [style.width.%]="(tasksService.progress.value[schedule.taskName].progress/(tasksService.progress.value[schedule.taskName].left+tasksService.progress.value[schedule.taskName].progress))*100"> | ||||||
|  |                 {{tasksService.progress.value[schedule.taskName].progress}} | ||||||
|  |                 /{{tasksService.progress.value[schedule.taskName].progress + tasksService.progress.value[schedule.taskName].left}} | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |   </div> | ||||||
|  |  | ||||||
|  | </form> | ||||||
							
								
								
									
										136
									
								
								frontend/app/ui/settings/tasks/tasks.settings.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								frontend/app/ui/settings/tasks/tasks.settings.component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | |||||||
|  | import {Component, OnChanges, OnDestroy, OnInit} from '@angular/core'; | ||||||
|  | import {TasksSettingsService} from './tasks.settings.service'; | ||||||
|  | import {AuthenticationService} from '../../../model/network/authentication.service'; | ||||||
|  | import {NavigationService} from '../../../model/navigation.service'; | ||||||
|  | import {NotificationService} from '../../../model/notification.service'; | ||||||
|  | import {TaskConfig} from '../../../../../common/config/private/IPrivateConfig'; | ||||||
|  | import {SettingsComponent} from '../_abstract/abstract.settings.component'; | ||||||
|  | import {I18n} from '@ngx-translate/i18n-polyfill'; | ||||||
|  | import {ErrorDTO} from '../../../../../common/entities/Error'; | ||||||
|  | import {ScheduledTasksService} from '../scheduled-tasks.service'; | ||||||
|  | import {TaskScheduleDTO} from '../../../../../common/entities/task/TaskScheduleDTO'; | ||||||
|  |  | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-settings-tasks', | ||||||
|  |   templateUrl: './tasks.settings.component.html', | ||||||
|  |   styleUrls: ['./tasks.settings.component.css', | ||||||
|  |     './../_abstract/abstract.settings.component.css'], | ||||||
|  |   providers: [TasksSettingsService], | ||||||
|  | }) | ||||||
|  | export class TasksSettingsComponent extends SettingsComponent<TaskConfig, TasksSettingsService> | ||||||
|  |   implements OnInit, OnDestroy, OnChanges { | ||||||
|  |  | ||||||
|  |   disableButtons = false; | ||||||
|  |  | ||||||
|  |   constructor(_authService: AuthenticationService, | ||||||
|  |               _navigation: NavigationService, | ||||||
|  |               _settingsService: TasksSettingsService, | ||||||
|  |               public tasksService: ScheduledTasksService, | ||||||
|  |               notification: NotificationService, | ||||||
|  |               i18n: I18n) { | ||||||
|  |  | ||||||
|  |     super(i18n('Tasks'), | ||||||
|  |       _authService, | ||||||
|  |       _navigation, | ||||||
|  |       <any>_settingsService, | ||||||
|  |       notification, | ||||||
|  |       i18n, | ||||||
|  |       s => s.Server.tasks); | ||||||
|  |     this.hasAvailableSettings = !this.simplifiedMode; | ||||||
|  |  | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   ngOnChanges(): void { | ||||||
|  |     this.hasAvailableSettings = !this.simplifiedMode; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getConfigTemplate(taskName: string) { | ||||||
|  |     const task = this._settingsService.availableTasks.value.find(t => t.Name === taskName); | ||||||
|  |     if (task && task.ConfigTemplate && task.ConfigTemplate.length > 0) { | ||||||
|  |       const schedule = this.settings.scheduled.find(s => s.taskName === taskName); | ||||||
|  |       if (schedule) { | ||||||
|  |         schedule.config = schedule.config || {}; | ||||||
|  |         task.ConfigTemplate.forEach(ct => schedule.config[ct.id] = ct.defaultValue); | ||||||
|  |       } | ||||||
|  |       return task.ConfigTemplate; | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ngOnInit() { | ||||||
|  |     super.ngOnInit(); | ||||||
|  |     this.tasksService.subscribeToProgress(); | ||||||
|  |     this._settingsService.getAvailableTasks(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ngOnDestroy() { | ||||||
|  |     super.ngOnDestroy(); | ||||||
|  |     this.tasksService.unsubscribeFromProgress(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getTimeLeft(id: string): number { | ||||||
|  |     const prg = this.tasksService.progress.value[id]; | ||||||
|  |     if (!prg) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     return (prg.time.current - prg.time.start) / prg.progress * prg.left; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getTimeElapsed(id: string) { | ||||||
|  |     const prg = this.tasksService.progress.value[id]; | ||||||
|  |     if (!prg) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     return (prg.time.current - prg.time.start); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   public async start(schedule: TaskScheduleDTO) { | ||||||
|  |     this.error = ''; | ||||||
|  |     try { | ||||||
|  |       this.disableButtons = true; | ||||||
|  |       await this.tasksService.start(schedule.taskName, schedule.config); | ||||||
|  |       await this.tasksService.forceUpdate(); | ||||||
|  |       this.notification.info(this.i18n('Task') + ' ' + schedule.taskName + ' ' + this.i18n('started')); | ||||||
|  |       return true; | ||||||
|  |     } catch (err) { | ||||||
|  |       console.log(err); | ||||||
|  |       if (err.message) { | ||||||
|  |         this.error = (<ErrorDTO>err).message; | ||||||
|  |       } | ||||||
|  |     } finally { | ||||||
|  |       this.disableButtons = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async stop(schedule: TaskScheduleDTO) { | ||||||
|  |     this.error = ''; | ||||||
|  |     try { | ||||||
|  |       this.disableButtons = true; | ||||||
|  |       await this.tasksService.stop(schedule.taskName); | ||||||
|  |       await this.tasksService.forceUpdate(); | ||||||
|  |       this.notification.info(this.i18n('Task') + ' ' + schedule.taskName + ' ' + this.i18n('stopped')); | ||||||
|  |       return true; | ||||||
|  |     } catch (err) { | ||||||
|  |       console.log(err); | ||||||
|  |       if (err.message) { | ||||||
|  |         this.error = (<ErrorDTO>err).message; | ||||||
|  |       } | ||||||
|  |     } finally { | ||||||
|  |       this.disableButtons = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   remove(id: string) { | ||||||
|  |  | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										34
									
								
								frontend/app/ui/settings/tasks/tasks.settings.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								frontend/app/ui/settings/tasks/tasks.settings.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | import {Injectable} from '@angular/core'; | ||||||
|  | import {NetworkService} from '../../../model/network/network.service'; | ||||||
|  | import {SettingsService} from '../settings.service'; | ||||||
|  | import {AbstractSettingsService} from '../_abstract/abstract.settings.service'; | ||||||
|  | import {TaskConfig} from '../../../../../common/config/private/IPrivateConfig'; | ||||||
|  | import {BehaviorSubject} from 'rxjs'; | ||||||
|  | import {TaskDTO} from '../../../../../common/entities/task/TaskDTO'; | ||||||
|  |  | ||||||
|  | @Injectable() | ||||||
|  | export class TasksSettingsService extends AbstractSettingsService<TaskConfig> { | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   public availableTasks: BehaviorSubject<TaskDTO[]>; | ||||||
|  |  | ||||||
|  |   constructor(private _networkService: NetworkService, | ||||||
|  |               _settingsService: SettingsService) { | ||||||
|  |     super(_settingsService); | ||||||
|  |     this.availableTasks = new BehaviorSubject([]); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public updateSettings(settings: TaskConfig): Promise<void> { | ||||||
|  |     return this._networkService.putJson('/settings/tasks', {settings: settings}); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public isSupported(): boolean { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   public async getAvailableTasks() { | ||||||
|  |     this.availableTasks.next(await this._networkService.getJson<TaskDTO[]>('/admin/tasks/available')); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -2,12 +2,13 @@ | |||||||
|   "compileOnSave": true, |   "compileOnSave": true, | ||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "noImplicitAny": true, |     "noImplicitAny": true, | ||||||
|  |     "module": "commonjs", | ||||||
|     "sourceMap": true, |     "sourceMap": true, | ||||||
|     "declaration": false, |     "declaration": false, | ||||||
|     "moduleResolution": "node", |     "moduleResolution": "node", | ||||||
|     "emitDecoratorMetadata": true, |     "emitDecoratorMetadata": true, | ||||||
|     "experimentalDecorators": true, |     "experimentalDecorators": true, | ||||||
|     "target": "es5", |     "target": "es6", | ||||||
|     "typeRoots": [ |     "typeRoots": [ | ||||||
|       "node_modules/@types" |       "node_modules/@types" | ||||||
|     ], |     ], | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user