mirror of
https://github.com/bpatrik/pigallery2.git
synced 2024-12-23 01:27:14 +02:00
Merge pull request #768 from bpatrik/feature/extension
Add backend extension support #743
This commit is contained in:
commit
d38830f8be
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,3 +28,4 @@ locale.source.xlf
|
|||||||
test.*
|
test.*
|
||||||
/db/
|
/db/
|
||||||
/test/cypress/screenshots/
|
/test/cypress/screenshots/
|
||||||
|
/extensions/
|
||||||
|
@ -121,7 +121,7 @@ export class BenchmarkRunner {
|
|||||||
const bm = new Benchmark('List directory', req,
|
const bm = new Benchmark('List directory', req,
|
||||||
async (): Promise<void> => {
|
async (): Promise<void> => {
|
||||||
await ObjectManagers.reset();
|
await ObjectManagers.reset();
|
||||||
await ObjectManagers.InitSQLManagers();
|
await ObjectManagers.getInstance().init();
|
||||||
}, null,
|
}, null,
|
||||||
async (): Promise<void> => {
|
async (): Promise<void> => {
|
||||||
Config.Indexing.reIndexingSensitivity = ReIndexingSensitivity.low;
|
Config.Indexing.reIndexingSensitivity = ReIndexingSensitivity.low;
|
||||||
@ -135,7 +135,7 @@ export class BenchmarkRunner {
|
|||||||
async bmListPersons(): Promise<BenchmarkResult[]> {
|
async bmListPersons(): Promise<BenchmarkResult[]> {
|
||||||
const bm = new Benchmark('Listing Faces', Utils.clone(this.requestTemplate), async (): Promise<void> => {
|
const bm = new Benchmark('Listing Faces', Utils.clone(this.requestTemplate), async (): Promise<void> => {
|
||||||
await ObjectManagers.reset();
|
await ObjectManagers.reset();
|
||||||
await ObjectManagers.InitSQLManagers();
|
await ObjectManagers.getInstance().init();
|
||||||
}, null,
|
}, null,
|
||||||
async (): Promise<void> => {
|
async (): Promise<void> => {
|
||||||
Config.Indexing.reIndexingSensitivity = ReIndexingSensitivity.low;
|
Config.Indexing.reIndexingSensitivity = ReIndexingSensitivity.low;
|
||||||
@ -289,7 +289,7 @@ export class BenchmarkRunner {
|
|||||||
await fs.promises.rm(ProjectPath.DBFolder, {recursive: true, force: true});
|
await fs.promises.rm(ProjectPath.DBFolder, {recursive: true, force: true});
|
||||||
Config.Database.type = DatabaseType.sqlite;
|
Config.Database.type = DatabaseType.sqlite;
|
||||||
Config.Jobs.scheduled = [];
|
Config.Jobs.scheduled = [];
|
||||||
await ObjectManagers.InitSQLManagers();
|
await ObjectManagers.getInstance().init();
|
||||||
};
|
};
|
||||||
|
|
||||||
private async setupDB(): Promise<void> {
|
private async setupDB(): Promise<void> {
|
||||||
|
21
package-lock.json
generated
21
package-lock.json
generated
@ -90,7 +90,7 @@
|
|||||||
"codelyzer": "6.0.2",
|
"codelyzer": "6.0.2",
|
||||||
"core-js": "3.29.0",
|
"core-js": "3.29.0",
|
||||||
"coveralls": "3.1.1",
|
"coveralls": "3.1.1",
|
||||||
"cypress": "latest",
|
"cypress": "13.1.0",
|
||||||
"deep-equal-in-any-order": "2.0.5",
|
"deep-equal-in-any-order": "2.0.5",
|
||||||
"ejs-loader": "0.5.0",
|
"ejs-loader": "0.5.0",
|
||||||
"eslint": "8.36.0",
|
"eslint": "8.36.0",
|
||||||
@ -132,6 +132,13 @@
|
|||||||
"mysql": "2.18.1"
|
"mysql": "2.18.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"extensions/logger": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"extraneous": true,
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "4.17.21"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@ampproject/remapping": {
|
"node_modules/@ampproject/remapping": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
|
||||||
@ -8646,9 +8653,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cypress/node_modules/@types/node": {
|
"node_modules/cypress/node_modules/@types/node": {
|
||||||
"version": "16.18.48",
|
"version": "16.18.61",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.48.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.61.tgz",
|
||||||
"integrity": "sha512-mlaecDKQ7rIZrYD7iiKNdzFb6e/qD5I9U1rAhq+Fd+DWvYVs+G2kv74UFHmSOlg5+i/vF3XxuR522V4u8BqO+Q==",
|
"integrity": "sha512-k0N7BqGhJoJzdh6MuQg1V1ragJiXTh8VUBAZTWjJ9cUq23SG0F0xavOwZbhiP4J3y20xd6jxKx+xNUhkMAi76Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/cypress/node_modules/ansi-styles": {
|
"node_modules/cypress/node_modules/ansi-styles": {
|
||||||
@ -30554,9 +30561,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "16.18.48",
|
"version": "16.18.61",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.48.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.61.tgz",
|
||||||
"integrity": "sha512-mlaecDKQ7rIZrYD7iiKNdzFb6e/qD5I9U1rAhq+Fd+DWvYVs+G2kv74UFHmSOlg5+i/vF3XxuR522V4u8BqO+Q==",
|
"integrity": "sha512-k0N7BqGhJoJzdh6MuQg1V1ragJiXTh8VUBAZTWjJ9cUq23SG0F0xavOwZbhiP4J3y20xd6jxKx+xNUhkMAi76Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ansi-styles": {
|
"ansi-styles": {
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
"create-release": "gulp create-release",
|
"create-release": "gulp create-release",
|
||||||
"build-backend": "tsc",
|
"build-backend": "tsc",
|
||||||
"pretest": "tsc",
|
"pretest": "tsc",
|
||||||
"test": "ng test && nyc mocha --recursive test",
|
"test": "ng test && nyc mocha --recursive test --exclude test/cypress/**/*.js",
|
||||||
"test-backend": "tsc && mocha --recursive test",
|
"test-backend": "tsc && mocha --recursive test --exclude test/cypress/**/*.js",
|
||||||
"coverage": "nyc report --reporter=lcov",
|
"coverage": "nyc report --reporter=lcov",
|
||||||
"start": "node ./src/backend/index",
|
"start": "node ./src/backend/index",
|
||||||
"run-dev": "ng build --configuration=dev",
|
"run-dev": "ng build --configuration=dev",
|
||||||
@ -113,7 +113,7 @@
|
|||||||
"codelyzer": "6.0.2",
|
"codelyzer": "6.0.2",
|
||||||
"core-js": "3.29.0",
|
"core-js": "3.29.0",
|
||||||
"coveralls": "3.1.1",
|
"coveralls": "3.1.1",
|
||||||
"cypress": "latest",
|
"cypress": "13.1.0",
|
||||||
"deep-equal-in-any-order": "2.0.5",
|
"deep-equal-in-any-order": "2.0.5",
|
||||||
"ejs-loader": "0.5.0",
|
"ejs-loader": "0.5.0",
|
||||||
"eslint": "8.36.0",
|
"eslint": "8.36.0",
|
||||||
|
@ -11,6 +11,38 @@ if (forcedDebug === true) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LoggerFunction = (...args: (string | number)[]) => void;
|
||||||
|
|
||||||
|
export interface ILogger {
|
||||||
|
silly: LoggerFunction;
|
||||||
|
debug: LoggerFunction;
|
||||||
|
verbose: LoggerFunction;
|
||||||
|
info: LoggerFunction;
|
||||||
|
warn: LoggerFunction;
|
||||||
|
error: LoggerFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createLoggerWrapper = (TAG: string): ILogger => ({
|
||||||
|
silly: (...args: (string | number)[]) => {
|
||||||
|
Logger.silly(TAG, ...args);
|
||||||
|
},
|
||||||
|
debug: (...args: (string | number)[]) => {
|
||||||
|
Logger.debug(TAG, ...args);
|
||||||
|
},
|
||||||
|
verbose: (...args: (string | number)[]) => {
|
||||||
|
Logger.verbose(TAG, ...args);
|
||||||
|
},
|
||||||
|
info: (...args: (string | number)[]) => {
|
||||||
|
Logger.info(TAG, ...args);
|
||||||
|
},
|
||||||
|
warn: (...args: (string | number)[]) => {
|
||||||
|
Logger.warn(TAG, ...args);
|
||||||
|
},
|
||||||
|
error: (...args: (string | number)[]) => {
|
||||||
|
Logger.error(TAG, ...args);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export class Logger {
|
export class Logger {
|
||||||
public static silly(...args: (string | number)[]): void {
|
public static silly(...args: (string | number)[]): void {
|
||||||
if (!forcedDebug && Config.Server.Log.level < LogLevel.silly) {
|
if (!forcedDebug && Config.Server.Log.level < LogLevel.silly) {
|
||||||
|
@ -2,28 +2,29 @@ import * as path from 'path';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import {Config} from '../common/config/private/Config';
|
import {Config} from '../common/config/private/Config';
|
||||||
|
|
||||||
class ProjectPathClass {
|
export class ProjectPathClass {
|
||||||
public Root: string;
|
public Root: string;
|
||||||
public ImageFolder: string;
|
public ImageFolder: string;
|
||||||
public TempFolder: string;
|
public TempFolder: string;
|
||||||
public TranscodedFolder: string;
|
public TranscodedFolder: string;
|
||||||
public FacesFolder: string;
|
public FacesFolder: string;
|
||||||
public FrontendFolder: string;
|
public FrontendFolder: string;
|
||||||
|
public ExtensionFolder: string;
|
||||||
public DBFolder: string;
|
public DBFolder: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeRelative(pathStr: string): string {
|
public normalizeRelative(pathStr: string): string {
|
||||||
return path.join(pathStr, path.sep);
|
return path.join(pathStr, path.sep);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAbsolutePath(pathStr: string): string {
|
public getAbsolutePath(pathStr: string): string {
|
||||||
return path.isAbsolute(pathStr) ? pathStr : path.join(this.Root, pathStr);
|
return path.isAbsolute(pathStr) ? pathStr : path.join(this.Root, pathStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRelativePathToImages(pathStr: string): string {
|
public getRelativePathToImages(pathStr: string): string {
|
||||||
return path.relative(this.ImageFolder, pathStr);
|
return path.relative(this.ImageFolder, pathStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ class ProjectPathClass {
|
|||||||
this.TranscodedFolder = path.join(this.TempFolder, 'tc');
|
this.TranscodedFolder = path.join(this.TempFolder, 'tc');
|
||||||
this.FacesFolder = path.join(this.TempFolder, 'f');
|
this.FacesFolder = path.join(this.TempFolder, 'f');
|
||||||
this.DBFolder = this.getAbsolutePath(Config.Database.dbFolder);
|
this.DBFolder = this.getAbsolutePath(Config.Database.dbFolder);
|
||||||
|
this.ExtensionFolder = path.join(this.Root, 'extensions');
|
||||||
|
|
||||||
// create thumbnail folder if not exist
|
// create thumbnail folder if not exist
|
||||||
if (!fs.existsSync(this.TempFolder)) {
|
if (!fs.existsSync(this.TempFolder)) {
|
||||||
|
@ -11,5 +11,5 @@ if ((process.argv || []).includes('--run-diagnostics')) {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
new Server();
|
Server.getInstance();
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import {SharingDTO} from '../../common/entities/SharingDTO';
|
|||||||
import {Utils} from '../../common/Utils';
|
import {Utils} from '../../common/Utils';
|
||||||
import {LoggerRouter} from '../routes/LoggerRouter';
|
import {LoggerRouter} from '../routes/LoggerRouter';
|
||||||
import {TAGS} from '../../common/config/public/ClientConfig';
|
import {TAGS} from '../../common/config/public/ClientConfig';
|
||||||
|
import {ExtensionConfigWrapper} from '../model/extension/ExtensionConfigWrapper';
|
||||||
|
|
||||||
const forcedDebug = process.env['NODE_ENV'] === 'debug';
|
const forcedDebug = process.env['NODE_ENV'] === 'debug';
|
||||||
|
|
||||||
@ -107,7 +108,7 @@ export class RenderingMWs {
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const originalConf = await Config.original();
|
const originalConf = await ExtensionConfigWrapper.original();
|
||||||
// These are sensitive information, do not send to the client side
|
// These are sensitive information, do not send to the client side
|
||||||
originalConf.Server.sessionSecret = null;
|
originalConf.Server.sessionSecret = null;
|
||||||
const message = new Message<PrivateConfigClass>(
|
const message = new Message<PrivateConfigClass>(
|
||||||
|
@ -2,6 +2,7 @@ import {NextFunction, Request, Response} from 'express';
|
|||||||
import {ErrorCodes, ErrorDTO} from '../../../common/entities/Error';
|
import {ErrorCodes, ErrorDTO} from '../../../common/entities/Error';
|
||||||
import {ObjectManagers} from '../../model/ObjectManagers';
|
import {ObjectManagers} from '../../model/ObjectManagers';
|
||||||
import {StatisticDTO} from '../../../common/entities/settings/StatisticDTO';
|
import {StatisticDTO} from '../../../common/entities/settings/StatisticDTO';
|
||||||
|
import {MessengerRepository} from '../../model/messenger/MessengerRepository';
|
||||||
|
|
||||||
export class AdminMWs {
|
export class AdminMWs {
|
||||||
public static async loadStatistic(
|
public static async loadStatistic(
|
||||||
@ -80,7 +81,7 @@ export class AdminMWs {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const id = req.params['id'];
|
const id = req.params['id'];
|
||||||
const JobConfig: unknown = req.body.config;
|
const JobConfig: Record<string, unknown> = req.body.config;
|
||||||
const soloRun: boolean = req.body.soloRun;
|
const soloRun: boolean = req.body.soloRun;
|
||||||
const allowParallelRun: boolean = req.body.allowParallelRun;
|
const allowParallelRun: boolean = req.body.allowParallelRun;
|
||||||
await ObjectManagers.getInstance().JobManager.run(
|
await ObjectManagers.getInstance().JobManager.run(
|
||||||
@ -137,6 +138,35 @@ export class AdminMWs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static getAvailableMessengers(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
req.resultPipe = MessengerRepository.Instance.getAll().map(msgr => msgr.Name);
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
return next(
|
||||||
|
new ErrorDTO(
|
||||||
|
ErrorCodes.JOB_ERROR,
|
||||||
|
'Messenger error: ' + err.toString(),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return next(
|
||||||
|
new ErrorDTO(
|
||||||
|
ErrorCodes.JOB_ERROR,
|
||||||
|
'Messenger error: ' + JSON.stringify(err, null, ' '),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static getAvailableJobs(
|
public static getAvailableJobs(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
@ -3,9 +3,10 @@ import {ErrorCodes, ErrorDTO} from '../../../common/entities/Error';
|
|||||||
import {Logger} from '../../Logger';
|
import {Logger} from '../../Logger';
|
||||||
import {Config} from '../../../common/config/private/Config';
|
import {Config} from '../../../common/config/private/Config';
|
||||||
import {ConfigDiagnostics} from '../../model/diagnostics/ConfigDiagnostics';
|
import {ConfigDiagnostics} from '../../model/diagnostics/ConfigDiagnostics';
|
||||||
import {ConfigClassBuilder} from '../../../../node_modules/typeconfig/node';
|
import {ConfigClassBuilder} from 'typeconfig/node';
|
||||||
import {TAGS} from '../../../common/config/public/ClientConfig';
|
import {TAGS} from '../../../common/config/public/ClientConfig';
|
||||||
import {ObjectManagers} from '../../model/ObjectManagers';
|
import {ObjectManagers} from '../../model/ObjectManagers';
|
||||||
|
import {ExtensionConfigWrapper} from '../../model/extension/ExtensionConfigWrapper';
|
||||||
|
|
||||||
const LOG_TAG = '[SettingsMWs]';
|
const LOG_TAG = '[SettingsMWs]';
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ export class SettingsMWs {
|
|||||||
try {
|
try {
|
||||||
let settings = req.body.settings; // Top level settings JSON
|
let settings = req.body.settings; // Top level settings JSON
|
||||||
const settingsPath: string = req.body.settingsPath; // Name of the top level settings
|
const settingsPath: string = req.body.settingsPath; // Name of the top level settings
|
||||||
const transformer = await Config.original();
|
const transformer = await ExtensionConfigWrapper.original();
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
transformer[settingsPath] = settings;
|
transformer[settingsPath] = settings;
|
||||||
@ -37,7 +38,7 @@ export class SettingsMWs {
|
|||||||
settings = ConfigClassBuilder.attachPrivateInterface(transformer[settingsPath]).toJSON({
|
settings = ConfigClassBuilder.attachPrivateInterface(transformer[settingsPath]).toJSON({
|
||||||
skipTags: {secret: true} as TAGS
|
skipTags: {secret: true} as TAGS
|
||||||
});
|
});
|
||||||
const original = await Config.original();
|
const original = await ExtensionConfigWrapper.original();
|
||||||
// only updating explicitly set config (not saving config set by the diagnostics)
|
// only updating explicitly set config (not saving config set by the diagnostics)
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -14,6 +14,7 @@ import {AlbumManager} from './database/AlbumManager';
|
|||||||
import {PersonManager} from './database/PersonManager';
|
import {PersonManager} from './database/PersonManager';
|
||||||
import {SharingManager} from './database/SharingManager';
|
import {SharingManager} from './database/SharingManager';
|
||||||
import {IObjectManager} from './database/IObjectManager';
|
import {IObjectManager} from './database/IObjectManager';
|
||||||
|
import {ExtensionManager} from './extension/ExtensionManager';
|
||||||
|
|
||||||
const LOG_TAG = '[ObjectManagers]';
|
const LOG_TAG = '[ObjectManagers]';
|
||||||
|
|
||||||
@ -32,11 +33,99 @@ export class ObjectManagers {
|
|||||||
private jobManager: JobManager;
|
private jobManager: JobManager;
|
||||||
private locationManager: LocationManager;
|
private locationManager: LocationManager;
|
||||||
private albumManager: AlbumManager;
|
private albumManager: AlbumManager;
|
||||||
|
private extensionManager: ExtensionManager;
|
||||||
|
private initDone = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.managers = [];
|
this.managers = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static getInstance(): ObjectManagers {
|
||||||
|
if (!this.instance) {
|
||||||
|
this.instance = new ObjectManagers();
|
||||||
|
}
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async reset(): Promise<void> {
|
||||||
|
Logger.silly(LOG_TAG, 'Object manager reset begin');
|
||||||
|
if (ObjectManagers.isReady()) {
|
||||||
|
if (
|
||||||
|
ObjectManagers.getInstance().IndexingManager &&
|
||||||
|
ObjectManagers.getInstance().IndexingManager.IsSavingInProgress
|
||||||
|
) {
|
||||||
|
await ObjectManagers.getInstance().IndexingManager.SavingReady;
|
||||||
|
}
|
||||||
|
for (const manager of ObjectManagers.getInstance().managers) {
|
||||||
|
if (manager === ObjectManagers.getInstance().versionManager) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (manager.cleanUp) {
|
||||||
|
await manager.cleanUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await SQLConnection.close();
|
||||||
|
this.instance = null;
|
||||||
|
Logger.debug(LOG_TAG, 'Object manager reset done');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static isReady(): boolean {
|
||||||
|
return this.instance && this.instance.initDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async init(): Promise<void> {
|
||||||
|
if (this.initDone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await SQLConnection.init();
|
||||||
|
await this.initManagers();
|
||||||
|
Logger.debug(LOG_TAG, 'SQL DB inited');
|
||||||
|
this.initDone = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initManagers(): Promise<void> {
|
||||||
|
this.AlbumManager = new AlbumManager();
|
||||||
|
this.GalleryManager = new GalleryManager();
|
||||||
|
this.IndexingManager = new IndexingManager();
|
||||||
|
this.PersonManager = new PersonManager();
|
||||||
|
this.CoverManager = new CoverManager();
|
||||||
|
this.SearchManager = new SearchManager();
|
||||||
|
this.SharingManager = new SharingManager();
|
||||||
|
this.UserManager = new UserManager();
|
||||||
|
this.VersionManager = new VersionManager();
|
||||||
|
this.JobManager = new JobManager();
|
||||||
|
this.LocationManager = new LocationManager();
|
||||||
|
this.ExtensionManager = new ExtensionManager();
|
||||||
|
|
||||||
|
for (const manager of ObjectManagers.getInstance().managers) {
|
||||||
|
if (manager === ObjectManagers.getInstance().versionManager) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (manager.init) {
|
||||||
|
await manager.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async onDataChange(
|
||||||
|
changedDir: ParentDirectoryDTO = null
|
||||||
|
): Promise<void> {
|
||||||
|
await this.VersionManager.onNewDataVersion();
|
||||||
|
|
||||||
|
for (const manager of this.managers) {
|
||||||
|
if (manager === this.versionManager) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (manager.onNewDataVersion) {
|
||||||
|
await manager.onNewDataVersion(changedDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
get VersionManager(): VersionManager {
|
get VersionManager(): VersionManager {
|
||||||
return this.versionManager;
|
return this.versionManager;
|
||||||
}
|
}
|
||||||
@ -169,62 +258,15 @@ export class ObjectManagers {
|
|||||||
this.managers.push(this.jobManager as IObjectManager);
|
this.managers.push(this.jobManager as IObjectManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getInstance(): ObjectManagers {
|
get ExtensionManager(): ExtensionManager {
|
||||||
if (this.instance === null) {
|
return this.extensionManager;
|
||||||
this.instance = new ObjectManagers();
|
|
||||||
}
|
|
||||||
return this.instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async reset(): Promise<void> {
|
set ExtensionManager(value: ExtensionManager) {
|
||||||
Logger.silly(LOG_TAG, 'Object manager reset begin');
|
if (this.extensionManager) {
|
||||||
if (
|
this.managers.splice(this.managers.indexOf(this.extensionManager as IObjectManager), 1);
|
||||||
ObjectManagers.getInstance().IndexingManager &&
|
|
||||||
ObjectManagers.getInstance().IndexingManager.IsSavingInProgress
|
|
||||||
) {
|
|
||||||
await ObjectManagers.getInstance().IndexingManager.SavingReady;
|
|
||||||
}
|
|
||||||
if (ObjectManagers.getInstance().JobManager) {
|
|
||||||
ObjectManagers.getInstance().JobManager.stopSchedules();
|
|
||||||
}
|
|
||||||
await SQLConnection.close();
|
|
||||||
this.instance = null;
|
|
||||||
Logger.debug(LOG_TAG, 'Object manager reset');
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async InitSQLManagers(): Promise<void> {
|
|
||||||
await ObjectManagers.reset();
|
|
||||||
await SQLConnection.init();
|
|
||||||
this.initManagers();
|
|
||||||
Logger.debug(LOG_TAG, 'SQL DB inited');
|
|
||||||
}
|
|
||||||
|
|
||||||
private static initManagers(): void {
|
|
||||||
ObjectManagers.getInstance().AlbumManager = new AlbumManager();
|
|
||||||
ObjectManagers.getInstance().GalleryManager = new GalleryManager();
|
|
||||||
ObjectManagers.getInstance().IndexingManager = new IndexingManager();
|
|
||||||
ObjectManagers.getInstance().PersonManager = new PersonManager();
|
|
||||||
ObjectManagers.getInstance().CoverManager = new CoverManager();
|
|
||||||
ObjectManagers.getInstance().SearchManager = new SearchManager();
|
|
||||||
ObjectManagers.getInstance().SharingManager = new SharingManager();
|
|
||||||
ObjectManagers.getInstance().UserManager = new UserManager();
|
|
||||||
ObjectManagers.getInstance().VersionManager = new VersionManager();
|
|
||||||
ObjectManagers.getInstance().JobManager = new JobManager();
|
|
||||||
ObjectManagers.getInstance().LocationManager = new LocationManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async onDataChange(
|
|
||||||
changedDir: ParentDirectoryDTO = null
|
|
||||||
): Promise<void> {
|
|
||||||
await this.VersionManager.onNewDataVersion();
|
|
||||||
|
|
||||||
for (const manager of this.managers) {
|
|
||||||
if (manager === this.versionManager) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (manager.onNewDataVersion) {
|
|
||||||
await manager.onNewDataVersion(changedDir);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this.extensionManager = value;
|
||||||
|
this.managers.push(this.extensionManager as IObjectManager);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ export class AlbumManager implements IObjectManager {
|
|||||||
private static async updateAlbum(album: SavedSearchEntity): Promise<void> {
|
private static async updateAlbum(album: SavedSearchEntity): Promise<void> {
|
||||||
const connection = await SQLConnection.getConnection();
|
const connection = await SQLConnection.getConnection();
|
||||||
const cover =
|
const cover =
|
||||||
await ObjectManagers.getInstance().CoverManager.getAlbumCover(album);
|
await ObjectManagers.getInstance().CoverManager.getCoverForAlbum(album);
|
||||||
const count = await
|
const count = await
|
||||||
ObjectManagers.getInstance().SearchManager.getCount((album as SavedSearchDTO).searchQuery);
|
ObjectManagers.getInstance().SearchManager.getCount((album as SavedSearchDTO).searchQuery);
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import {CoverPhotoDTO} from '../../../common/entities/PhotoDTO';
|
|||||||
import {IObjectManager} from './IObjectManager';
|
import {IObjectManager} from './IObjectManager';
|
||||||
import {Logger} from '../../Logger';
|
import {Logger} from '../../Logger';
|
||||||
import {SearchManager} from './SearchManager';
|
import {SearchManager} from './SearchManager';
|
||||||
|
import {ExtensionDecorator} from '../extension/ExtensionDecorator';
|
||||||
|
|
||||||
const LOG_TAG = '[CoverManager]';
|
const LOG_TAG = '[CoverManager]';
|
||||||
|
|
||||||
@ -35,10 +36,11 @@ export class CoverManager implements IObjectManager {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onNewDataVersion(changedDir: ParentDirectoryDTO): Promise<void> {
|
@ExtensionDecorator(e => e.gallery.CoverManager.invalidateDirectoryCovers)
|
||||||
|
protected async invalidateDirectoryCovers(dir: ParentDirectoryDTO) {
|
||||||
// Invalidating Album cover
|
// Invalidating Album cover
|
||||||
let fullPath = DiskManager.normalizeDirPath(
|
let fullPath = DiskManager.normalizeDirPath(
|
||||||
path.join(changedDir.path, changedDir.name)
|
path.join(dir.path, dir.name)
|
||||||
);
|
);
|
||||||
const query = (await SQLConnection.getConnection())
|
const query = (await SQLConnection.getConnection())
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
@ -77,7 +79,12 @@ export class CoverManager implements IObjectManager {
|
|||||||
await query.execute();
|
await query.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAlbumCover(album: {
|
public async onNewDataVersion(changedDir: ParentDirectoryDTO): Promise<void> {
|
||||||
|
await this.invalidateDirectoryCovers(changedDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExtensionDecorator(e => e.gallery.CoverManager.getCoverForAlbum)
|
||||||
|
public async getCoverForAlbum(album: {
|
||||||
searchQuery: SearchQueryDTO;
|
searchQuery: SearchQueryDTO;
|
||||||
}): Promise<CoverPhotoDTOWithID> {
|
}): Promise<CoverPhotoDTOWithID> {
|
||||||
const albumQuery: Brackets = await
|
const albumQuery: Brackets = await
|
||||||
@ -138,11 +145,12 @@ export class CoverManager implements IObjectManager {
|
|||||||
.getRawMany();
|
.getRawMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setAndGetCoverForDirectory(dir: {
|
@ExtensionDecorator(e => e.gallery.CoverManager.getCoverForDirectory)
|
||||||
|
protected async getCoverForDirectory(dir: {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
}): Promise<CoverPhotoDTOWithID> {
|
}) {
|
||||||
const connection = await SQLConnection.getConnection();
|
const connection = await SQLConnection.getConnection();
|
||||||
const coverQuery = (): SelectQueryBuilder<MediaEntity> => {
|
const coverQuery = (): SelectQueryBuilder<MediaEntity> => {
|
||||||
const query = connection
|
const query = connection
|
||||||
@ -198,6 +206,16 @@ export class CoverManager implements IObjectManager {
|
|||||||
if (!coverMedia) {
|
if (!coverMedia) {
|
||||||
coverMedia = await coverQuery().limit(1).getOne();
|
coverMedia = await coverQuery().limit(1).getOne();
|
||||||
}
|
}
|
||||||
|
return coverMedia;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setAndGetCoverForDirectory(dir: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
}): Promise<CoverPhotoDTOWithID> {
|
||||||
|
const connection = await SQLConnection.getConnection();
|
||||||
|
const coverMedia = await this.getCoverForDirectory(dir);
|
||||||
|
|
||||||
// set validCover bit to true even if there is no cover (to prevent future updates)
|
// set validCover bit to true even if there is no cover (to prevent future updates)
|
||||||
await connection
|
await connection
|
||||||
|
@ -2,4 +2,6 @@ import {ParentDirectoryDTO} from '../../../common/entities/DirectoryDTO';
|
|||||||
|
|
||||||
export interface IObjectManager {
|
export interface IObjectManager {
|
||||||
onNewDataVersion?: (changedDir?: ParentDirectoryDTO) => Promise<void>;
|
onNewDataVersion?: (changedDir?: ParentDirectoryDTO) => Promise<void>;
|
||||||
|
cleanUp?: () => Promise<void>;
|
||||||
|
init?: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,38 @@ const LOG_TAG = '[SQLConnection]';
|
|||||||
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||||
|
|
||||||
export class SQLConnection {
|
export class SQLConnection {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
public static getEntries(): Function[] {
|
||||||
|
return this.entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
public static async addEntries(tables: Function[]) {
|
||||||
|
if (!tables?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.close();
|
||||||
|
this.entries = Utils.getUnique(this.entries.concat(tables));
|
||||||
|
await (await this.getConnection()).synchronize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
private static entries: Function[] = [
|
||||||
|
UserEntity,
|
||||||
|
FileEntity,
|
||||||
|
MDFileEntity,
|
||||||
|
PersonJunctionTable,
|
||||||
|
PersonEntry,
|
||||||
|
MediaEntity,
|
||||||
|
PhotoEntity,
|
||||||
|
VideoEntity,
|
||||||
|
DirectoryEntity,
|
||||||
|
SharingEntity,
|
||||||
|
AlbumBaseEntity,
|
||||||
|
SavedSearchEntity,
|
||||||
|
VersionEntity,
|
||||||
|
];
|
||||||
|
|
||||||
private static connection: Connection = null;
|
private static connection: Connection = null;
|
||||||
|
|
||||||
|
|
||||||
@ -128,6 +160,33 @@ export class SQLConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static FIXED_SQL_TABLE = [
|
||||||
|
'sqlite_sequence'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears up the DB from unused tables. use it when the entities list are up-to-date (extensions won't add any new)
|
||||||
|
*/
|
||||||
|
public static async removeUnusedTables() {
|
||||||
|
const conn = await this.getConnection();
|
||||||
|
const validTableNames = this.entries.map(e => conn.getRepository(e).metadata.tableName).concat(this.FIXED_SQL_TABLE);
|
||||||
|
let currentTables: string[];
|
||||||
|
|
||||||
|
if (Config.Database.type === DatabaseType.sqlite) {
|
||||||
|
currentTables = (await conn.query('SELECT name FROM sqlite_master WHERE type=\'table\''))
|
||||||
|
.map((r: { name: string }) => r.name);
|
||||||
|
} else {
|
||||||
|
currentTables = (await conn.query(`SELECT table_name FROM information_schema.tables ` +
|
||||||
|
`WHERE table_schema = '${Config.Database.mysql.database}'`))
|
||||||
|
.map((r: { table_name: string }) => r.table_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableToDrop = currentTables.filter(ct => !validTableNames.includes(ct));
|
||||||
|
for (let i = 0; i < tableToDrop.length; ++i) {
|
||||||
|
await conn.query('DROP TABLE ' + tableToDrop[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static getSQLiteDB(config: ServerDataBaseConfig): string {
|
public static getSQLiteDB(config: ServerDataBaseConfig): string {
|
||||||
return path.join(ProjectPath.getAbsolutePath(config.dbFolder), 'sqlite.db');
|
return path.join(ProjectPath.getAbsolutePath(config.dbFolder), 'sqlite.db');
|
||||||
}
|
}
|
||||||
@ -222,21 +281,7 @@ export class SQLConnection {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
driver.entities = [
|
driver.entities = this.entries;
|
||||||
UserEntity,
|
|
||||||
FileEntity,
|
|
||||||
MDFileEntity,
|
|
||||||
PersonJunctionTable,
|
|
||||||
PersonEntry,
|
|
||||||
MediaEntity,
|
|
||||||
PhotoEntity,
|
|
||||||
VideoEntity,
|
|
||||||
DirectoryEntity,
|
|
||||||
SharingEntity,
|
|
||||||
AlbumBaseEntity,
|
|
||||||
SavedSearchEntity,
|
|
||||||
VersionEntity,
|
|
||||||
];
|
|
||||||
driver.synchronize = false;
|
driver.synchronize = false;
|
||||||
if (Config.Server.Log.sqlLevel !== SQLLogLevel.none) {
|
if (Config.Server.Log.sqlLevel !== SQLLogLevel.none) {
|
||||||
driver.logging = SQLLogLevel[Config.Server.Log.sqlLevel] as LoggerOptions;
|
driver.logging = SQLLogLevel[Config.Server.Log.sqlLevel] as LoggerOptions;
|
||||||
|
79
src/backend/model/extension/ExpressRouterWrapper.ts
Normal file
79
src/backend/model/extension/ExpressRouterWrapper.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import * as express from 'express';
|
||||||
|
import {NextFunction, Request, Response} from 'express';
|
||||||
|
import {UserDTO, UserRoles} from '../../../common/entities/UserDTO';
|
||||||
|
import {AuthenticationMWs} from '../../middlewares/user/AuthenticationMWs';
|
||||||
|
import {RenderingMWs} from '../../middlewares/RenderingMWs';
|
||||||
|
import {ParamsDictionary} from 'express-serve-static-core';
|
||||||
|
import {IExtensionRESTApi, IExtensionRESTRoute} from './IExtension';
|
||||||
|
import {ILogger} from '../../Logger';
|
||||||
|
import {ExtensionManager} from './ExtensionManager';
|
||||||
|
import {Utils} from '../../../common/Utils';
|
||||||
|
|
||||||
|
|
||||||
|
export class ExpressRouterWrapper implements IExtensionRESTApi {
|
||||||
|
|
||||||
|
constructor(private readonly router: express.Router,
|
||||||
|
private readonly name: string,
|
||||||
|
private readonly extLogger: ILogger) {
|
||||||
|
}
|
||||||
|
|
||||||
|
get use() {
|
||||||
|
return new ExpressRouteWrapper(this.router, this.name, 'use', this.extLogger);
|
||||||
|
}
|
||||||
|
|
||||||
|
get get() {
|
||||||
|
return new ExpressRouteWrapper(this.router, this.name, 'get', this.extLogger);
|
||||||
|
}
|
||||||
|
|
||||||
|
get put() {
|
||||||
|
return new ExpressRouteWrapper(this.router, this.name, 'put', this.extLogger);
|
||||||
|
}
|
||||||
|
|
||||||
|
get post() {
|
||||||
|
return new ExpressRouteWrapper(this.router, this.name, 'post', this.extLogger);
|
||||||
|
}
|
||||||
|
|
||||||
|
get delete() {
|
||||||
|
return new ExpressRouteWrapper(this.router, this.name, 'delete', this.extLogger);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExpressRouteWrapper implements IExtensionRESTRoute {
|
||||||
|
|
||||||
|
constructor(private readonly router: express.Router,
|
||||||
|
private readonly name: string,
|
||||||
|
private readonly func: 'get' | 'use' | 'put' | 'post' | 'delete',
|
||||||
|
private readonly extLogger: ILogger) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAuthMWs(minRole: UserRoles) {
|
||||||
|
return minRole ? [AuthenticationMWs.authenticate,
|
||||||
|
AuthenticationMWs.authorise(minRole)] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public jsonResponse(paths: string[], minRole: UserRoles, cb: (params?: ParamsDictionary, body?: any, user?: UserDTO) => Promise<unknown> | unknown) {
|
||||||
|
const fullPaths = paths.map(p => (Utils.concatUrls('/' + this.name + '/' + p)));
|
||||||
|
this.router[this.func](fullPaths,
|
||||||
|
...(this.getAuthMWs(minRole).concat([
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
req.resultPipe = await cb(req.params, req.body, req.session['user']);
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
RenderingMWs.renderResult
|
||||||
|
])));
|
||||||
|
const p = ExtensionManager.EXTENSION_API_PATH + fullPaths;
|
||||||
|
this.extLogger.silly(`Listening on ${this.func} ${p}`);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
public rawMiddleware(paths: string[], minRole: UserRoles, mw: (req: Request, res: Response, next: NextFunction) => void | Promise<void>) {
|
||||||
|
const fullPaths = paths.map(p => (Utils.concatUrls('/' + this.name + '/' + p)));
|
||||||
|
this.router[this.func](fullPaths,
|
||||||
|
...this.getAuthMWs(minRole),
|
||||||
|
mw);
|
||||||
|
const p = ExtensionManager.EXTENSION_API_PATH + fullPaths;
|
||||||
|
this.extLogger.silly(`Listening on ${this.func} ${p}`);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
18
src/backend/model/extension/ExtensionApp.ts
Normal file
18
src/backend/model/extension/ExtensionApp.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import {IExtensionApp} from './IExtension';
|
||||||
|
import {ObjectManagers} from '../ObjectManagers';
|
||||||
|
import {Config} from '../../../common/config/private/Config';
|
||||||
|
import {Server} from '../../server';
|
||||||
|
|
||||||
|
export class ExtensionApp implements IExtensionApp {
|
||||||
|
get config() {
|
||||||
|
return Config;
|
||||||
|
}
|
||||||
|
|
||||||
|
get expressApp() {
|
||||||
|
return Server.getInstance().app;
|
||||||
|
}
|
||||||
|
|
||||||
|
get objectManagers() {
|
||||||
|
return ObjectManagers.getInstance();
|
||||||
|
}
|
||||||
|
}
|
52
src/backend/model/extension/ExtensionConfigWrapper.ts
Normal file
52
src/backend/model/extension/ExtensionConfigWrapper.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import {IConfigClass} from 'typeconfig/common';
|
||||||
|
import {Config, PrivateConfigClass} from '../../../common/config/private/Config';
|
||||||
|
import {ConfigClassBuilder} from 'typeconfig/node';
|
||||||
|
import {IExtensionConfig} from './IExtension';
|
||||||
|
import {Utils} from '../../../common/Utils';
|
||||||
|
import {ObjectManagers} from '../ObjectManagers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps to original config and makes sure all extension related config is loaded
|
||||||
|
*/
|
||||||
|
export class ExtensionConfigWrapper {
|
||||||
|
static async original(): Promise<PrivateConfigClass & IConfigClass> {
|
||||||
|
const pc = ConfigClassBuilder.attachPrivateInterface(new PrivateConfigClass());
|
||||||
|
try {
|
||||||
|
await pc.load();
|
||||||
|
if (ObjectManagers.isReady()) {
|
||||||
|
for (const ext of Object.values(ObjectManagers.getInstance().ExtensionManager.extObjects)) {
|
||||||
|
ext.config.loadToConfig(ConfigClassBuilder.attachPrivateInterface(pc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error during loading original config. Reverting to defaults.');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return pc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExtensionConfig<C> implements IExtensionConfig<C> {
|
||||||
|
public template: new() => C;
|
||||||
|
|
||||||
|
constructor(private readonly extensionId: string) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public getConfig(): C {
|
||||||
|
return Config.Extensions.configs[this.extensionId] as C;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setTemplate(template: new() => C): void {
|
||||||
|
this.template = template;
|
||||||
|
this.loadToConfig(Config);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadToConfig(config: PrivateConfigClass) {
|
||||||
|
if (!this.template) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const conf = ConfigClassBuilder.attachPrivateInterface(new this.template());
|
||||||
|
conf.__loadJSONObject(Utils.clone(config.Extensions.configs[this.extensionId] || {}));
|
||||||
|
config.Extensions.configs[this.extensionId] = conf;
|
||||||
|
}
|
||||||
|
}
|
26
src/backend/model/extension/ExtensionDB.ts
Normal file
26
src/backend/model/extension/ExtensionDB.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import {IExtensionDB} from './IExtension';
|
||||||
|
import {SQLConnection} from '../database/SQLConnection';
|
||||||
|
import {Connection} from 'typeorm';
|
||||||
|
import {ILogger} from '../../Logger';
|
||||||
|
|
||||||
|
export class ExtensionDB implements IExtensionDB {
|
||||||
|
|
||||||
|
constructor(private readonly extLogger: ILogger) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
_getAllTables(): Function[] {
|
||||||
|
return SQLConnection.getEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSQLConnection(): Promise<Connection> {
|
||||||
|
return SQLConnection.getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
async setExtensionTables(tables: Function[]): Promise<void> {
|
||||||
|
this.extLogger.debug('Adding ' + tables?.length + ' extension tables to DB');
|
||||||
|
await SQLConnection.addEntries(tables);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
src/backend/model/extension/ExtensionDecorator.ts
Normal file
40
src/backend/model/extension/ExtensionDecorator.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import {IExtensionEvent, IExtensionEvents} from './IExtension';
|
||||||
|
import {ExtensionEvent} from './ExtensionEvent';
|
||||||
|
|
||||||
|
export class ExtensionDecoratorObject {
|
||||||
|
public static events: IExtensionEvents;
|
||||||
|
|
||||||
|
static init(events: IExtensionEvents) {
|
||||||
|
this.events = events;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExtensionDecorator = <I extends [], O>(fn: (ee: IExtensionEvents) => IExtensionEvent<I, O>) => {
|
||||||
|
return (
|
||||||
|
target: unknown,
|
||||||
|
propertyName: string,
|
||||||
|
descriptor: PropertyDescriptor
|
||||||
|
) => {
|
||||||
|
|
||||||
|
const targetMethod = descriptor.value;
|
||||||
|
descriptor.value = async function(...args: I) {
|
||||||
|
if (!ExtensionDecoratorObject.events) {
|
||||||
|
return await targetMethod.apply(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = fn(ExtensionDecoratorObject.events) as ExtensionEvent<I, O>;
|
||||||
|
const eventObj = {stopPropagation: false};
|
||||||
|
const input = await event.triggerBefore({inputs: args}, eventObj);
|
||||||
|
|
||||||
|
// skip the rest of the execution if the before handler asked for stop propagation
|
||||||
|
if (eventObj.stopPropagation) {
|
||||||
|
return input as O;
|
||||||
|
}
|
||||||
|
const out = await targetMethod.apply(this, args);
|
||||||
|
return await event.triggerAfter(out);
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
};
|
||||||
|
};
|
57
src/backend/model/extension/ExtensionEvent.ts
Normal file
57
src/backend/model/extension/ExtensionEvent.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import {IExtensionAfterEventHandler, IExtensionBeforeEventHandler, IExtensionEvent} from './IExtension';
|
||||||
|
|
||||||
|
export class ExtensionEvent<I, O> implements IExtensionEvent<I, O> {
|
||||||
|
protected beforeHandlers: IExtensionBeforeEventHandler<I, O>[] = [];
|
||||||
|
protected afterHandlers: IExtensionAfterEventHandler<O>[] = [];
|
||||||
|
|
||||||
|
public before(handler: IExtensionBeforeEventHandler<I, O>): void {
|
||||||
|
if (typeof handler !== 'function') {
|
||||||
|
throw new Error('ExtensionEvent::before: Handler is not a function');
|
||||||
|
}
|
||||||
|
this.beforeHandlers.push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public after(handler: IExtensionAfterEventHandler<O>): void {
|
||||||
|
if (typeof handler !== 'function') {
|
||||||
|
throw new Error('ExtensionEvent::after: Handler is not a function');
|
||||||
|
}
|
||||||
|
this.afterHandlers.push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public offAfter(handler: IExtensionAfterEventHandler<O>): void {
|
||||||
|
this.afterHandlers = this.afterHandlers.filter((h) => h !== handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public offBefore(handler: IExtensionBeforeEventHandler<I, O>): void {
|
||||||
|
this.beforeHandlers = this.beforeHandlers.filter((h) => h !== handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async triggerBefore(input: { inputs: I }, event: { stopPropagation: boolean }): Promise<{ inputs: I } | O> {
|
||||||
|
let pipe: { inputs: I } | O = input;
|
||||||
|
if (this.beforeHandlers && this.beforeHandlers.length > 0) {
|
||||||
|
const s = this.beforeHandlers.slice(0);
|
||||||
|
for (let i = 0; i < s.length; ++i) {
|
||||||
|
if (event.stopPropagation) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pipe = await s[i](pipe as { inputs: I }, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pipe;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async triggerAfter(output: O): Promise<O> {
|
||||||
|
if (this.afterHandlers && this.afterHandlers.length > 0) {
|
||||||
|
const s = this.afterHandlers.slice(0);
|
||||||
|
for (let i = 0; i < s.length; ++i) {
|
||||||
|
output = await s[i](output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
157
src/backend/model/extension/ExtensionManager.ts
Normal file
157
src/backend/model/extension/ExtensionManager.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import {ProjectPath} from '../../ProjectPath';
|
||||||
|
import {Config} from '../../../common/config/private/Config';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import {IObjectManager} from '../database/IObjectManager';
|
||||||
|
import {Logger} from '../../Logger';
|
||||||
|
import {IExtensionEvents, IExtensionObject} from './IExtension';
|
||||||
|
import {Server} from '../../server';
|
||||||
|
import {ExtensionEvent} from './ExtensionEvent';
|
||||||
|
import * as express from 'express';
|
||||||
|
import {SQLConnection} from '../database/SQLConnection';
|
||||||
|
import {ExtensionObject} from './ExtensionObject';
|
||||||
|
import {ExtensionDecoratorObject} from './ExtensionDecorator';
|
||||||
|
import * as util from 'util';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const exec = util.promisify(require('child_process').exec);
|
||||||
|
|
||||||
|
const LOG_TAG = '[ExtensionManager]';
|
||||||
|
|
||||||
|
export class ExtensionManager implements IObjectManager {
|
||||||
|
|
||||||
|
public static EXTENSION_API_PATH = Config.Server.apiPath + '/extension';
|
||||||
|
|
||||||
|
events: IExtensionEvents;
|
||||||
|
extObjects: { [key: string]: ExtensionObject<unknown> } = {};
|
||||||
|
router: express.Router;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.initEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async init() {
|
||||||
|
this.extObjects = {};
|
||||||
|
this.initEvents();
|
||||||
|
if (!Config.Extensions.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.router = express.Router();
|
||||||
|
Server.getInstance().app.use(ExtensionManager.EXTENSION_API_PATH, this.router);
|
||||||
|
this.loadExtensionsList();
|
||||||
|
await this.initExtensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initEvents() {
|
||||||
|
this.events = {
|
||||||
|
gallery: {
|
||||||
|
MetadataLoader: {
|
||||||
|
loadPhotoMetadata: new ExtensionEvent(),
|
||||||
|
loadVideoMetadata: new ExtensionEvent()
|
||||||
|
},
|
||||||
|
CoverManager: {
|
||||||
|
getCoverForDirectory: new ExtensionEvent(),
|
||||||
|
getCoverForAlbum: new ExtensionEvent(),
|
||||||
|
invalidateDirectoryCovers: new ExtensionEvent(),
|
||||||
|
},
|
||||||
|
DiskManager: {
|
||||||
|
scanDirectory: new ExtensionEvent()
|
||||||
|
},
|
||||||
|
ImageRenderer: {
|
||||||
|
render: new ExtensionEvent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ExtensionDecoratorObject.init(this.events);
|
||||||
|
}
|
||||||
|
|
||||||
|
public loadExtensionsList() {
|
||||||
|
Logger.debug(LOG_TAG, 'Loading extension list from ' + ProjectPath.ExtensionFolder);
|
||||||
|
if (!fs.existsSync(ProjectPath.ExtensionFolder)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.Extensions.list = fs
|
||||||
|
.readdirSync(ProjectPath.ExtensionFolder)
|
||||||
|
.filter((f): boolean =>
|
||||||
|
fs.statSync(path.join(ProjectPath.ExtensionFolder, f)).isDirectory()
|
||||||
|
);
|
||||||
|
Config.Extensions.list.sort();
|
||||||
|
Logger.debug(LOG_TAG, 'Extensions found ', JSON.stringify(Config.Extensions.list));
|
||||||
|
}
|
||||||
|
|
||||||
|
private createUniqueExtensionObject(name: string, folder: string): IExtensionObject<unknown> {
|
||||||
|
let id = name;
|
||||||
|
if (this.extObjects[id]) {
|
||||||
|
let i = 0;
|
||||||
|
while (this.extObjects[`${name}_${++i}`]) { /* empty */
|
||||||
|
}
|
||||||
|
id = `${name}_${++i}`;
|
||||||
|
}
|
||||||
|
if (!this.extObjects[id]) {
|
||||||
|
this.extObjects[id] = new ExtensionObject(id, name, folder, this.router, this.events);
|
||||||
|
}
|
||||||
|
return this.extObjects[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initExtensions() {
|
||||||
|
|
||||||
|
for (let i = 0; i < Config.Extensions.list.length; ++i) {
|
||||||
|
const extFolder = Config.Extensions.list[i];
|
||||||
|
let extName = extFolder;
|
||||||
|
const extPath = path.join(ProjectPath.ExtensionFolder, extFolder);
|
||||||
|
const serverExtPath = path.join(extPath, 'server.js');
|
||||||
|
const packageJsonPath = path.join(extPath, 'package.json');
|
||||||
|
if (!fs.existsSync(serverExtPath)) {
|
||||||
|
Logger.silly(LOG_TAG, `Skipping ${extFolder} server initiation. server.js does not exists`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(packageJsonPath)) {
|
||||||
|
Logger.silly(LOG_TAG, `Running: "npm install --prefer-offline --no-audit --progress=false --omit=dev" in ${extPath}`);
|
||||||
|
await exec('npm install --no-audit --progress=false --omit=dev', {
|
||||||
|
cwd: extPath
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const pkg = require(packageJsonPath);
|
||||||
|
if (pkg.name) {
|
||||||
|
extName = pkg.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const ext = require(serverExtPath);
|
||||||
|
if (typeof ext?.init === 'function') {
|
||||||
|
Logger.debug(LOG_TAG, 'Running init on extension: ' + extFolder);
|
||||||
|
await ext?.init(this.createUniqueExtensionObject(extName, extPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Config.Extensions.cleanUpUnusedTables) {
|
||||||
|
// Clean up tables after all Extension was initialized.
|
||||||
|
await SQLConnection.removeUnusedTables();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async cleanUpExtensions() {
|
||||||
|
for (const extObj of Object.values(this.extObjects)) {
|
||||||
|
const serverExt = path.join(extObj.folder, 'server.js');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const ext = require(serverExt);
|
||||||
|
if (typeof ext?.cleanUp === 'function') {
|
||||||
|
Logger.debug(LOG_TAG, 'Running Init on extension:' + extObj.extensionName);
|
||||||
|
await ext?.cleanUp(extObj);
|
||||||
|
}
|
||||||
|
extObj.messengers.cleanUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async cleanUp() {
|
||||||
|
if (!Config.Extensions.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.initEvents(); // reset events
|
||||||
|
await this.cleanUpExtensions();
|
||||||
|
Server.getInstance().app.use(ExtensionManager.EXTENSION_API_PATH, express.Router());
|
||||||
|
this.extObjects = {};
|
||||||
|
}
|
||||||
|
}
|
31
src/backend/model/extension/ExtensionMessengerHandler.ts
Normal file
31
src/backend/model/extension/ExtensionMessengerHandler.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import {IExtensionMessengers} from './IExtension';
|
||||||
|
import {DynamicConfig} from '../../../common/entities/DynamicConfig';
|
||||||
|
import {MediaDTOWithThPath, Messenger} from '../messenger/Messenger';
|
||||||
|
import {ExtensionMessenger} from '../messenger/ExtensionMessenger';
|
||||||
|
import {MessengerRepository} from '../messenger/MessengerRepository';
|
||||||
|
import {ILogger} from '../../Logger';
|
||||||
|
|
||||||
|
export class ExtensionMessengerHandler implements IExtensionMessengers {
|
||||||
|
|
||||||
|
messengers: Messenger[] = [];
|
||||||
|
|
||||||
|
|
||||||
|
constructor(private readonly extLogger: ILogger) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
addMessenger<C extends Record<string, unknown>>(name: string, config: DynamicConfig[], callbacks: {
|
||||||
|
sendMedia: (config: C, media: MediaDTOWithThPath[]) => Promise<void>
|
||||||
|
}): void {
|
||||||
|
this.extLogger.silly('Adding new Messenger:', name);
|
||||||
|
const em = new ExtensionMessenger(name, config, callbacks);
|
||||||
|
this.messengers.push(em);
|
||||||
|
MessengerRepository.Instance.register(em);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanUp() {
|
||||||
|
this.extLogger.silly('Removing Messenger');
|
||||||
|
this.messengers.forEach(m => MessengerRepository.Instance.remove(m));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
38
src/backend/model/extension/ExtensionObject.ts
Normal file
38
src/backend/model/extension/ExtensionObject.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import {IExtensionEvents, IExtensionObject} from './IExtension';
|
||||||
|
import {ExtensionApp} from './ExtensionApp';
|
||||||
|
import {ExtensionConfig} from './ExtensionConfigWrapper';
|
||||||
|
import {ExtensionDB} from './ExtensionDB';
|
||||||
|
import {ProjectPath} from '../../ProjectPath';
|
||||||
|
import {ExpressRouterWrapper} from './ExpressRouterWrapper';
|
||||||
|
import {createLoggerWrapper} from '../../Logger';
|
||||||
|
import * as express from 'express';
|
||||||
|
import {ExtensionMessengerHandler} from './ExtensionMessengerHandler';
|
||||||
|
|
||||||
|
export class ExtensionObject<C> implements IExtensionObject<C> {
|
||||||
|
|
||||||
|
public readonly _app;
|
||||||
|
public readonly config;
|
||||||
|
public readonly db;
|
||||||
|
public readonly paths;
|
||||||
|
public readonly Logger;
|
||||||
|
public readonly events;
|
||||||
|
public readonly RESTApi;
|
||||||
|
public readonly messengers;
|
||||||
|
|
||||||
|
constructor(public readonly extensionId: string,
|
||||||
|
public readonly extensionName: string,
|
||||||
|
public readonly folder: string,
|
||||||
|
extensionRouter: express.Router,
|
||||||
|
events: IExtensionEvents) {
|
||||||
|
const logger = createLoggerWrapper(`[Extension][${extensionId}]`);
|
||||||
|
this._app = new ExtensionApp();
|
||||||
|
this.config = new ExtensionConfig<C>(extensionId);
|
||||||
|
this.db = new ExtensionDB(logger);
|
||||||
|
this.paths = ProjectPath;
|
||||||
|
this.Logger = logger;
|
||||||
|
this.events = events;
|
||||||
|
this.RESTApi = new ExpressRouterWrapper(extensionRouter, extensionId, logger);
|
||||||
|
this.messengers = new ExtensionMessengerHandler(logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
199
src/backend/model/extension/IExtension.ts
Normal file
199
src/backend/model/extension/IExtension.ts
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import * as express from 'express';
|
||||||
|
import {NextFunction, Request, Response} from 'express';
|
||||||
|
import {PrivateConfigClass} from '../../../common/config/private/Config';
|
||||||
|
import {ObjectManagers} from '../ObjectManagers';
|
||||||
|
import {ProjectPathClass} from '../../ProjectPath';
|
||||||
|
import {ILogger} from '../../Logger';
|
||||||
|
import {UserDTO, UserRoles} from '../../../common/entities/UserDTO';
|
||||||
|
import {ParamsDictionary} from 'express-serve-static-core';
|
||||||
|
import {Connection} from 'typeorm';
|
||||||
|
import {DynamicConfig} from '../../../common/entities/DynamicConfig';
|
||||||
|
import {MediaDTOWithThPath} from '../messenger/Messenger';
|
||||||
|
|
||||||
|
|
||||||
|
export type IExtensionBeforeEventHandler<I, O> = (input: { inputs: I }, event: { stopPropagation: boolean }) => Promise<{ inputs: I } | O>;
|
||||||
|
export type IExtensionAfterEventHandler<O> = (output: O) => Promise<O>;
|
||||||
|
|
||||||
|
|
||||||
|
export interface IExtensionEvent<I, O> {
|
||||||
|
before: (handler: IExtensionBeforeEventHandler<I, O>) => void;
|
||||||
|
after: (handler: IExtensionAfterEventHandler<O>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All main event callbacks in the app
|
||||||
|
*/
|
||||||
|
export interface IExtensionEvents {
|
||||||
|
gallery: {
|
||||||
|
/**
|
||||||
|
* Events for Directory and Album covers
|
||||||
|
*/
|
||||||
|
CoverManager: {
|
||||||
|
getCoverForAlbum: IExtensionEvent<any, any>;
|
||||||
|
getCoverForDirectory: IExtensionEvent<any, any>
|
||||||
|
/**
|
||||||
|
* Invalidates directory covers for a given directory and every parent
|
||||||
|
*/
|
||||||
|
invalidateDirectoryCovers: IExtensionEvent<any, any>;
|
||||||
|
},
|
||||||
|
ImageRenderer: {
|
||||||
|
/**
|
||||||
|
* Renders a thumbnail or photo
|
||||||
|
*/
|
||||||
|
render: IExtensionEvent<any, any>
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Reads exif, iptc, etc.. metadata for photos/videos
|
||||||
|
*/
|
||||||
|
MetadataLoader: {
|
||||||
|
loadVideoMetadata: IExtensionEvent<any, any>,
|
||||||
|
loadPhotoMetadata: IExtensionEvent<any, any>
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Scans the storage for a given directory and returns the list of child directories,
|
||||||
|
* photos, videos and metafiles
|
||||||
|
*/
|
||||||
|
DiskManager: {
|
||||||
|
scanDirectory: IExtensionEvent<any, any>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExtensionApp {
|
||||||
|
expressApp: express.Express;
|
||||||
|
objectManagers: ObjectManagers;
|
||||||
|
config: PrivateConfigClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExtensionRESTRoute {
|
||||||
|
/**
|
||||||
|
* Sends a pigallery2 standard JSON object with payload or error message back to the client.
|
||||||
|
* @param paths RESTapi path, relative to the extension base endpoint
|
||||||
|
* @param minRole set to null to omit auer check (ie make the endpoint public)
|
||||||
|
* @param cb function callback
|
||||||
|
* @return newly added REST api path
|
||||||
|
*/
|
||||||
|
jsonResponse(paths: string[], minRole: UserRoles, cb: (params?: ParamsDictionary, body?: any, user?: UserDTO) => Promise<unknown> | unknown): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes a standard expressjs middleware
|
||||||
|
* @param paths RESTapi path, relative to the extension base endpoint
|
||||||
|
* @param minRole set to null to omit auer check (ie make the endpoint public)
|
||||||
|
* @param mw expressjs middleware
|
||||||
|
* @return newly added REST api path
|
||||||
|
*/
|
||||||
|
rawMiddleware(paths: string[], minRole: UserRoles, mw: (req: Request, res: Response, next: NextFunction) => void | Promise<void>): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExtensionRESTApi {
|
||||||
|
use: IExtensionRESTRoute;
|
||||||
|
get: IExtensionRESTRoute;
|
||||||
|
post: IExtensionRESTRoute;
|
||||||
|
put: IExtensionRESTRoute;
|
||||||
|
delete: IExtensionRESTRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExtensionDB {
|
||||||
|
/**
|
||||||
|
* Returns with a typeorm SQL connection
|
||||||
|
*/
|
||||||
|
getSQLConnection(): Promise<Connection>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds SQL tables to typeorm
|
||||||
|
* @param tables
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
setExtensionTables(tables: Function[]): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes all tables. You can use this if you van to have a foreign key to a built in table.
|
||||||
|
* Use with caution. This exposes the app's internal working.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
_getAllTables(): Function[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExtensionConfig<C> {
|
||||||
|
setTemplate(template: new() => C): void;
|
||||||
|
|
||||||
|
getConfig(): C;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExtensionMessengers {
|
||||||
|
/**
|
||||||
|
* Adds a new messenger that the user can select e.g.: for sending top pick photos
|
||||||
|
* @param name Name of the messenger (also used as id)
|
||||||
|
* @param config config metadata for this messenger
|
||||||
|
* @param callbacks messenger logic
|
||||||
|
*/
|
||||||
|
addMessenger<C extends Record<string, unknown> = Record<string, unknown>>(name: string, config: DynamicConfig[], callbacks: {
|
||||||
|
sendMedia: (config: C, media: MediaDTOWithThPath[]) => Promise<void>
|
||||||
|
}): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExtensionObject<C = void> {
|
||||||
|
/**
|
||||||
|
* ID of the extension that is internally used. By default, the name and ID matches if there is no collision.
|
||||||
|
*/
|
||||||
|
extensionId: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the extension
|
||||||
|
*/
|
||||||
|
extensionName: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inner functionality of the app. Use this with caution.
|
||||||
|
* If you want to go deeper than the standard exposed APIs, you can try doing so here.
|
||||||
|
*/
|
||||||
|
_app: IExtensionApp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create extension related configuration
|
||||||
|
*/
|
||||||
|
config: IExtensionConfig<C>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new SQL tables and access SQL connection
|
||||||
|
*/
|
||||||
|
db: IExtensionDB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paths to the main components of the app.
|
||||||
|
*/
|
||||||
|
paths: ProjectPathClass;
|
||||||
|
/**
|
||||||
|
* Logger of the app
|
||||||
|
*/
|
||||||
|
Logger: ILogger;
|
||||||
|
/**
|
||||||
|
* Main app events. Use this change indexing, cover or serving gallery
|
||||||
|
*/
|
||||||
|
events: IExtensionEvents;
|
||||||
|
/**
|
||||||
|
* Use this to define REST calls related to the extension
|
||||||
|
*/
|
||||||
|
RESTApi: IExtensionRESTApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object to manipulate messengers.
|
||||||
|
* Messengers are used to send messages (like emails) from the app.
|
||||||
|
* One type of message is a list of selected photos.
|
||||||
|
*/
|
||||||
|
messengers: IExtensionMessengers;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension interface. All extension is expected to implement and export these methods
|
||||||
|
*/
|
||||||
|
export interface IServerExtension<C> {
|
||||||
|
/**
|
||||||
|
* Extension init function. Extension should at minimum expose this function.
|
||||||
|
* @param extension
|
||||||
|
*/
|
||||||
|
init(extension: IExtensionObject<C>): Promise<void>;
|
||||||
|
|
||||||
|
cleanUp?: (extension: IExtensionObject<C>) => Promise<void>;
|
||||||
|
}
|
@ -14,6 +14,7 @@ import {GPXProcessing} from './fileprocessing/GPXProcessing';
|
|||||||
import {MDFileDTO} from '../../../common/entities/MDFileDTO';
|
import {MDFileDTO} from '../../../common/entities/MDFileDTO';
|
||||||
import {MetadataLoader} from './MetadataLoader';
|
import {MetadataLoader} from './MetadataLoader';
|
||||||
import {NotificationManager} from '../NotifocationManager';
|
import {NotificationManager} from '../NotifocationManager';
|
||||||
|
import {ExtensionDecorator} from '../extension/ExtensionDecorator';
|
||||||
|
|
||||||
|
|
||||||
const LOG_TAG = '[DiskManager]';
|
const LOG_TAG = '[DiskManager]';
|
||||||
@ -101,6 +102,7 @@ export class DiskManager {
|
|||||||
)) as ParentDirectoryDTO<FileDTO>;
|
)) as ParentDirectoryDTO<FileDTO>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExtensionDecorator(e => e.gallery.DiskManager.scanDirectory)
|
||||||
public static async scanDirectory(
|
public static async scanDirectory(
|
||||||
relativeDirectoryName: string,
|
relativeDirectoryName: string,
|
||||||
settings: DirectoryScanSettings = {}
|
settings: DirectoryScanSettings = {}
|
||||||
|
@ -12,6 +12,7 @@ import {IptcParser} from 'ts-node-iptc';
|
|||||||
import {FFmpegFactory} from '../FFmpegFactory';
|
import {FFmpegFactory} from '../FFmpegFactory';
|
||||||
import {FfprobeData} from 'fluent-ffmpeg';
|
import {FfprobeData} from 'fluent-ffmpeg';
|
||||||
import {Utils} from '../../../common/Utils';
|
import {Utils} from '../../../common/Utils';
|
||||||
|
import { ExtensionDecorator } from '../extension/ExtensionDecorator';
|
||||||
import * as exifr from 'exifr';
|
import * as exifr from 'exifr';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
@ -19,6 +20,8 @@ const LOG_TAG = '[MetadataLoader]';
|
|||||||
const ffmpeg = FFmpegFactory.get();
|
const ffmpeg = FFmpegFactory.get();
|
||||||
|
|
||||||
export class MetadataLoader {
|
export class MetadataLoader {
|
||||||
|
|
||||||
|
@ExtensionDecorator(e=>e.gallery.MetadataLoader.loadVideoMetadata)
|
||||||
public static loadVideoMetadata(fullPath: string): Promise<VideoMetadata> {
|
public static loadVideoMetadata(fullPath: string): Promise<VideoMetadata> {
|
||||||
return new Promise<VideoMetadata>((resolve) => {
|
return new Promise<VideoMetadata>((resolve) => {
|
||||||
const metadata: VideoMetadata = {
|
const metadata: VideoMetadata = {
|
||||||
@ -153,6 +156,7 @@ export class MetadataLoader {
|
|||||||
fileSize: 0,
|
fileSize: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ExtensionDecorator(e=>e.gallery.MetadataLoader.loadPhotoMetadata)
|
||||||
public static loadPhotoMetadata(fullPath: string): Promise<PhotoMetadata> {
|
public static loadPhotoMetadata(fullPath: string): Promise<PhotoMetadata> {
|
||||||
return new Promise<PhotoMetadata>((resolve, reject) => {
|
return new Promise<PhotoMetadata>((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
|
@ -5,6 +5,7 @@ import {Logger} from '../../Logger';
|
|||||||
import {FfmpegCommand, FfprobeData} from 'fluent-ffmpeg';
|
import {FfmpegCommand, FfprobeData} from 'fluent-ffmpeg';
|
||||||
import {FFmpegFactory} from '../FFmpegFactory';
|
import {FFmpegFactory} from '../FFmpegFactory';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import {ExtensionDecorator} from '../extension/ExtensionDecorator';
|
||||||
|
|
||||||
|
|
||||||
sharp.cache(false);
|
sharp.cache(false);
|
||||||
@ -129,6 +130,7 @@ export class VideoRendererFactory {
|
|||||||
|
|
||||||
export class ImageRendererFactory {
|
export class ImageRendererFactory {
|
||||||
|
|
||||||
|
@ExtensionDecorator(e=>e.gallery.ImageRenderer.render)
|
||||||
public static async render(input: MediaRendererInput | SvgRendererInput): Promise<void> {
|
public static async render(input: MediaRendererInput | SvgRendererInput): Promise<void> {
|
||||||
|
|
||||||
let image: Sharp;
|
let image: Sharp;
|
||||||
|
@ -10,14 +10,15 @@ import {JobProgress} from './jobs/JobProgress';
|
|||||||
import {JobProgressManager} from './JobProgressManager';
|
import {JobProgressManager} from './JobProgressManager';
|
||||||
import {JobDTOUtils} from '../../../common/entities/job/JobDTO';
|
import {JobDTOUtils} from '../../../common/entities/job/JobDTO';
|
||||||
import {Utils} from '../../../common/Utils';
|
import {Utils} from '../../../common/Utils';
|
||||||
|
import {IObjectManager} from '../database/IObjectManager';
|
||||||
|
|
||||||
const LOG_TAG = '[JobManager]';
|
const LOG_TAG = '[JobManager]';
|
||||||
|
|
||||||
export class JobManager implements IJobListener {
|
export class JobManager implements IJobListener, IObjectManager {
|
||||||
protected timers: { schedule: JobScheduleDTO; timer: NodeJS.Timeout }[] = [];
|
protected timers: { schedule: JobScheduleDTO; timer: NodeJS.Timeout }[] = [];
|
||||||
protected progressManager: JobProgressManager = null;
|
protected progressManager: JobProgressManager = null;
|
||||||
|
|
||||||
constructor() {
|
async init(){
|
||||||
this.progressManager = new JobProgressManager();
|
this.progressManager = new JobProgressManager();
|
||||||
this.runSchedules();
|
this.runSchedules();
|
||||||
}
|
}
|
||||||
@ -49,7 +50,7 @@ export class JobManager implements IJobListener {
|
|||||||
return prg;
|
return prg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run<T>(
|
public async run<T extends Record<string, unknown>>(
|
||||||
jobName: string,
|
jobName: string,
|
||||||
config: T,
|
config: T,
|
||||||
soloRun: boolean,
|
soloRun: boolean,
|
||||||
@ -85,7 +86,7 @@ export class JobManager implements IJobListener {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onJobFinished = async (
|
onJobFinished = async (
|
||||||
job: IJob<unknown>,
|
job: IJob,
|
||||||
state: JobProgressStates,
|
state: JobProgressStates,
|
||||||
soloRun: boolean
|
soloRun: boolean
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
@ -120,11 +121,16 @@ export class JobManager implements IJobListener {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getAvailableJobs(): IJob<unknown>[] {
|
getAvailableJobs(): IJob[] {
|
||||||
return JobRepository.Instance.getAvailableJobs();
|
return JobRepository.Instance.getAvailableJobs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async cleanUp() {
|
||||||
|
this.stopSchedules();
|
||||||
|
}
|
||||||
|
|
||||||
public stopSchedules(): void {
|
public stopSchedules(): void {
|
||||||
|
Logger.silly(LOG_TAG, 'Stopping all schedules');
|
||||||
this.timers.forEach((t): void => clearTimeout(t.timer));
|
this.timers.forEach((t): void => clearTimeout(t.timer));
|
||||||
this.timers = [];
|
this.timers = [];
|
||||||
}
|
}
|
||||||
@ -138,7 +144,7 @@ export class JobManager implements IJobListener {
|
|||||||
Config.Jobs.scheduled.forEach((s): void => this.runSchedule(s));
|
Config.Jobs.scheduled.forEach((s): void => this.runSchedule(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected findJob<T = unknown>(jobName: string): IJob<T> {
|
protected findJob(jobName: string): IJob {
|
||||||
return this.getAvailableJobs().find((t): boolean => t.Name === jobName);
|
return this.getAvailableJobs().find((t): boolean => t.Name === jobName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import {AlbumCoverRestJob} from './jobs/AlbumCoverResetJob';
|
|||||||
|
|
||||||
export class JobRepository {
|
export class JobRepository {
|
||||||
private static instance: JobRepository = null;
|
private static instance: JobRepository = null;
|
||||||
availableJobs: { [key: string]: IJob<unknown> } = {};
|
availableJobs: { [key: string]: IJob } = {};
|
||||||
|
|
||||||
public static get Instance(): JobRepository {
|
public static get Instance(): JobRepository {
|
||||||
if (JobRepository.instance == null) {
|
if (JobRepository.instance == null) {
|
||||||
@ -23,11 +23,11 @@ export class JobRepository {
|
|||||||
return JobRepository.instance;
|
return JobRepository.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAvailableJobs(): IJob<unknown>[] {
|
getAvailableJobs(): IJob[] {
|
||||||
return Object.values(this.availableJobs).filter((t) => t.Supported);
|
return Object.values(this.availableJobs).filter((t) => t.Supported);
|
||||||
}
|
}
|
||||||
|
|
||||||
register(job: IJob<unknown>): void {
|
register(job: IJob): void {
|
||||||
if (typeof this.availableJobs[job.Name] !== 'undefined') {
|
if (typeof this.availableJobs[job.Name] !== 'undefined') {
|
||||||
throw new Error('Job already exist:' + job.Name);
|
throw new Error('Job already exist:' + job.Name);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import {ObjectManagers} from '../../ObjectManagers';
|
import {ObjectManagers} from '../../ObjectManagers';
|
||||||
import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
|
||||||
import {Job} from './Job';
|
import {Job} from './Job';
|
||||||
|
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||||
|
|
||||||
export class AlbumCoverFillingJob extends Job {
|
export class AlbumCoverFillingJob extends Job {
|
||||||
public readonly Name = DefaultsJobs[DefaultsJobs['Album Cover Filling']];
|
public readonly Name = DefaultsJobs[DefaultsJobs['Album Cover Filling']];
|
||||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = null;
|
public readonly ConfigTemplate: DynamicConfig[] = null;
|
||||||
directoryToSetCover: { id: number; name: string; path: string }[] = null;
|
directoryToSetCover: { id: number; name: string; path: string }[] = null;
|
||||||
status: 'Persons' | 'Albums' | 'Directory' = 'Persons';
|
status: 'Persons' | 'Albums' | 'Directory' = 'Persons';
|
||||||
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import {ObjectManagers} from '../../ObjectManagers';
|
import {ObjectManagers} from '../../ObjectManagers';
|
||||||
import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
|
||||||
import {Job} from './Job';
|
import {Job} from './Job';
|
||||||
|
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||||
|
|
||||||
export class AlbumCoverRestJob extends Job {
|
export class AlbumCoverRestJob extends Job {
|
||||||
public readonly Name = DefaultsJobs[DefaultsJobs['Album Cover Reset']];
|
public readonly Name = DefaultsJobs[DefaultsJobs['Album Cover Reset']];
|
||||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = null;
|
public readonly ConfigTemplate: DynamicConfig[] = null;
|
||||||
protected readonly IsInstant = true;
|
protected readonly IsInstant = true;
|
||||||
|
|
||||||
public get Supported(): boolean {
|
public get Supported(): boolean {
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import {ObjectManagers} from '../../ObjectManagers';
|
import {ObjectManagers} from '../../ObjectManagers';
|
||||||
import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
import {DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
||||||
import {Job} from './Job';
|
import {Job} from './Job';
|
||||||
|
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||||
|
|
||||||
export class AlbumRestJob extends Job {
|
export class AlbumRestJob extends Job {
|
||||||
public readonly Name = DefaultsJobs[DefaultsJobs['Album Reset']];
|
public readonly Name = DefaultsJobs[DefaultsJobs['Album Reset']];
|
||||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = null;
|
public readonly ConfigTemplate: DynamicConfig[] = null;
|
||||||
protected readonly IsInstant = true;
|
protected readonly IsInstant = true;
|
||||||
|
|
||||||
public get Supported(): boolean {
|
public get Supported(): boolean {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import {ConfigTemplateEntry} from '../../../../common/entities/job/JobDTO';
|
|
||||||
import {Job} from './Job';
|
import {Job} from './Job';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import {Logger} from '../../../Logger';
|
import {Logger} from '../../../Logger';
|
||||||
@ -13,6 +12,7 @@ import {ProjectPath} from '../../../ProjectPath';
|
|||||||
import {FileEntity} from '../../database/enitites/FileEntity';
|
import {FileEntity} from '../../database/enitites/FileEntity';
|
||||||
import {DirectoryBaseDTO, DirectoryDTOUtils} from '../../../../common/entities/DirectoryDTO';
|
import {DirectoryBaseDTO, DirectoryDTOUtils} from '../../../../common/entities/DirectoryDTO';
|
||||||
import {DirectoryScanSettings, DiskManager} from '../../fileaccess/DiskManager';
|
import {DirectoryScanSettings, DiskManager} from '../../fileaccess/DiskManager';
|
||||||
|
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||||
|
|
||||||
const LOG_TAG = '[FileJob]';
|
const LOG_TAG = '[FileJob]';
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ const LOG_TAG = '[FileJob]';
|
|||||||
* Abstract class for thumbnail creation, file deleting etc.
|
* Abstract class for thumbnail creation, file deleting etc.
|
||||||
*/
|
*/
|
||||||
export abstract class FileJob<S extends { indexedOnly?: boolean } = { indexedOnly?: boolean }> extends Job<S> {
|
export abstract class FileJob<S extends { indexedOnly?: boolean } = { indexedOnly?: boolean }> extends Job<S> {
|
||||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = [];
|
public readonly ConfigTemplate: DynamicConfig[] = [];
|
||||||
directoryQueue: string[] = [];
|
directoryQueue: string[] = [];
|
||||||
fileQueue: string[] = [];
|
fileQueue: string[] = [];
|
||||||
DBProcessing = {
|
DBProcessing = {
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import {ObjectManagers} from '../../ObjectManagers';
|
import {ObjectManagers} from '../../ObjectManagers';
|
||||||
import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
|
||||||
import {Job} from './Job';
|
import {Job} from './Job';
|
||||||
|
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||||
|
|
||||||
export class GalleryRestJob extends Job {
|
export class GalleryRestJob extends Job {
|
||||||
public readonly Name = DefaultsJobs[DefaultsJobs['Gallery Reset']];
|
public readonly Name = DefaultsJobs[DefaultsJobs['Gallery Reset']];
|
||||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = null;
|
public readonly ConfigTemplate: DynamicConfig[] = null;
|
||||||
protected readonly IsInstant = true;
|
protected readonly IsInstant = true;
|
||||||
|
|
||||||
public get Supported(): boolean {
|
public get Supported(): boolean {
|
||||||
|
@ -2,7 +2,7 @@ import {JobDTO} from '../../../../common/entities/job/JobDTO';
|
|||||||
import {JobProgress} from './JobProgress';
|
import {JobProgress} from './JobProgress';
|
||||||
import {IJobListener} from './IJobListener';
|
import {IJobListener} from './IJobListener';
|
||||||
|
|
||||||
export interface IJob<T> extends JobDTO {
|
export interface IJob<T extends Record<string, unknown> = Record<string, unknown>> extends JobDTO {
|
||||||
Name: string;
|
Name: string;
|
||||||
Supported: boolean;
|
Supported: boolean;
|
||||||
Progress: JobProgress;
|
Progress: JobProgress;
|
||||||
|
@ -4,7 +4,7 @@ import {JobProgressStates} from '../../../../common/entities/job/JobProgressDTO'
|
|||||||
|
|
||||||
export interface IJobListener {
|
export interface IJobListener {
|
||||||
onJobFinished(
|
onJobFinished(
|
||||||
job: IJob<unknown>,
|
job: IJob,
|
||||||
state: JobProgressStates,
|
state: JobProgressStates,
|
||||||
soloRun: boolean
|
soloRun: boolean
|
||||||
): void;
|
): void;
|
||||||
|
@ -2,7 +2,7 @@ import {ObjectManagers} from '../../ObjectManagers';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import {Job} from './Job';
|
import {Job} from './Job';
|
||||||
import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
|
||||||
import {JobProgressStates} from '../../../../common/entities/job/JobProgressDTO';
|
import {JobProgressStates} from '../../../../common/entities/job/JobProgressDTO';
|
||||||
import {ProjectPath} from '../../../ProjectPath';
|
import {ProjectPath} from '../../../ProjectPath';
|
||||||
import {backendTexts} from '../../../../common/BackendTexts';
|
import {backendTexts} from '../../../../common/BackendTexts';
|
||||||
@ -10,6 +10,7 @@ import {ParentDirectoryDTO} from '../../../../common/entities/DirectoryDTO';
|
|||||||
import {Logger} from '../../../Logger';
|
import {Logger} from '../../../Logger';
|
||||||
import {FileDTO} from '../../../../common/entities/FileDTO';
|
import {FileDTO} from '../../../../common/entities/FileDTO';
|
||||||
import {DiskManager} from '../../fileaccess/DiskManager';
|
import {DiskManager} from '../../fileaccess/DiskManager';
|
||||||
|
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||||
|
|
||||||
const LOG_TAG = '[IndexingJob]';
|
const LOG_TAG = '[IndexingJob]';
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ export class IndexingJob<
|
|||||||
> extends Job<S> {
|
> extends Job<S> {
|
||||||
public readonly Name = DefaultsJobs[DefaultsJobs.Indexing];
|
public readonly Name = DefaultsJobs[DefaultsJobs.Indexing];
|
||||||
directoriesToIndex: string[] = [];
|
directoriesToIndex: string[] = [];
|
||||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = [
|
public readonly ConfigTemplate: DynamicConfig[] = [
|
||||||
{
|
{
|
||||||
id: 'indexChangesOnly',
|
id: 'indexChangesOnly',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import {Logger} from '../../../Logger';
|
import {Logger} from '../../../Logger';
|
||||||
import {IJob} from './IJob';
|
import {IJob} from './IJob';
|
||||||
import {ConfigTemplateEntry, JobDTO, JobDTOUtils,} from '../../../../common/entities/job/JobDTO';
|
import {JobDTO, JobDTOUtils} from '../../../../common/entities/job/JobDTO';
|
||||||
import {JobProgress} from './JobProgress';
|
import {JobProgress} from './JobProgress';
|
||||||
import {IJobListener} from './IJobListener';
|
import {IJobListener} from './IJobListener';
|
||||||
import {JobProgressStates} from '../../../../common/entities/job/JobProgressDTO';
|
import {JobProgressStates} from '../../../../common/entities/job/JobProgressDTO';
|
||||||
|
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||||
|
|
||||||
declare const process: { nextTick: (_: unknown) => void };
|
declare const process: { nextTick: (_: unknown) => void };
|
||||||
declare const global: { gc: () => void };
|
declare const global: { gc: () => void };
|
||||||
@ -27,7 +28,7 @@ export abstract class Job<T extends Record<string, unknown> = Record<string, unk
|
|||||||
|
|
||||||
public abstract get Name(): string;
|
public abstract get Name(): string;
|
||||||
|
|
||||||
public abstract get ConfigTemplate(): ConfigTemplateEntry[];
|
public abstract get ConfigTemplate(): DynamicConfig[];
|
||||||
|
|
||||||
public get Progress(): JobProgress {
|
public get Progress(): JobProgress {
|
||||||
return this.progress;
|
return this.progress;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import {Job} from './Job';
|
import {Job} from './Job';
|
||||||
@ -6,10 +6,11 @@ import {ProjectPath} from '../../../ProjectPath';
|
|||||||
import {GPXProcessing} from '../../fileaccess/fileprocessing/GPXProcessing';
|
import {GPXProcessing} from '../../fileaccess/fileprocessing/GPXProcessing';
|
||||||
import {PhotoProcessing} from '../../fileaccess/fileprocessing/PhotoProcessing';
|
import {PhotoProcessing} from '../../fileaccess/fileprocessing/PhotoProcessing';
|
||||||
import {VideoProcessing} from '../../fileaccess/fileprocessing/VideoProcessing';
|
import {VideoProcessing} from '../../fileaccess/fileprocessing/VideoProcessing';
|
||||||
|
import { DynamicConfig } from '../../../../common/entities/DynamicConfig';
|
||||||
|
|
||||||
export class TempFolderCleaningJob extends Job {
|
export class TempFolderCleaningJob extends Job {
|
||||||
public readonly Name = DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']];
|
public readonly Name = DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']];
|
||||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = null;
|
public readonly ConfigTemplate: DynamicConfig[] = null;
|
||||||
public readonly Supported: boolean = true;
|
public readonly Supported: boolean = true;
|
||||||
directoryQueue: string[] = [];
|
directoryQueue: string[] = [];
|
||||||
private tempRootCleaned = false;
|
private tempRootCleaned = false;
|
||||||
|
@ -35,7 +35,7 @@ export class ThumbnailGenerationJob extends FileJob<{
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!config || !config.sizes || !Array.isArray(config.sizes) || config.sizes.length === 0) {
|
if (!config || !config.sizes || !Array.isArray(config.sizes) || config.sizes.length === 0) {
|
||||||
config = config || {};
|
config = config || {};
|
||||||
config.sizes = this.ConfigTemplate.find(ct => ct.id == 'sizes').defaultValue;
|
config.sizes = this.ConfigTemplate.find(ct => ct.id == 'sizes').defaultValue as number[];
|
||||||
}
|
}
|
||||||
for (const item of config.sizes) {
|
for (const item of config.sizes) {
|
||||||
if (Config.Media.Thumbnail.thumbnailSizes.indexOf(item) === -1) {
|
if (Config.Media.Thumbnail.thumbnailSizes.indexOf(item) === -1) {
|
||||||
|
@ -1,25 +1,33 @@
|
|||||||
import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
import {DefaultMessengers, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
||||||
import {Job} from './Job';
|
import {Job} from './Job';
|
||||||
import {backendTexts} from '../../../../common/BackendTexts';
|
import {backendTexts} from '../../../../common/BackendTexts';
|
||||||
import {SortByTypes} from '../../../../common/entities/SortingMethods';
|
import {SortByTypes} from '../../../../common/entities/SortingMethods';
|
||||||
import {DatePatternFrequency, DatePatternSearch, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO';
|
import {DatePatternFrequency, DatePatternSearch, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO';
|
||||||
import {ObjectManagers} from '../../ObjectManagers';
|
import {ObjectManagers} from '../../ObjectManagers';
|
||||||
import {PhotoEntity} from '../../database/enitites/PhotoEntity';
|
import {PhotoEntity} from '../../database/enitites/PhotoEntity';
|
||||||
import {EmailMediaMessenger} from '../../mediamessengers/EmailMediaMessenger';
|
|
||||||
import {MediaPickDTO} from '../../../../common/entities/MediaPickDTO';
|
import {MediaPickDTO} from '../../../../common/entities/MediaPickDTO';
|
||||||
import {MediaDTOUtils} from '../../../../common/entities/MediaDTO';
|
import {MediaDTOUtils} from '../../../../common/entities/MediaDTO';
|
||||||
|
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||||
|
import {MessengerRepository} from '../../messenger/MessengerRepository';
|
||||||
|
import {Utils} from '../../../../common/Utils';
|
||||||
|
|
||||||
|
|
||||||
export class TopPickSendJob extends Job<{
|
export class TopPickSendJob extends Job<{
|
||||||
mediaPick: MediaPickDTO[],
|
mediaPick: MediaPickDTO[],
|
||||||
|
messenger: string,
|
||||||
emailTo: string,
|
emailTo: string,
|
||||||
emailFrom: string,
|
|
||||||
emailSubject: string,
|
emailSubject: string,
|
||||||
emailText: string,
|
emailText: string,
|
||||||
}> {
|
}> {
|
||||||
public readonly Name = DefaultsJobs[DefaultsJobs['Top Pick Sending']];
|
public readonly Name = DefaultsJobs[DefaultsJobs['Top Pick Sending']];
|
||||||
public readonly Supported: boolean = true;
|
public readonly Supported: boolean = true;
|
||||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = [
|
public readonly ConfigTemplate: DynamicConfig[];
|
||||||
|
private status: 'Listing' | 'Sending' = 'Listing';
|
||||||
|
private mediaList: PhotoEntity[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.ConfigTemplate = [
|
||||||
{
|
{
|
||||||
id: 'mediaPick',
|
id: 'mediaPick',
|
||||||
type: 'MediaPickDTO-array',
|
type: 'MediaPickDTO-array',
|
||||||
@ -36,27 +44,23 @@ export class TopPickSendJob extends Job<{
|
|||||||
pick: 5
|
pick: 5
|
||||||
}] as MediaPickDTO[],
|
}] as MediaPickDTO[],
|
||||||
}, {
|
}, {
|
||||||
id: 'emailTo',
|
id: 'messenger',
|
||||||
type: 'string-array',
|
type: 'messenger',
|
||||||
name: backendTexts.emailTo.name,
|
name: backendTexts.messenger.name,
|
||||||
description: backendTexts.emailTo.description,
|
description: backendTexts.messenger.description,
|
||||||
defaultValue: [],
|
defaultValue: DefaultMessengers[DefaultMessengers.Email]
|
||||||
}, {
|
}
|
||||||
id: 'emailSubject',
|
|
||||||
type: 'string',
|
|
||||||
name: backendTexts.emailSubject.name,
|
|
||||||
description: backendTexts.emailSubject.description,
|
|
||||||
defaultValue: 'Latest photos for you',
|
|
||||||
}, {
|
|
||||||
id: 'emailText',
|
|
||||||
type: 'string',
|
|
||||||
name: backendTexts.emailText.name,
|
|
||||||
description: backendTexts.emailText.description,
|
|
||||||
defaultValue: 'I hand picked these photos just for you:',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
private status: 'Listing' | 'Sending' = 'Listing';
|
|
||||||
private mediaList: PhotoEntity[] = [];
|
// add all messenger's config to the config template
|
||||||
|
MessengerRepository.Instance.getAll()
|
||||||
|
.forEach(msgr => Utils.clone(msgr.ConfigTemplate)
|
||||||
|
.forEach(ct => {
|
||||||
|
const c = Utils.clone(ct);
|
||||||
|
c.validIf = {configFiled: 'messenger', equalsValue: msgr.Name};
|
||||||
|
this.ConfigTemplate.push(c);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
protected async init(): Promise<void> {
|
protected async init(): Promise<void> {
|
||||||
@ -102,17 +106,16 @@ export class TopPickSendJob extends Job<{
|
|||||||
|
|
||||||
private async stepSending(): Promise<boolean> {
|
private async stepSending(): Promise<boolean> {
|
||||||
if (this.mediaList.length <= 0) {
|
if (this.mediaList.length <= 0) {
|
||||||
this.Progress.log('No photos found skipping e-mail sending.');
|
this.Progress.log('No photos found skipping sending.');
|
||||||
this.Progress.Skipped++;
|
this.Progress.Skipped++;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.Progress.log('Sending emails of ' + this.mediaList.length + ' photos.');
|
const msgr = MessengerRepository.Instance.get(this.config.messenger);
|
||||||
const messenger = new EmailMediaMessenger();
|
if (!msgr) {
|
||||||
await messenger.sendMedia({
|
throw new Error('Can\t find "' + this.config.messenger + '" messenger.');
|
||||||
to: this.config.emailTo,
|
}
|
||||||
subject: this.config.emailSubject,
|
this.Progress.log('Sending ' + this.mediaList.length + ' photos.');
|
||||||
text: this.config.emailText
|
await msgr.send(this.config, this.mediaList);
|
||||||
}, this.mediaList);
|
|
||||||
this.Progress.Processed++;
|
this.Progress.Processed++;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,40 @@
|
|||||||
import {createTransport, Transporter} from 'nodemailer';
|
import {createTransport, Transporter} from 'nodemailer';
|
||||||
import {MediaDTO, MediaDTOUtils} from '../../../common/entities/MediaDTO';
|
|
||||||
import {Config} from '../../../common/config/private/Config';
|
import {Config} from '../../../common/config/private/Config';
|
||||||
import {PhotoProcessing} from '../fileaccess/fileprocessing/PhotoProcessing';
|
|
||||||
import {ThumbnailSourceType} from '../fileaccess/PhotoWorker';
|
|
||||||
import {ProjectPath} from '../../ProjectPath';
|
|
||||||
import * as path from 'path';
|
|
||||||
import {PhotoMetadata} from '../../../common/entities/PhotoDTO';
|
import {PhotoMetadata} from '../../../common/entities/PhotoDTO';
|
||||||
import {Utils} from '../../../common/Utils';
|
import {MediaDTOWithThPath, Messenger} from './Messenger';
|
||||||
import {QueryParams} from '../../../common/QueryParams';
|
import {backendTexts} from '../../../common/BackendTexts';
|
||||||
|
import {DynamicConfig} from '../../../common/entities/DynamicConfig';
|
||||||
|
import {DefaultMessengers} from '../../../common/entities/job/JobDTO';
|
||||||
|
|
||||||
export class EmailMediaMessenger {
|
export class EmailMessenger extends Messenger<{
|
||||||
|
emailTo: string,
|
||||||
|
emailSubject: string,
|
||||||
|
emailText: string,
|
||||||
|
}> {
|
||||||
|
public readonly Name = DefaultMessengers[DefaultMessengers.Email];
|
||||||
|
public readonly ConfigTemplate: DynamicConfig[] = [{
|
||||||
|
id: 'emailTo',
|
||||||
|
type: 'string-array',
|
||||||
|
name: backendTexts.emailTo.name,
|
||||||
|
description: backendTexts.emailTo.description,
|
||||||
|
defaultValue: [],
|
||||||
|
}, {
|
||||||
|
id: 'emailSubject',
|
||||||
|
type: 'string',
|
||||||
|
name: backendTexts.emailSubject.name,
|
||||||
|
description: backendTexts.emailSubject.description,
|
||||||
|
defaultValue: 'Latest photos for you',
|
||||||
|
}, {
|
||||||
|
id: 'emailText',
|
||||||
|
type: 'string',
|
||||||
|
name: backendTexts.emailText.name,
|
||||||
|
description: backendTexts.emailText.description,
|
||||||
|
defaultValue: 'I hand picked these photos just for you:',
|
||||||
|
}];
|
||||||
transporter: Transporter;
|
transporter: Transporter;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
super();
|
||||||
this.transporter = createTransport({
|
this.transporter = createTransport({
|
||||||
host: Config.Messaging.Email.smtp.host,
|
host: Config.Messaging.Email.smtp.host,
|
||||||
port: Config.Messaging.Email.smtp.port,
|
port: Config.Messaging.Email.smtp.port,
|
||||||
@ -25,24 +47,16 @@ export class EmailMediaMessenger {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getThumbnail(m: MediaDTO) {
|
|
||||||
return await PhotoProcessing.generateThumbnail(
|
|
||||||
path.join(ProjectPath.ImageFolder, m.directory.path, m.directory.name, m.name),
|
|
||||||
Config.Media.Thumbnail.thumbnailSizes[0],
|
|
||||||
MediaDTOUtils.isPhoto(m) ? ThumbnailSourceType.Photo : ThumbnailSourceType.Video,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async sendMedia(mailSettings: {
|
protected async sendMedia(mailSettings: {
|
||||||
to: string,
|
emailTo: string,
|
||||||
subject: string,
|
emailSubject: string,
|
||||||
text: string
|
emailText: string
|
||||||
}, media: MediaDTO[]) {
|
}, media: MediaDTOWithThPath[]) {
|
||||||
|
|
||||||
const attachments = [];
|
const attachments = [];
|
||||||
const htmlStart = '<h1 style="text-align: center; margin-bottom: 2em">' + Config.Server.applicationTitle + '</h1>\n' +
|
const htmlStart = '<h1 style="text-align: center; margin-bottom: 2em">' + Config.Server.applicationTitle + '</h1>\n' +
|
||||||
'<h3>' + mailSettings.text + '</h3>\n' +
|
'<h3>' + mailSettings.emailText + '</h3>\n' +
|
||||||
'<table style="margin-left: auto; margin-right: auto;">\n' +
|
'<table style="margin-left: auto; margin-right: auto;">\n' +
|
||||||
' <tbody>\n';
|
' <tbody>\n';
|
||||||
const htmlEnd = ' </tr>\n' +
|
const htmlEnd = ' </tr>\n' +
|
||||||
@ -51,9 +65,6 @@ export class EmailMediaMessenger {
|
|||||||
let htmlMiddle = '';
|
let htmlMiddle = '';
|
||||||
const numberOfColumns = media.length >= 6 ? 3 : 2;
|
const numberOfColumns = media.length >= 6 ? 3 : 2;
|
||||||
for (let i = 0; i < media.length; ++i) {
|
for (let i = 0; i < media.length; ++i) {
|
||||||
const thPath = await this.getThumbnail(media[i]);
|
|
||||||
const linkUrl = Utils.concatUrls(Config.Server.publicUrl, '/gallery/', encodeURIComponent(path.join(media[i].directory.path, media[i].directory.name))) +
|
|
||||||
'?' + QueryParams.gallery.photo + '=' + encodeURIComponent(media[i].name);
|
|
||||||
const location = (media[i].metadata as PhotoMetadata).positionData?.country ?
|
const location = (media[i].metadata as PhotoMetadata).positionData?.country ?
|
||||||
(media[i].metadata as PhotoMetadata).positionData?.country :
|
(media[i].metadata as PhotoMetadata).positionData?.country :
|
||||||
((media[i].metadata as PhotoMetadata).positionData?.city ?
|
((media[i].metadata as PhotoMetadata).positionData?.city ?
|
||||||
@ -61,14 +72,14 @@ export class EmailMediaMessenger {
|
|||||||
const caption = (new Date(media[i].metadata.creationDate)).getFullYear() + (location ? ', ' + location : '');
|
const caption = (new Date(media[i].metadata.creationDate)).getFullYear() + (location ? ', ' + location : '');
|
||||||
attachments.push({
|
attachments.push({
|
||||||
filename: media[i].name,
|
filename: media[i].name,
|
||||||
path: thPath,
|
path: media[i].thumbnailPath,
|
||||||
cid: 'img' + i
|
cid: 'img' + i
|
||||||
});
|
});
|
||||||
if (i % numberOfColumns == 0) {
|
if (i % numberOfColumns == 0) {
|
||||||
htmlMiddle += '<tr>';
|
htmlMiddle += '<tr>';
|
||||||
}
|
}
|
||||||
htmlMiddle += '<td>\n' +
|
htmlMiddle += '<td>\n' +
|
||||||
' <a style="display: block;text-align: center;" href="' + linkUrl + '"><img alt="' + media[i].name + '" style="max-width: 200px; max-height: 150px; height:auto; width:auto;" src="cid:img' + i + '"/></a>\n' +
|
' <a style="display: block;text-align: center;" href="' + media[i].thumbnailUrl + '"><img alt="' + media[i].name + '" style="max-width: 200px; max-height: 150px; height:auto; width:auto;" src="cid:img' + i + '"/></a>\n' +
|
||||||
caption +
|
caption +
|
||||||
' </td>\n';
|
' </td>\n';
|
||||||
|
|
||||||
@ -79,8 +90,8 @@ export class EmailMediaMessenger {
|
|||||||
|
|
||||||
return await this.transporter.sendMail({
|
return await this.transporter.sendMail({
|
||||||
from: Config.Messaging.Email.emailFrom,
|
from: Config.Messaging.Email.emailFrom,
|
||||||
to: mailSettings.to,
|
to: mailSettings.emailTo,
|
||||||
subject: mailSettings.subject,
|
subject: mailSettings.emailSubject,
|
||||||
html: htmlStart + htmlMiddle + htmlEnd,
|
html: htmlStart + htmlMiddle + htmlEnd,
|
||||||
attachments: attachments
|
attachments: attachments
|
||||||
});
|
});
|
15
src/backend/model/messenger/ExtensionMessenger.ts
Normal file
15
src/backend/model/messenger/ExtensionMessenger.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import {MediaDTOWithThPath, Messenger} from './Messenger';
|
||||||
|
import {DynamicConfig} from '../../../common/entities/DynamicConfig';
|
||||||
|
|
||||||
|
export class ExtensionMessenger<C extends Record<string, unknown> = Record<string, unknown>> extends Messenger<C> {
|
||||||
|
|
||||||
|
constructor(public readonly Name: string,
|
||||||
|
public readonly ConfigTemplate: DynamicConfig[],
|
||||||
|
private readonly callbacks: { sendMedia: (config: C, media: MediaDTOWithThPath[]) => Promise<void> }) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sendMedia(config: C, media: MediaDTOWithThPath[]): Promise<void> {
|
||||||
|
return this.callbacks.sendMedia(config, media);
|
||||||
|
}
|
||||||
|
}
|
50
src/backend/model/messenger/Messenger.ts
Normal file
50
src/backend/model/messenger/Messenger.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import {MediaDTO, MediaDTOUtils} from '../../../common/entities/MediaDTO';
|
||||||
|
import {PhotoProcessing} from '../fileaccess/fileprocessing/PhotoProcessing';
|
||||||
|
import {ProjectPath} from '../../ProjectPath';
|
||||||
|
import {Config} from '../../../common/config/private/Config';
|
||||||
|
import {ThumbnailSourceType} from '../fileaccess/PhotoWorker';
|
||||||
|
import * as path from 'path';
|
||||||
|
import {Utils} from '../../../common/Utils';
|
||||||
|
import {QueryParams} from '../../../common/QueryParams';
|
||||||
|
import {DynamicConfig} from '../../../common/entities/DynamicConfig';
|
||||||
|
|
||||||
|
export interface MediaDTOWithThPath extends MediaDTO {
|
||||||
|
thumbnailPath: string;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class Messenger<C extends Record<string, unknown> = Record<string, unknown>> {
|
||||||
|
|
||||||
|
public abstract get Name(): string;
|
||||||
|
protected config: C;
|
||||||
|
public readonly ConfigTemplate: DynamicConfig[] = [];
|
||||||
|
|
||||||
|
private async getThumbnail(m: MediaDTO) {
|
||||||
|
return await PhotoProcessing.generateThumbnail(
|
||||||
|
path.join(ProjectPath.ImageFolder, m.directory.path, m.directory.name, m.name),
|
||||||
|
Config.Media.Thumbnail.thumbnailSizes[0],
|
||||||
|
MediaDTOUtils.isPhoto(m) ? ThumbnailSourceType.Photo : ThumbnailSourceType.Video,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async send(config: C, input: string | MediaDTO[] | unknown) {
|
||||||
|
if (Array.isArray(input) && input.length > 0
|
||||||
|
&& (input as MediaDTO[])[0]?.name
|
||||||
|
&& (input as MediaDTO[])[0]?.directory
|
||||||
|
&& (input as MediaDTO[])[0]?.metadata?.creationDate) {
|
||||||
|
const media = input as MediaDTOWithThPath[];
|
||||||
|
for (let i = 0; i < media.length; ++i) {
|
||||||
|
media[i].thumbnailPath = await this.getThumbnail(media[i]);
|
||||||
|
media[i].thumbnailUrl = Utils.concatUrls(Config.Server.publicUrl, '/gallery/', encodeURIComponent(path.join(media[i].directory.path, media[i].directory.name))) +
|
||||||
|
'?' + QueryParams.gallery.photo + '=' + encodeURIComponent(media[i].name);
|
||||||
|
}
|
||||||
|
return await this.sendMedia(config, media);
|
||||||
|
}
|
||||||
|
// TODO: implement other branches
|
||||||
|
throw new Error('Not yet implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract sendMedia(config: C, media: MediaDTOWithThPath[]): Promise<void> ;
|
||||||
|
}
|
41
src/backend/model/messenger/MessengerRepository.ts
Normal file
41
src/backend/model/messenger/MessengerRepository.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import {Messenger} from './Messenger';
|
||||||
|
import {EmailMessenger} from './EmailMessenger';
|
||||||
|
import {StdoutMessenger} from './StdoutMessenger';
|
||||||
|
|
||||||
|
export class MessengerRepository {
|
||||||
|
|
||||||
|
private static instance: MessengerRepository = null;
|
||||||
|
messengers: { [key: string]: Messenger } = {};
|
||||||
|
|
||||||
|
public static get Instance(): MessengerRepository {
|
||||||
|
if (MessengerRepository.instance == null) {
|
||||||
|
MessengerRepository.instance = new MessengerRepository();
|
||||||
|
}
|
||||||
|
return MessengerRepository.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(): Messenger[] {
|
||||||
|
return Object.values(this.messengers);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(m: Messenger<Record<string, unknown>>): void {
|
||||||
|
if (!this.messengers[m.Name]) {
|
||||||
|
throw new Error('Messenger does not exist:' + m.Name);
|
||||||
|
}
|
||||||
|
delete this.messengers[m.Name];
|
||||||
|
}
|
||||||
|
|
||||||
|
register(msgr: Messenger): void {
|
||||||
|
if (typeof this.messengers[msgr.Name] !== 'undefined') {
|
||||||
|
throw new Error('Messenger already exist:' + msgr.Name);
|
||||||
|
}
|
||||||
|
this.messengers[msgr.Name] = msgr;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(name: string): Messenger {
|
||||||
|
return this.messengers[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessengerRepository.Instance.register(new EmailMessenger());
|
||||||
|
MessengerRepository.Instance.register(new StdoutMessenger());
|
17
src/backend/model/messenger/StdoutMessenger.ts
Normal file
17
src/backend/model/messenger/StdoutMessenger.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import {MediaDTOWithThPath, Messenger} from './Messenger';
|
||||||
|
import {DynamicConfig} from '../../../common/entities/DynamicConfig';
|
||||||
|
import {DefaultMessengers} from '../../../common/entities/job/JobDTO';
|
||||||
|
|
||||||
|
export class StdoutMessenger extends Messenger {
|
||||||
|
public readonly Name = DefaultMessengers[DefaultMessengers.Stdout];
|
||||||
|
public readonly ConfigTemplate: DynamicConfig[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected async sendMedia(config: never, media: MediaDTOWithThPath[]) {
|
||||||
|
console.log(media.map(m => m.thumbnailPath));
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ export class AdminRouter {
|
|||||||
this.addGetStatistic(app);
|
this.addGetStatistic(app);
|
||||||
this.addGetDuplicates(app);
|
this.addGetDuplicates(app);
|
||||||
this.addJobs(app);
|
this.addJobs(app);
|
||||||
|
this.addMessengers(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static addGetStatistic(app: Express): void {
|
private static addGetStatistic(app: Express): void {
|
||||||
@ -32,6 +33,15 @@ export class AdminRouter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static addMessengers(app: Express): void {
|
||||||
|
app.get(
|
||||||
|
Config.Server.apiPath + '/admin/messengers/available',
|
||||||
|
AuthenticationMWs.authenticate,
|
||||||
|
AuthenticationMWs.authorise(UserRoles.Admin),
|
||||||
|
AdminMWs.getAvailableMessengers,
|
||||||
|
RenderingMWs.renderResult
|
||||||
|
);
|
||||||
|
}
|
||||||
private static addJobs(app: Express): void {
|
private static addJobs(app: Express): void {
|
||||||
app.get(
|
app.get(
|
||||||
Config.Server.apiPath + '/admin/jobs/available',
|
Config.Server.apiPath + '/admin/jobs/available',
|
||||||
|
@ -32,9 +32,18 @@ const LOG_TAG = '[server]';
|
|||||||
|
|
||||||
export class Server {
|
export class Server {
|
||||||
public onStarted = new Event<void>();
|
public onStarted = new Event<void>();
|
||||||
private app: express.Express;
|
public app: express.Express;
|
||||||
private server: HttpServer;
|
private server: HttpServer;
|
||||||
|
|
||||||
|
static instance: Server = null;
|
||||||
|
|
||||||
|
public static getInstance(): Server {
|
||||||
|
if (!this.instance) {
|
||||||
|
this.instance = new Server();
|
||||||
|
}
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!(process.env.NODE_ENV === 'production')) {
|
if (!(process.env.NODE_ENV === 'production')) {
|
||||||
Logger.info(
|
Logger.info(
|
||||||
@ -45,11 +54,16 @@ export class Server {
|
|||||||
this.init().catch(console.error);
|
this.init().catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
get App(): any {
|
get Server(): HttpServer {
|
||||||
return this.server;
|
return this.server;
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
|
|
||||||
|
this.app = express();
|
||||||
|
LoggerRouter.route(this.app);
|
||||||
|
this.app.set('view engine', 'ejs');
|
||||||
|
|
||||||
Logger.info(LOG_TAG, 'running diagnostics...');
|
Logger.info(LOG_TAG, 'running diagnostics...');
|
||||||
await ConfigDiagnostics.runDiagnostics();
|
await ConfigDiagnostics.runDiagnostics();
|
||||||
Logger.verbose(
|
Logger.verbose(
|
||||||
@ -61,13 +75,14 @@ export class Server {
|
|||||||
).configPath +
|
).configPath +
|
||||||
':'
|
':'
|
||||||
);
|
);
|
||||||
Logger.verbose(LOG_TAG, JSON.stringify(Config.toJSON({attachDescription: false}), null, '\t'));
|
Logger.verbose(LOG_TAG, JSON.stringify(Config.toJSON({attachDescription: false}), (k, v) => {
|
||||||
|
const MAX_LENGTH = 80;
|
||||||
|
if (typeof v === 'string' && v.length > MAX_LENGTH) {
|
||||||
|
v = v.slice(0, MAX_LENGTH - 3) + '...';
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}, 2));
|
||||||
|
|
||||||
this.app = express();
|
|
||||||
|
|
||||||
LoggerRouter.route(this.app);
|
|
||||||
|
|
||||||
this.app.set('view engine', 'ejs');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Session above all
|
* Session above all
|
||||||
@ -115,7 +130,7 @@ export class Server {
|
|||||||
Localizations.init();
|
Localizations.init();
|
||||||
|
|
||||||
this.app.use(locale(Config.Server.languages, 'en'));
|
this.app.use(locale(Config.Server.languages, 'en'));
|
||||||
await ObjectManagers.InitSQLManagers();
|
await ObjectManagers.getInstance().init();
|
||||||
|
|
||||||
Router.route(this.app);
|
Router.route(this.app);
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export type backendText = number;
|
export type backendText = number;
|
||||||
|
// keep the numbering sparse to support later addition
|
||||||
export const backendTexts = {
|
export const backendTexts = {
|
||||||
indexedFilesOnly: {name: 10, description: 12},
|
indexedFilesOnly: {name: 10, description: 12},
|
||||||
sizeToGenerate: {name: 20, description: 22},
|
sizeToGenerate: {name: 20, description: 22},
|
||||||
@ -6,6 +7,7 @@ export const backendTexts = {
|
|||||||
mediaPick: {name: 40, description: 42},
|
mediaPick: {name: 40, description: 42},
|
||||||
emailTo: {name: 70, description: 72},
|
emailTo: {name: 70, description: 72},
|
||||||
emailSubject: {name: 90, description: 92},
|
emailSubject: {name: 90, description: 92},
|
||||||
emailText: {name: 100, description: 102}
|
emailText: {name: 100, description: 102},
|
||||||
|
messenger: {name: 110,description: 112}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -85,16 +85,6 @@ export class PrivateConfigClass extends ServerConfig {
|
|||||||
this.Environment.isDocker = !!process.env.PI_DOCKER;
|
this.Environment.isDocker = !!process.env.PI_DOCKER;
|
||||||
}
|
}
|
||||||
|
|
||||||
async original(): Promise<PrivateConfigClass & IConfigClass> {
|
|
||||||
const pc = ConfigClassBuilder.attachPrivateInterface(new PrivateConfigClass());
|
|
||||||
try {
|
|
||||||
await pc.load();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error during loading original config. Reverting to defaults.');
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
return pc;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-inferrable-types */
|
/* eslint-disable @typescript-eslint/no-inferrable-types */
|
||||||
import {SubConfigClass} from '../../../../node_modules/typeconfig/src/decorators/class/SubConfigClass';
|
import {ConfigProperty, SubConfigClass} from 'typeconfig/common';
|
||||||
import {ConfigPriority, TAGS} from '../public/ClientConfig';
|
import {ConfigPriority, TAGS} from '../public/ClientConfig';
|
||||||
import {ConfigProperty} from '../../../../node_modules/typeconfig/src/decorators/property/ConfigPropoerty';
|
|
||||||
|
|
||||||
declare let $localize: (s: TemplateStringsArray) => string;
|
declare let $localize: (s: TemplateStringsArray) => string;
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
} from '../../entities/job/JobScheduleDTO';
|
} from '../../entities/job/JobScheduleDTO';
|
||||||
import {
|
import {
|
||||||
ClientConfig,
|
ClientConfig,
|
||||||
|
ClientExtensionsConfig,
|
||||||
ClientGPXCompressingConfig,
|
ClientGPXCompressingConfig,
|
||||||
ClientMediaConfig,
|
ClientMediaConfig,
|
||||||
ClientMetaFileConfig,
|
ClientMetaFileConfig,
|
||||||
@ -25,8 +26,7 @@ import {
|
|||||||
ConfigPriority,
|
ConfigPriority,
|
||||||
TAGS
|
TAGS
|
||||||
} from '../public/ClientConfig';
|
} from '../public/ClientConfig';
|
||||||
import {SubConfigClass} from 'typeconfig/src/decorators/class/SubConfigClass';
|
import {ConfigProperty, SubConfigClass} from 'typeconfig/common';
|
||||||
import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty';
|
|
||||||
import {DefaultsJobs} from '../../entities/job/JobDTO';
|
import {DefaultsJobs} from '../../entities/job/JobDTO';
|
||||||
import {SearchQueryDTO, SearchQueryTypes, TextSearch,} from '../../entities/SearchQueryDTO';
|
import {SearchQueryDTO, SearchQueryTypes, TextSearch,} from '../../entities/SearchQueryDTO';
|
||||||
import {SortByTypes} from '../../entities/SortingMethods';
|
import {SortByTypes} from '../../entities/SortingMethods';
|
||||||
@ -1013,6 +1013,25 @@ export class ServerServiceConfig extends ClientServiceConfig {
|
|||||||
Log: ServerLogConfig = new ServerLogConfig();
|
Log: ServerLogConfig = new ServerLogConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SubConfigClass<TAGS>({softReadonly: true})
|
||||||
|
export class ServerExtensionsConfig extends ClientExtensionsConfig {
|
||||||
|
@ConfigProperty({volatile: true})
|
||||||
|
list: string[] = [];
|
||||||
|
|
||||||
|
@ConfigProperty({type: 'object'})
|
||||||
|
configs: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
@ConfigProperty({
|
||||||
|
tags: {
|
||||||
|
name: $localize`Clean up unused tables`,
|
||||||
|
priority: ConfigPriority.underTheHood,
|
||||||
|
},
|
||||||
|
description: $localize`Automatically removes all tables from the DB that are not used anymore.`,
|
||||||
|
})
|
||||||
|
cleanUpUnusedTables: boolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
@SubConfigClass({softReadonly: true})
|
@SubConfigClass({softReadonly: true})
|
||||||
export class ServerEnvironmentConfig {
|
export class ServerEnvironmentConfig {
|
||||||
@ConfigProperty({volatile: true})
|
@ConfigProperty({volatile: true})
|
||||||
@ -1133,6 +1152,15 @@ export class ServerConfig extends ClientConfig {
|
|||||||
})
|
})
|
||||||
Messaging: MessagingConfig = new MessagingConfig();
|
Messaging: MessagingConfig = new MessagingConfig();
|
||||||
|
|
||||||
|
|
||||||
|
@ConfigProperty({
|
||||||
|
tags: {
|
||||||
|
name: $localize`Extensions`,
|
||||||
|
uiIcon: 'ionCloudOutline'
|
||||||
|
} as TAGS,
|
||||||
|
})
|
||||||
|
Extensions: ServerExtensionsConfig = new ServerExtensionsConfig();
|
||||||
|
|
||||||
@ConfigProperty({
|
@ConfigProperty({
|
||||||
tags: {
|
tags: {
|
||||||
name: $localize`Jobs`,
|
name: $localize`Jobs`,
|
||||||
|
@ -1424,6 +1424,16 @@ export class ClientUserConfig {
|
|||||||
unAuthenticatedUserRole: UserRoles = UserRoles.Admin;
|
unAuthenticatedUserRole: UserRoles = UserRoles.Admin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SubConfigClass({tags: {client: true}, softReadonly: true})
|
||||||
|
export class ClientExtensionsConfig {
|
||||||
|
@ConfigProperty({
|
||||||
|
tags: {
|
||||||
|
name: $localize`Enabled`,
|
||||||
|
priority: ConfigPriority.advanced,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
enabled: boolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
@SubConfigClass<TAGS>({tags: {client: true}, softReadonly: true})
|
@SubConfigClass<TAGS>({tags: {client: true}, softReadonly: true})
|
||||||
export class ClientConfig {
|
export class ClientConfig {
|
||||||
@ -1496,4 +1506,13 @@ export class ClientConfig {
|
|||||||
description: $localize`This feature enables you to generate 'random photo' urls. That URL returns a photo random selected from your gallery. You can use the url with 3rd party application like random changing desktop background. Note: With the current implementation, random link also requires login.`
|
description: $localize`This feature enables you to generate 'random photo' urls. That URL returns a photo random selected from your gallery. You can use the url with 3rd party application like random changing desktop background. Note: With the current implementation, random link also requires login.`
|
||||||
})
|
})
|
||||||
RandomPhoto: ClientRandomPhotoConfig = new ClientRandomPhotoConfig();
|
RandomPhoto: ClientRandomPhotoConfig = new ClientRandomPhotoConfig();
|
||||||
|
|
||||||
|
|
||||||
|
@ConfigProperty({
|
||||||
|
tags: {
|
||||||
|
name: $localize`Extensions`,
|
||||||
|
uiIcon: 'ionCloudOutline'
|
||||||
|
} as TAGS,
|
||||||
|
})
|
||||||
|
Extensions: ClientExtensionsConfig = new ClientExtensionsConfig();
|
||||||
}
|
}
|
||||||
|
21
src/common/entities/DynamicConfig.ts
Normal file
21
src/common/entities/DynamicConfig.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {backendText} from '../BackendTexts';
|
||||||
|
|
||||||
|
|
||||||
|
export type fieldType = 'string' | 'string-array' | 'number' | 'boolean' | 'number-array' | 'MediaPickDTO-array' | 'messenger';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic configs are not part of the typeconfig maintained config.
|
||||||
|
* Pigallery uses them to dynamically define configuration
|
||||||
|
* on the serverside so the client can parse and render it.
|
||||||
|
* It is mostly used for configuring jobs
|
||||||
|
*/
|
||||||
|
export interface DynamicConfig {
|
||||||
|
id: string;
|
||||||
|
// Use a predefined and localized backend text id or explicitly define the text
|
||||||
|
name: backendText | string;
|
||||||
|
description: backendText | string;
|
||||||
|
type: fieldType;
|
||||||
|
defaultValue: unknown;
|
||||||
|
validIf?: { configFiled: string, equalsValue: string }; // only shows this config if this predicate is true
|
||||||
|
}
|
@ -1,6 +1,4 @@
|
|||||||
import {backendText} from '../../BackendTexts';
|
import {DynamicConfig} from '../DynamicConfig';
|
||||||
|
|
||||||
export type fieldType = 'string' | 'string-array' | 'number' | 'boolean' | 'number-array' | 'MediaPickDTO-array';
|
|
||||||
|
|
||||||
export enum DefaultsJobs {
|
export enum DefaultsJobs {
|
||||||
Indexing = 1,
|
Indexing = 1,
|
||||||
@ -17,17 +15,16 @@ export enum DefaultsJobs {
|
|||||||
'Top Pick Sending' = 12
|
'Top Pick Sending' = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigTemplateEntry {
|
|
||||||
id: string;
|
export enum DefaultMessengers {
|
||||||
name: backendText;
|
Email = 1,
|
||||||
description: backendText;
|
Stdout = 2
|
||||||
type: fieldType;
|
|
||||||
defaultValue: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface JobDTO {
|
export interface JobDTO {
|
||||||
Name: string;
|
Name: string;
|
||||||
ConfigTemplate: ConfigTemplateEntry[];
|
ConfigTemplate: DynamicConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JobDTOUtils = {
|
export const JobDTOUtils = {
|
||||||
|
@ -5,7 +5,10 @@ import {DefaultsJobs} from '../../../common/entities/job/JobDTO';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class BackendtextService {
|
export class BackendtextService {
|
||||||
|
|
||||||
public get(id: backendText): string {
|
public get(id: backendText | string): string {
|
||||||
|
if (typeof id === 'string') {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case backendTexts.sizeToGenerate.name:
|
case backendTexts.sizeToGenerate.name:
|
||||||
return $localize`Size to generate`;
|
return $localize`Size to generate`;
|
||||||
@ -35,6 +38,10 @@ export class BackendtextService {
|
|||||||
return $localize`Message`;
|
return $localize`Message`;
|
||||||
case backendTexts.emailText.description:
|
case backendTexts.emailText.description:
|
||||||
return $localize`E-mail text.`;
|
return $localize`E-mail text.`;
|
||||||
|
case backendTexts.messenger.name:
|
||||||
|
return $localize`Messenger`;
|
||||||
|
case backendTexts.messenger.description:
|
||||||
|
return $localize`Messenger to send this message with.`;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import {ConfigStyle, SettingsService} from '../settings/settings.service';
|
|||||||
import {ConfigPriority} from '../../../../common/config/public/ClientConfig';
|
import {ConfigPriority} from '../../../../common/config/public/ClientConfig';
|
||||||
import {WebConfig} from '../../../../common/config/private/WebConfig';
|
import {WebConfig} from '../../../../common/config/private/WebConfig';
|
||||||
import {ISettingsComponent} from '../settings/template/ISettingsComponent';
|
import {ISettingsComponent} from '../settings/template/ISettingsComponent';
|
||||||
import {WebConfigClassBuilder} from '../../../../../node_modules/typeconfig/src/decorators/builders/WebConfigClassBuilder';
|
import {WebConfigClassBuilder} from 'typeconfig/src/decorators/builders/WebConfigClassBuilder';
|
||||||
import {enumToTranslatedArray} from '../EnumTranslations';
|
import {enumToTranslatedArray} from '../EnumTranslations';
|
||||||
import {PiTitleService} from '../../model/pi-title.service';
|
import {PiTitleService} from '../../model/pi-title.service';
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
import {Component, EventEmitter, Input, Output,OnChanges} from '@angular/core';
|
||||||
import {BlogService, GroupedMarkdown} from './blog.service';
|
import {BlogService, GroupedMarkdown} from './blog.service';
|
||||||
import {OnChanges} from '../../../../../../node_modules/@angular/core';
|
|
||||||
import {map, Observable} from 'rxjs';
|
import {map, Observable} from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -4,7 +4,7 @@ import {AutoCompleteService} from '../autocomplete.service';
|
|||||||
import {SearchQueryDTO} from '../../../../../../common/entities/SearchQueryDTO';
|
import {SearchQueryDTO} from '../../../../../../common/entities/SearchQueryDTO';
|
||||||
import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator,} from '@angular/forms';
|
import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator,} from '@angular/forms';
|
||||||
import {SearchQueryParserService} from '../search-query-parser.service';
|
import {SearchQueryParserService} from '../search-query-parser.service';
|
||||||
import {BsModalRef, BsModalService,} from '../../../../../../../node_modules/ngx-bootstrap/modal';
|
import {BsModalRef, BsModalService,} from 'ngx-bootstrap/modal';
|
||||||
import {Utils} from '../../../../../../common/Utils';
|
import {Utils} from '../../../../../../common/Utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -3,9 +3,10 @@ import {BehaviorSubject} from 'rxjs';
|
|||||||
import {JobProgressDTO, JobProgressStates, OnTimerJobProgressDTO,} from '../../../../common/entities/job/JobProgressDTO';
|
import {JobProgressDTO, JobProgressStates, OnTimerJobProgressDTO,} from '../../../../common/entities/job/JobProgressDTO';
|
||||||
import {NetworkService} from '../../model/network/network.service';
|
import {NetworkService} from '../../model/network/network.service';
|
||||||
import {JobScheduleDTO} from '../../../../common/entities/job/JobScheduleDTO';
|
import {JobScheduleDTO} from '../../../../common/entities/job/JobScheduleDTO';
|
||||||
import {ConfigTemplateEntry, JobDTO, JobDTOUtils} from '../../../../common/entities/job/JobDTO';
|
import {JobDTO, JobDTOUtils} from '../../../../common/entities/job/JobDTO';
|
||||||
import {BackendtextService} from '../../model/backendtext.service';
|
import {BackendtextService} from '../../model/backendtext.service';
|
||||||
import {NotificationService} from '../../model/notification.service';
|
import {NotificationService} from '../../model/notification.service';
|
||||||
|
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ScheduledJobsService {
|
export class ScheduledJobsService {
|
||||||
@ -13,6 +14,7 @@ export class ScheduledJobsService {
|
|||||||
public onJobFinish: EventEmitter<string> = new EventEmitter<string>();
|
public onJobFinish: EventEmitter<string> = new EventEmitter<string>();
|
||||||
timer: number = null;
|
timer: number = null;
|
||||||
public availableJobs: BehaviorSubject<JobDTO[]>;
|
public availableJobs: BehaviorSubject<JobDTO[]>;
|
||||||
|
public availableMessengers: BehaviorSubject<string[]>;
|
||||||
public jobStartingStopping: { [key: string]: boolean } = {};
|
public jobStartingStopping: { [key: string]: boolean } = {};
|
||||||
private subscribers = 0;
|
private subscribers = 0;
|
||||||
|
|
||||||
@ -23,6 +25,7 @@ export class ScheduledJobsService {
|
|||||||
) {
|
) {
|
||||||
this.progress = new BehaviorSubject({});
|
this.progress = new BehaviorSubject({});
|
||||||
this.availableJobs = new BehaviorSubject([]);
|
this.availableJobs = new BehaviorSubject([]);
|
||||||
|
this.availableMessengers = new BehaviorSubject([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -32,7 +35,13 @@ export class ScheduledJobsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getConfigTemplate(JobName: string): ConfigTemplateEntry[] {
|
public async getAvailableMessengers(): Promise<void> {
|
||||||
|
this.availableMessengers.next(
|
||||||
|
await this.networkService.getJson<string[]>('/admin/messengers/available')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getConfigTemplate(JobName: string): DynamicConfig[] {
|
||||||
const job = this.availableJobs.value.find(
|
const job = this.availableJobs.value.find(
|
||||||
(t) => t.Name === JobName
|
(t) => t.Name === JobName
|
||||||
);
|
);
|
||||||
|
@ -10,7 +10,7 @@ import {CookieService} from 'ngx-cookie-service';
|
|||||||
import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
|
import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
|
||||||
import {StatisticDTO} from '../../../../common/entities/settings/StatisticDTO';
|
import {StatisticDTO} from '../../../../common/entities/settings/StatisticDTO';
|
||||||
import {ScheduledJobsService} from './scheduled-jobs.service';
|
import {ScheduledJobsService} from './scheduled-jobs.service';
|
||||||
import {IWebConfigClassPrivate} from '../../../../../node_modules/typeconfig/src/decorators/class/IWebConfigClass';
|
import {IWebConfigClassPrivate} from 'typeconfig/src/decorators/class/IWebConfigClass';
|
||||||
|
|
||||||
|
|
||||||
export enum ConfigStyle {
|
export enum ConfigStyle {
|
||||||
|
@ -16,7 +16,7 @@ import {ConfigStyle, SettingsService} from '../../settings.service';
|
|||||||
import {WebConfig} from '../../../../../../common/config/private/WebConfig';
|
import {WebConfig} from '../../../../../../common/config/private/WebConfig';
|
||||||
import {JobScheduleConfig, UserConfig} from '../../../../../../common/config/private/PrivateConfig';
|
import {JobScheduleConfig, UserConfig} from '../../../../../../common/config/private/PrivateConfig';
|
||||||
import {enumToTranslatedArray} from '../../../EnumTranslations';
|
import {enumToTranslatedArray} from '../../../EnumTranslations';
|
||||||
import {BsModalService} from '../../../../../../../node_modules/ngx-bootstrap/modal';
|
import {BsModalService} from 'ngx-bootstrap/modal';
|
||||||
import {CustomSettingsEntries} from '../CustomSettingsEntries';
|
import {CustomSettingsEntries} from '../CustomSettingsEntries';
|
||||||
import {GroupByTypes, SortByTypes} from '../../../../../../common/entities/SortingMethods';
|
import {GroupByTypes, SortByTypes} from '../../../../../../common/entities/SortingMethods';
|
||||||
|
|
||||||
@ -79,7 +79,10 @@ export class SettingsEntryComponent
|
|||||||
public arrayType: string;
|
public arrayType: string;
|
||||||
public uiType: string;
|
public uiType: string;
|
||||||
newThemeModalRef: any;
|
newThemeModalRef: any;
|
||||||
iconModal: { ref?: any, error?: string };
|
iconModal: {
|
||||||
|
ref?: any,
|
||||||
|
error?: string
|
||||||
|
};
|
||||||
@Input() noChangeDetection = false;
|
@Input() noChangeDetection = false;
|
||||||
public readonly ConfigStyle = ConfigStyle;
|
public readonly ConfigStyle = ConfigStyle;
|
||||||
protected readonly SortByTypes = SortByTypes;
|
protected readonly SortByTypes = SortByTypes;
|
||||||
@ -177,9 +180,9 @@ export class SettingsEntryComponent
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof this.state.value === 'object') {
|
if (typeof this.state.value === 'object') {
|
||||||
this.state.value = JSON.parse(value);
|
this.state.value = JSON.parse(value);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.value = value;
|
this.state.value = value;
|
||||||
@ -198,7 +201,9 @@ export class SettingsEntryComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
get SelectedThemeSettings(): { theme: string } {
|
get SelectedThemeSettings(): {
|
||||||
|
theme: string
|
||||||
|
} {
|
||||||
return (this.state.value as ThemeConfig[]).find(th => th.name === (this.state.rootConfig as any).__state.selectedTheme.value) || {theme: 'N/A'};
|
return (this.state.value as ThemeConfig[]).find(th => th.name === (this.state.rootConfig as any).__state.selectedTheme.value) || {theme: 'N/A'};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,7 +311,12 @@ export class SettingsEntryComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getOptionsView(state: IState & { optionsView?: { key: number | string; value: string | number }[] }) {
|
getOptionsView(state: IState & {
|
||||||
|
optionsView?: {
|
||||||
|
key: number | string;
|
||||||
|
value: string | number
|
||||||
|
}[]
|
||||||
|
}) {
|
||||||
if (!state.optionsView) {
|
if (!state.optionsView) {
|
||||||
const eClass = state.isEnumType
|
const eClass = state.isEnumType
|
||||||
? state.type
|
? state.type
|
||||||
|
@ -5,12 +5,12 @@ import {
|
|||||||
NG_VALUE_ACCESSOR,
|
NG_VALUE_ACCESSOR,
|
||||||
ValidationErrors,
|
ValidationErrors,
|
||||||
Validator
|
Validator
|
||||||
} from '../../../../../../../../node_modules/@angular/forms';
|
} from '@angular/forms';
|
||||||
import {SortByDirectionalTypes, SortingMethod} from '../../../../../../../common/entities/SortingMethods';
|
import {SortByDirectionalTypes, SortingMethod} from '../../../../../../../common/entities/SortingMethods';
|
||||||
import {enumToTranslatedArray} from '../../../../EnumTranslations';
|
import {enumToTranslatedArray} from '../../../../EnumTranslations';
|
||||||
import {AutoCompleteService} from '../../../../gallery/search/autocomplete.service';
|
import {AutoCompleteService} from '../../../../gallery/search/autocomplete.service';
|
||||||
import {RouterLink} from '../../../../../../../../node_modules/@angular/router';
|
import {RouterLink} from '@angular/router';
|
||||||
import {forwardRef} from '../../../../../../../../node_modules/@angular/core';
|
import {forwardRef} from '@angular/core';
|
||||||
import {Utils} from '../../../../../../../common/Utils';
|
import {Utils} from '../../../../../../../common/Utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -9,11 +9,11 @@ import {JobDTOUtils} from '../../../../../common/entities/job/JobDTO';
|
|||||||
import {ScheduledJobsService} from '../scheduled-jobs.service';
|
import {ScheduledJobsService} from '../scheduled-jobs.service';
|
||||||
import {UntypedFormControl} from '@angular/forms';
|
import {UntypedFormControl} from '@angular/forms';
|
||||||
import {Subscription} from 'rxjs';
|
import {Subscription} from 'rxjs';
|
||||||
import {IWebConfigClassPrivate} from '../../../../../../node_modules/typeconfig/src/decorators/class/IWebConfigClass';
|
import {IWebConfigClassPrivate} from 'typeconfig/src/decorators/class/IWebConfigClass';
|
||||||
import {ConfigPriority, TAGS} from '../../../../../common/config/public/ClientConfig';
|
import {ConfigPriority, TAGS} from '../../../../../common/config/public/ClientConfig';
|
||||||
import {Utils} from '../../../../../common/Utils';
|
import {Utils} from '../../../../../common/Utils';
|
||||||
import {UserRoles} from '../../../../../common/entities/UserDTO';
|
import {UserRoles} from '../../../../../common/entities/UserDTO';
|
||||||
import {WebConfigClassBuilder} from '../../../../../../node_modules/typeconfig/src/decorators/builders/WebConfigClassBuilder';
|
import {WebConfigClassBuilder} from 'typeconfig/web';
|
||||||
import {ErrorDTO} from '../../../../../common/entities/Error';
|
import {ErrorDTO} from '../../../../../common/entities/Error';
|
||||||
import {ISettingsComponent} from './ISettingsComponent';
|
import {ISettingsComponent} from './ISettingsComponent';
|
||||||
import {CustomSettingsEntries} from './CustomSettingsEntries';
|
import {CustomSettingsEntries} from './CustomSettingsEntries';
|
||||||
|
@ -170,6 +170,7 @@
|
|||||||
<div *ngFor="let configEntry of jobsService.getConfigTemplate(schedule.jobName)">
|
<div *ngFor="let configEntry of jobsService.getConfigTemplate(schedule.jobName)">
|
||||||
|
|
||||||
<div class="mb-1 row"
|
<div class="mb-1 row"
|
||||||
|
*ngIf="!configEntry.validIf || schedule.config[configEntry.validIf.configFiled] == configEntry.validIf.equalsValue"
|
||||||
[class.mb-3]="settingsService.configStyle == ConfigStyle.full">
|
[class.mb-3]="settingsService.configStyle == ConfigStyle.full">
|
||||||
<label class="col-md-2 control-label"
|
<label class="col-md-2 control-label"
|
||||||
[for]="configEntry.id+'_'+i">{{backendTextService.get(configEntry.name)}}</label>
|
[for]="configEntry.id+'_'+i">{{backendTextService.get(configEntry.name)}}</label>
|
||||||
@ -227,6 +228,18 @@
|
|||||||
placeholder="Search Query">
|
placeholder="Search Query">
|
||||||
</app-gallery-search-field>
|
</app-gallery-search-field>
|
||||||
|
|
||||||
|
<select
|
||||||
|
*ngSwitchCase="'messenger'"
|
||||||
|
[id]="configEntry.id+'_'+i"
|
||||||
|
[name]="configEntry.id+'_'+i"
|
||||||
|
(ngModelChange)="onChange($event)"
|
||||||
|
[(ngModel)]="schedule.config[configEntry.id]"
|
||||||
|
class="form-select">
|
||||||
|
<option *ngFor="let msg of jobsService.availableMessengers | async" [ngValue]="msg">{{msg}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
|
||||||
<ng-container *ngSwitchCase="'MediaPickDTO-array'">
|
<ng-container *ngSwitchCase="'MediaPickDTO-array'">
|
||||||
<ng-container *ngFor="let mp of AsMediaPickDTOArray(schedule.config[configEntry.id]); let j=index">
|
<ng-container *ngFor="let mp of AsMediaPickDTOArray(schedule.config[configEntry.id]); let j=index">
|
||||||
|
|
||||||
|
@ -108,6 +108,7 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.jobsService.subscribeToProgress();
|
this.jobsService.subscribeToProgress();
|
||||||
this.jobsService.getAvailableJobs().catch(console.error);
|
this.jobsService.getAvailableJobs().catch(console.error);
|
||||||
|
this.jobsService.getAvailableMessengers().catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@ -128,7 +129,7 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni
|
|||||||
schedule.config = schedule.config || {};
|
schedule.config = schedule.config || {};
|
||||||
if (job.ConfigTemplate) {
|
if (job.ConfigTemplate) {
|
||||||
job.ConfigTemplate.forEach(
|
job.ConfigTemplate.forEach(
|
||||||
(ct) => (schedule.config[ct.id] = ct.defaultValue)
|
(ct) => (schedule.config[ct.id] = ct.defaultValue as never)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -216,7 +217,7 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni
|
|||||||
this.newSchedule.config = this.newSchedule.config || {};
|
this.newSchedule.config = this.newSchedule.config || {};
|
||||||
if (job.ConfigTemplate) {
|
if (job.ConfigTemplate) {
|
||||||
job.ConfigTemplate.forEach(
|
job.ConfigTemplate.forEach(
|
||||||
(ct) => (this.newSchedule.config[ct.id] = ct.defaultValue)
|
(ct) => (this.newSchedule.config[ct.id] = ct.defaultValue as never)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.jobModalQL.first.show();
|
this.jobModalQL.first.show();
|
||||||
|
@ -2,7 +2,7 @@ import {Config} from '../../src/common/config/private/Config';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import {SQLConnection} from '../../src/backend/model/database/SQLConnection';
|
import {SQLConnection} from '../../src/backend/model/database/SQLConnection';
|
||||||
import {DatabaseType, LogLevel} from '../../src/common/config/private/PrivateConfig';
|
import {DatabaseType} from '../../src/common/config/private/PrivateConfig';
|
||||||
import {ProjectPath} from '../../src/backend/ProjectPath';
|
import {ProjectPath} from '../../src/backend/ProjectPath';
|
||||||
import {DirectoryBaseDTO, ParentDirectoryDTO, SubDirectoryDTO} from '../../src/common/entities/DirectoryDTO';
|
import {DirectoryBaseDTO, ParentDirectoryDTO, SubDirectoryDTO} from '../../src/common/entities/DirectoryDTO';
|
||||||
import {ObjectManagers} from '../../src/backend/model/ObjectManagers';
|
import {ObjectManagers} from '../../src/backend/model/ObjectManagers';
|
||||||
@ -111,7 +111,7 @@ export class DBTestHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async persistTestDir(directory: DirectoryBaseDTO): Promise<ParentDirectoryDTO> {
|
public static async persistTestDir(directory: DirectoryBaseDTO): Promise<ParentDirectoryDTO> {
|
||||||
await ObjectManagers.InitSQLManagers();
|
await ObjectManagers.getInstance().init();
|
||||||
const connection = await SQLConnection.getConnection();
|
const connection = await SQLConnection.getConnection();
|
||||||
ObjectManagers.getInstance().IndexingManager.indexDirectory = () => Promise.resolve(null);
|
ObjectManagers.getInstance().IndexingManager.indexDirectory = () => Promise.resolve(null);
|
||||||
|
|
||||||
@ -147,6 +147,7 @@ export class DBTestHelper {
|
|||||||
|
|
||||||
public async initDB(): Promise<void> {
|
public async initDB(): Promise<void> {
|
||||||
await Config.load();
|
await Config.load();
|
||||||
|
Config.Extensions.enabled = false; // make all tests clean
|
||||||
if (this.dbType === DatabaseType.sqlite) {
|
if (this.dbType === DatabaseType.sqlite) {
|
||||||
await this.initSQLite();
|
await this.initSQLite();
|
||||||
} else if (this.dbType === DatabaseType.mysql) {
|
} else if (this.dbType === DatabaseType.mysql) {
|
||||||
@ -197,7 +198,7 @@ export class DBTestHelper {
|
|||||||
const conn = await SQLConnection.getConnection();
|
const conn = await SQLConnection.getConnection();
|
||||||
await conn.query('CREATE DATABASE IF NOT EXISTS ' + conn.options.database);
|
await conn.query('CREATE DATABASE IF NOT EXISTS ' + conn.options.database);
|
||||||
await SQLConnection.close();
|
await SQLConnection.close();
|
||||||
await ObjectManagers.InitSQLManagers();
|
await ObjectManagers.getInstance().init();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async clearUpMysql(): Promise<void> {
|
private async clearUpMysql(): Promise<void> {
|
||||||
@ -218,7 +219,7 @@ export class DBTestHelper {
|
|||||||
private async resetSQLite(): Promise<void> {
|
private async resetSQLite(): Promise<void> {
|
||||||
Logger.debug(LOG_TAG, 'resetting sqlite');
|
Logger.debug(LOG_TAG, 'resetting sqlite');
|
||||||
await this.clearUpSQLite();
|
await this.clearUpSQLite();
|
||||||
await ObjectManagers.InitSQLManagers();
|
await ObjectManagers.getInstance().init();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async clearUpSQLite(): Promise<void> {
|
private async clearUpSQLite(): Promise<void> {
|
||||||
|
@ -48,7 +48,7 @@ describe('GalleryRouter', (sqlHelper: DBTestHelper) => {
|
|||||||
afterEach(tearDown);
|
afterEach(tearDown);
|
||||||
|
|
||||||
it('should load gallery', async () => {
|
it('should load gallery', async () => {
|
||||||
const result = await (chai.request(server.App) as SuperAgentStatic)
|
const result = await (chai.request(server.Server) as SuperAgentStatic)
|
||||||
.get(Config.Server.apiPath + '/gallery/content/');
|
.get(Config.Server.apiPath + '/gallery/content/');
|
||||||
|
|
||||||
(result.should as any).have.status(200);
|
(result.should as any).have.status(200);
|
||||||
@ -59,10 +59,10 @@ describe('GalleryRouter', (sqlHelper: DBTestHelper) => {
|
|||||||
|
|
||||||
it('should load gallery twice (to force loading form db)', async () => {
|
it('should load gallery twice (to force loading form db)', async () => {
|
||||||
Config.Indexing.reIndexingSensitivity = ReIndexingSensitivity.low;
|
Config.Indexing.reIndexingSensitivity = ReIndexingSensitivity.low;
|
||||||
const _ = await (chai.request(server.App) as SuperAgentStatic)
|
const _ = await (chai.request(server.Server) as SuperAgentStatic)
|
||||||
.get(Config.Server.apiPath + '/gallery/content/orientation');
|
.get(Config.Server.apiPath + '/gallery/content/orientation');
|
||||||
|
|
||||||
const result = await (chai.request(server.App) as SuperAgentStatic)
|
const result = await (chai.request(server.Server) as SuperAgentStatic)
|
||||||
.get(Config.Server.apiPath + '/gallery/content/orientation');
|
.get(Config.Server.apiPath + '/gallery/content/orientation');
|
||||||
|
|
||||||
(result.should as any).have.status(200);
|
(result.should as any).have.status(200);
|
||||||
@ -80,7 +80,7 @@ describe('GalleryRouter', (sqlHelper: DBTestHelper) => {
|
|||||||
afterEach(tearDown);
|
afterEach(tearDown);
|
||||||
|
|
||||||
it('should get video without transcoding', async () => {
|
it('should get video without transcoding', async () => {
|
||||||
const result = await (chai.request(server.App) as SuperAgentStatic)
|
const result = await (chai.request(server.Server) as SuperAgentStatic)
|
||||||
.get(Config.Server.apiPath + '/gallery/content/video.mp4/bestFit');
|
.get(Config.Server.apiPath + '/gallery/content/video.mp4/bestFit');
|
||||||
|
|
||||||
(result.should as any).have.status(200);
|
(result.should as any).have.status(200);
|
||||||
|
@ -41,7 +41,7 @@ describe('PublicRouter', () => {
|
|||||||
server = new Server();
|
server = new Server();
|
||||||
await server.onStarted.wait();
|
await server.onStarted.wait();
|
||||||
|
|
||||||
await ObjectManagers.InitSQLManagers();
|
await ObjectManagers.getInstance().init();
|
||||||
await ObjectManagers.getInstance().UserManager.createUser(Utils.clone(testUser));
|
await ObjectManagers.getInstance().UserManager.createUser(Utils.clone(testUser));
|
||||||
await SQLConnection.close();
|
await SQLConnection.close();
|
||||||
};
|
};
|
||||||
@ -71,7 +71,7 @@ describe('PublicRouter', () => {
|
|||||||
afterEach(tearDown);
|
afterEach(tearDown);
|
||||||
|
|
||||||
const fistLoad = async (srv: Server, sharingKey: string): Promise<any> => {
|
const fistLoad = async (srv: Server, sharingKey: string): Promise<any> => {
|
||||||
return (chai.request(srv.App) as SuperAgentStatic)
|
return (chai.request(srv.Server) as SuperAgentStatic)
|
||||||
.get('/share/' + sharingKey);
|
.get('/share/' + sharingKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ describe('SharingRouter', () => {
|
|||||||
server = new Server();
|
server = new Server();
|
||||||
await server.onStarted.wait();
|
await server.onStarted.wait();
|
||||||
|
|
||||||
await ObjectManagers.InitSQLManagers();
|
await ObjectManagers.getInstance().init();
|
||||||
await ObjectManagers.getInstance().UserManager.createUser(Utils.clone(testUser));
|
await ObjectManagers.getInstance().UserManager.createUser(Utils.clone(testUser));
|
||||||
await SQLConnection.close();
|
await SQLConnection.close();
|
||||||
};
|
};
|
||||||
@ -62,7 +62,7 @@ describe('SharingRouter', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const shareLogin = async (srv: Server, sharingKey: string, password?: string): Promise<any> => {
|
const shareLogin = async (srv: Server, sharingKey: string, password?: string): Promise<any> => {
|
||||||
return (chai.request(srv.App) as SuperAgentStatic)
|
return (chai.request(srv.Server) as SuperAgentStatic)
|
||||||
.post(Config.Server.apiPath + '/share/login?' + QueryParams.gallery.sharingKey_query + '=' + sharingKey)
|
.post(Config.Server.apiPath + '/share/login?' + QueryParams.gallery.sharingKey_query + '=' + sharingKey)
|
||||||
.send({password});
|
.send({password});
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ describe('UserRouter', () => {
|
|||||||
|
|
||||||
server = new Server();
|
server = new Server();
|
||||||
await server.onStarted.wait();
|
await server.onStarted.wait();
|
||||||
await ObjectManagers.InitSQLManagers();
|
await ObjectManagers.getInstance().init();
|
||||||
await ObjectManagers.getInstance().UserManager.createUser(Utils.clone(testUser));
|
await ObjectManagers.getInstance().UserManager.createUser(Utils.clone(testUser));
|
||||||
await SQLConnection.close();
|
await SQLConnection.close();
|
||||||
};
|
};
|
||||||
@ -62,7 +62,7 @@ describe('UserRouter', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const login = async (srv: Server): Promise<any> => {
|
const login = async (srv: Server): Promise<any> => {
|
||||||
const result = await (chai.request(srv.App) as SuperAgentStatic)
|
const result = await (chai.request(srv.Server) as SuperAgentStatic)
|
||||||
.post(Config.Server.apiPath + '/user/login')
|
.post(Config.Server.apiPath + '/user/login')
|
||||||
.send({
|
.send({
|
||||||
loginCredential: {
|
loginCredential: {
|
||||||
@ -87,7 +87,7 @@ describe('UserRouter', () => {
|
|||||||
});
|
});
|
||||||
it('it skip login', async () => {
|
it('it skip login', async () => {
|
||||||
Config.Users.authenticationRequired = false;
|
Config.Users.authenticationRequired = false;
|
||||||
const result = await chai.request(server.App)
|
const result = await chai.request(server.Server)
|
||||||
.post(Config.Server.apiPath + '/user/login');
|
.post(Config.Server.apiPath + '/user/login');
|
||||||
|
|
||||||
result.res.should.have.status(404);
|
result.res.should.have.status(404);
|
||||||
@ -105,7 +105,7 @@ describe('UserRouter', () => {
|
|||||||
|
|
||||||
const loginRes = await login(server);
|
const loginRes = await login(server);
|
||||||
|
|
||||||
const result = await chai.request(server.App)
|
const result = await chai.request(server.Server)
|
||||||
.get(Config.Server.apiPath + '/user/me')
|
.get(Config.Server.apiPath + '/user/me')
|
||||||
.set('Cookie', loginRes.res.headers['set-cookie'])
|
.set('Cookie', loginRes.res.headers['set-cookie'])
|
||||||
.set('CSRF-Token', loginRes.body.result.csrfToken);
|
.set('CSRF-Token', loginRes.body.result.csrfToken);
|
||||||
@ -116,7 +116,7 @@ describe('UserRouter', () => {
|
|||||||
it('it should not authenticate', async () => {
|
it('it should not authenticate', async () => {
|
||||||
Config.Users.authenticationRequired = true;
|
Config.Users.authenticationRequired = true;
|
||||||
|
|
||||||
const result = await chai.request(server.App)
|
const result = await chai.request(server.Server)
|
||||||
.get(Config.Server.apiPath + '/user/me');
|
.get(Config.Server.apiPath + '/user/me');
|
||||||
|
|
||||||
result.res.should.have.status(401);
|
result.res.should.have.status(401);
|
||||||
@ -133,7 +133,7 @@ describe('UserRouter', () => {
|
|||||||
const loginRes = await login(server);
|
const loginRes = await login(server);
|
||||||
const q: Record<string, string> = {};
|
const q: Record<string, string> = {};
|
||||||
q[QueryParams.gallery.sharingKey_query] = sharingKey;
|
q[QueryParams.gallery.sharingKey_query] = sharingKey;
|
||||||
const result = await chai.request(server.App)
|
const result = await chai.request(server.Server)
|
||||||
.get(Config.Server.apiPath + '/user/me?' + QueryParams.gallery.sharingKey_query + '=' + sharingKey)
|
.get(Config.Server.apiPath + '/user/me?' + QueryParams.gallery.sharingKey_query + '=' + sharingKey)
|
||||||
.set('Cookie', loginRes.res.headers['set-cookie'])
|
.set('Cookie', loginRes.res.headers['set-cookie'])
|
||||||
.set('CSRF-Token', loginRes.body.result.csrfToken);
|
.set('CSRF-Token', loginRes.body.result.csrfToken);
|
||||||
@ -152,7 +152,7 @@ describe('UserRouter', () => {
|
|||||||
|
|
||||||
const q: Record<string, string> = {};
|
const q: Record<string, string> = {};
|
||||||
q[QueryParams.gallery.sharingKey_query] = sharing.sharingKey;
|
q[QueryParams.gallery.sharingKey_query] = sharing.sharingKey;
|
||||||
const result = await chai.request(server.App)
|
const result = await chai.request(server.Server)
|
||||||
.get(Config.Server.apiPath + '/user/me?' + QueryParams.gallery.sharingKey_query + '=' + sharing.sharingKey);
|
.get(Config.Server.apiPath + '/user/me?' + QueryParams.gallery.sharingKey_query + '=' + sharing.sharingKey);
|
||||||
|
|
||||||
checkUserResult(result, RouteTestingHelper.getExpectedSharingUser(sharing));
|
checkUserResult(result, RouteTestingHelper.getExpectedSharingUser(sharing));
|
||||||
@ -167,7 +167,7 @@ describe('UserRouter', () => {
|
|||||||
|
|
||||||
const q: Record<string, string> = {};
|
const q: Record<string, string> = {};
|
||||||
q[QueryParams.gallery.sharingKey_query] = sharing.sharingKey;
|
q[QueryParams.gallery.sharingKey_query] = sharing.sharingKey;
|
||||||
const result = await chai.request(server.App)
|
const result = await chai.request(server.Server)
|
||||||
.get(Config.Server.apiPath + '/user/me?' + QueryParams.gallery.sharingKey_query + '=' + sharing.sharingKey);
|
.get(Config.Server.apiPath + '/user/me?' + QueryParams.gallery.sharingKey_query + '=' + sharing.sharingKey);
|
||||||
|
|
||||||
result.should.have.status(401);
|
result.should.have.status(401);
|
||||||
@ -179,7 +179,7 @@ describe('UserRouter', () => {
|
|||||||
it('it should authenticate as guest', async () => {
|
it('it should authenticate as guest', async () => {
|
||||||
Config.Users.authenticationRequired = false;
|
Config.Users.authenticationRequired = false;
|
||||||
|
|
||||||
const result = await chai.request(server.App)
|
const result = await chai.request(server.Server)
|
||||||
.get(Config.Server.apiPath + '/user/me');
|
.get(Config.Server.apiPath + '/user/me');
|
||||||
|
|
||||||
const expectedGuestUser = {
|
const expectedGuestUser = {
|
||||||
|
@ -7,6 +7,7 @@ import {ProjectPath} from '../../../../../src/backend/ProjectPath';
|
|||||||
import {TAGS} from '../../../../../src/common/config/public/ClientConfig';
|
import {TAGS} from '../../../../../src/common/config/public/ClientConfig';
|
||||||
import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers';
|
import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers';
|
||||||
import {UserRoles} from '../../../../../src/common/entities/UserDTO';
|
import {UserRoles} from '../../../../../src/common/entities/UserDTO';
|
||||||
|
import {ExtensionConfigWrapper} from '../../../../../src/backend/model/extension/ExtensionConfigWrapper';
|
||||||
|
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
const chai: any = require('chai');
|
const chai: any = require('chai');
|
||||||
@ -34,10 +35,10 @@ describe('SettingsRouter', () => {
|
|||||||
it('it should GET the settings', async () => {
|
it('it should GET the settings', async () => {
|
||||||
Config.Users.authenticationRequired = false;
|
Config.Users.authenticationRequired = false;
|
||||||
Config.Users.unAuthenticatedUserRole = UserRoles.Admin;
|
Config.Users.unAuthenticatedUserRole = UserRoles.Admin;
|
||||||
const originalSettings = await Config.original();
|
const originalSettings = await ExtensionConfigWrapper.original();
|
||||||
const srv = new Server();
|
const srv = new Server();
|
||||||
await srv.onStarted.wait();
|
await srv.onStarted.wait();
|
||||||
const result = await chai.request(srv.App)
|
const result = await chai.request(srv.Server)
|
||||||
.get(Config.Server.apiPath + '/settings');
|
.get(Config.Server.apiPath + '/settings');
|
||||||
|
|
||||||
result.res.should.have.status(200);
|
result.res.should.have.status(200);
|
||||||
|
@ -6,9 +6,10 @@ import {SettingsMWs} from '../../../../../src/backend/middlewares/admin/Settings
|
|||||||
import {ServerUserConfig} from '../../../../../src/common/config/private/PrivateConfig';
|
import {ServerUserConfig} from '../../../../../src/common/config/private/PrivateConfig';
|
||||||
import {Config} from '../../../../../src/common/config/private/Config';
|
import {Config} from '../../../../../src/common/config/private/Config';
|
||||||
import {UserRoles} from '../../../../../src/common/entities/UserDTO';
|
import {UserRoles} from '../../../../../src/common/entities/UserDTO';
|
||||||
import {ConfigClassBuilder} from '../../../../../node_modules/typeconfig/node';
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import {ExtensionConfigWrapper} from '../../../../../src/backend/model/extension/ExtensionConfigWrapper';
|
||||||
|
import {ConfigClassBuilder} from 'typeconfig/node';
|
||||||
|
|
||||||
|
|
||||||
declare const describe: any;
|
declare const describe: any;
|
||||||
@ -21,7 +22,7 @@ describe('Settings middleware', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await ObjectManagers.reset();
|
await ObjectManagers.reset();
|
||||||
await fs.promises.rm(tempDir, {recursive: true, force: true});
|
await fs.promises.rm(tempDir, {recursive: true, force: true});
|
||||||
await ObjectManagers.InitSQLManagers();
|
await ObjectManagers.getInstance().init();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should save empty enforced users settings', (done: (err?: any) => void) => {
|
it('should save empty enforced users settings', (done: (err?: any) => void) => {
|
||||||
@ -74,7 +75,7 @@ describe('Settings middleware', () => {
|
|||||||
expect(Config.Users.enforcedUsers.length).to.be.equal(1);
|
expect(Config.Users.enforcedUsers.length).to.be.equal(1);
|
||||||
expect(Config.Users.enforcedUsers[0].name).to.be.equal('Apple');
|
expect(Config.Users.enforcedUsers[0].name).to.be.equal('Apple');
|
||||||
expect(Config.Users.enforcedUsers.length).to.be.equal(1);
|
expect(Config.Users.enforcedUsers.length).to.be.equal(1);
|
||||||
Config.original().then((cfg) => {
|
ExtensionConfigWrapper.original().then((cfg) => {
|
||||||
try {
|
try {
|
||||||
expect(cfg.Users.enforcedUsers.length).to.be.equal(1);
|
expect(cfg.Users.enforcedUsers.length).to.be.equal(1);
|
||||||
expect(cfg.Users.enforcedUsers[0].name).to.be.equal('Apple');
|
expect(cfg.Users.enforcedUsers[0].name).to.be.equal('Apple');
|
||||||
|
@ -33,7 +33,7 @@ describe('AlbumManager', (sqlHelper: DBTestHelper) => {
|
|||||||
const setUpSqlDB = async () => {
|
const setUpSqlDB = async () => {
|
||||||
await sqlHelper.initDB();
|
await sqlHelper.initDB();
|
||||||
await sqlHelper.setUpTestGallery();
|
await sqlHelper.setUpTestGallery();
|
||||||
await ObjectManagers.InitSQLManagers();
|
await ObjectManagers.getInstance().init();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ describe('CoverManager', (sqlHelper: DBTestHelper) => {
|
|||||||
const setUpSqlDB = async () => {
|
const setUpSqlDB = async () => {
|
||||||
await sqlHelper.initDB();
|
await sqlHelper.initDB();
|
||||||
await setUpTestGallery();
|
await setUpTestGallery();
|
||||||
await ObjectManagers.InitSQLManagers();
|
await ObjectManagers.getInstance().init();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -212,21 +212,21 @@ describe('CoverManager', (sqlHelper: DBTestHelper) => {
|
|||||||
it('should get cover for saved search', async () => {
|
it('should get cover for saved search', async () => {
|
||||||
const pm = new CoverManager();
|
const pm = new CoverManager();
|
||||||
Config.AlbumCover.SearchQuery = null;
|
Config.AlbumCover.SearchQuery = null;
|
||||||
expect(Utils.clone(await pm.getAlbumCover({
|
expect(Utils.clone(await pm.getCoverForAlbum({
|
||||||
searchQuery: {
|
searchQuery: {
|
||||||
type: SearchQueryTypes.any_text,
|
type: SearchQueryTypes.any_text,
|
||||||
text: 'sw'
|
text: 'sw'
|
||||||
} as TextSearch
|
} as TextSearch
|
||||||
}))).to.deep.equalInAnyOrder(previewifyMedia(p4));
|
}))).to.deep.equalInAnyOrder(previewifyMedia(p4));
|
||||||
Config.AlbumCover.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Boba'} as TextSearch;
|
Config.AlbumCover.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Boba'} as TextSearch;
|
||||||
expect(Utils.clone(await pm.getAlbumCover({
|
expect(Utils.clone(await pm.getCoverForAlbum({
|
||||||
searchQuery: {
|
searchQuery: {
|
||||||
type: SearchQueryTypes.any_text,
|
type: SearchQueryTypes.any_text,
|
||||||
text: 'sw'
|
text: 'sw'
|
||||||
} as TextSearch
|
} as TextSearch
|
||||||
}))).to.deep.equalInAnyOrder(previewifyMedia(p));
|
}))).to.deep.equalInAnyOrder(previewifyMedia(p));
|
||||||
Config.AlbumCover.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Derem'} as TextSearch;
|
Config.AlbumCover.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Derem'} as TextSearch;
|
||||||
expect(Utils.clone(await pm.getAlbumCover({
|
expect(Utils.clone(await pm.getCoverForAlbum({
|
||||||
searchQuery: {
|
searchQuery: {
|
||||||
type: SearchQueryTypes.any_text,
|
type: SearchQueryTypes.any_text,
|
||||||
text: 'sw'
|
text: 'sw'
|
||||||
@ -234,7 +234,7 @@ describe('CoverManager', (sqlHelper: DBTestHelper) => {
|
|||||||
}))).to.deep.equalInAnyOrder(previewifyMedia(p2));
|
}))).to.deep.equalInAnyOrder(previewifyMedia(p2));
|
||||||
// Having a preview search query that does not return valid result
|
// Having a preview search query that does not return valid result
|
||||||
Config.AlbumCover.SearchQuery = {type: SearchQueryTypes.any_text, text: 'wont find it'} as TextSearch;
|
Config.AlbumCover.SearchQuery = {type: SearchQueryTypes.any_text, text: 'wont find it'} as TextSearch;
|
||||||
expect(Utils.clone(await pm.getAlbumCover({
|
expect(Utils.clone(await pm.getCoverForAlbum({
|
||||||
searchQuery: {
|
searchQuery: {
|
||||||
type: SearchQueryTypes.any_text,
|
type: SearchQueryTypes.any_text,
|
||||||
text: 'Derem'
|
text: 'Derem'
|
||||||
@ -242,7 +242,7 @@ describe('CoverManager', (sqlHelper: DBTestHelper) => {
|
|||||||
}))).to.deep.equalInAnyOrder(previewifyMedia(p2));
|
}))).to.deep.equalInAnyOrder(previewifyMedia(p2));
|
||||||
// having a saved search that does not have any image
|
// having a saved search that does not have any image
|
||||||
Config.AlbumCover.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Derem'} as TextSearch;
|
Config.AlbumCover.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Derem'} as TextSearch;
|
||||||
expect(Utils.clone(await pm.getAlbumCover({
|
expect(Utils.clone(await pm.getCoverForAlbum({
|
||||||
searchQuery: {
|
searchQuery: {
|
||||||
type: SearchQueryTypes.any_text,
|
type: SearchQueryTypes.any_text,
|
||||||
text: 'wont find it'
|
text: 'wont find it'
|
||||||
|
@ -146,7 +146,7 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => {
|
|||||||
const setUpSqlDB = async () => {
|
const setUpSqlDB = async () => {
|
||||||
await sqlHelper.initDB();
|
await sqlHelper.initDB();
|
||||||
await setUpTestGallery();
|
await setUpTestGallery();
|
||||||
await ObjectManagers.InitSQLManagers();
|
await ObjectManagers.getInstance().init();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ describe('DiskMangerWorker', () => {
|
|||||||
Config.Database.type = DatabaseType.sqlite;
|
Config.Database.type = DatabaseType.sqlite;
|
||||||
Config.Faces.enabled = true;
|
Config.Faces.enabled = true;
|
||||||
Config.Faces.keywordsToPersons = true;
|
Config.Faces.keywordsToPersons = true;
|
||||||
|
Config.Extensions.enabled = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ describe('DiskMangerWorker', () => {
|
|||||||
const dir = await DiskManager.scanDirectory('/');
|
const dir = await DiskManager.scanDirectory('/');
|
||||||
// should match the number of media (photo/video) files in the assets folder
|
// should match the number of media (photo/video) files in the assets folder
|
||||||
expect(dir.media.length).to.be.equals(10);
|
expect(dir.media.length).to.be.equals(10);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const expected = require(path.join(__dirname, '/../../../assets/test image öüóőúéáű-.,.json'));
|
const expected = require(path.join(__dirname, '/../../../assets/test image öüóőúéáű-.,.json'));
|
||||||
const i = dir.media.findIndex(m => m.name === 'test image öüóőúéáű-.,.jpg');
|
const i = dir.media.findIndex(m => m.name === 'test image öüóőúéáű-.,.jpg');
|
||||||
expect(Utils.clone(dir.media[i].name)).to.be.deep.equal('test image öüóőúéáű-.,.jpg');
|
expect(Utils.clone(dir.media[i].name)).to.be.deep.equal('test image öüóőúéáű-.,.jpg');
|
||||||
|
@ -18,6 +18,7 @@ describe('MetadataLoader', () => {
|
|||||||
Config.Database.type = DatabaseType.sqlite;
|
Config.Database.type = DatabaseType.sqlite;
|
||||||
Config.Faces.enabled = true;
|
Config.Faces.enabled = true;
|
||||||
Config.Faces.keywordsToPersons = true;
|
Config.Faces.keywordsToPersons = true;
|
||||||
|
Config.Extensions.enabled = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user