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.*
|
||||
/db/
|
||||
/test/cypress/screenshots/
|
||||
/extensions/
|
||||
|
@ -121,7 +121,7 @@ export class BenchmarkRunner {
|
||||
const bm = new Benchmark('List directory', req,
|
||||
async (): Promise<void> => {
|
||||
await ObjectManagers.reset();
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
await ObjectManagers.getInstance().init();
|
||||
}, null,
|
||||
async (): Promise<void> => {
|
||||
Config.Indexing.reIndexingSensitivity = ReIndexingSensitivity.low;
|
||||
@ -135,7 +135,7 @@ export class BenchmarkRunner {
|
||||
async bmListPersons(): Promise<BenchmarkResult[]> {
|
||||
const bm = new Benchmark('Listing Faces', Utils.clone(this.requestTemplate), async (): Promise<void> => {
|
||||
await ObjectManagers.reset();
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
await ObjectManagers.getInstance().init();
|
||||
}, null,
|
||||
async (): Promise<void> => {
|
||||
Config.Indexing.reIndexingSensitivity = ReIndexingSensitivity.low;
|
||||
@ -289,7 +289,7 @@ export class BenchmarkRunner {
|
||||
await fs.promises.rm(ProjectPath.DBFolder, {recursive: true, force: true});
|
||||
Config.Database.type = DatabaseType.sqlite;
|
||||
Config.Jobs.scheduled = [];
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
await ObjectManagers.getInstance().init();
|
||||
};
|
||||
|
||||
private async setupDB(): Promise<void> {
|
||||
|
21
package-lock.json
generated
21
package-lock.json
generated
@ -90,7 +90,7 @@
|
||||
"codelyzer": "6.0.2",
|
||||
"core-js": "3.29.0",
|
||||
"coveralls": "3.1.1",
|
||||
"cypress": "latest",
|
||||
"cypress": "13.1.0",
|
||||
"deep-equal-in-any-order": "2.0.5",
|
||||
"ejs-loader": "0.5.0",
|
||||
"eslint": "8.36.0",
|
||||
@ -132,6 +132,13 @@
|
||||
"mysql": "2.18.1"
|
||||
}
|
||||
},
|
||||
"extensions/logger": {
|
||||
"version": "1.0.0",
|
||||
"extraneous": true,
|
||||
"dependencies": {
|
||||
"lodash": "4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
|
||||
@ -8646,9 +8653,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cypress/node_modules/@types/node": {
|
||||
"version": "16.18.48",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.48.tgz",
|
||||
"integrity": "sha512-mlaecDKQ7rIZrYD7iiKNdzFb6e/qD5I9U1rAhq+Fd+DWvYVs+G2kv74UFHmSOlg5+i/vF3XxuR522V4u8BqO+Q==",
|
||||
"version": "16.18.61",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.61.tgz",
|
||||
"integrity": "sha512-k0N7BqGhJoJzdh6MuQg1V1ragJiXTh8VUBAZTWjJ9cUq23SG0F0xavOwZbhiP4J3y20xd6jxKx+xNUhkMAi76Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cypress/node_modules/ansi-styles": {
|
||||
@ -30554,9 +30561,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "16.18.48",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.48.tgz",
|
||||
"integrity": "sha512-mlaecDKQ7rIZrYD7iiKNdzFb6e/qD5I9U1rAhq+Fd+DWvYVs+G2kv74UFHmSOlg5+i/vF3XxuR522V4u8BqO+Q==",
|
||||
"version": "16.18.61",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.61.tgz",
|
||||
"integrity": "sha512-k0N7BqGhJoJzdh6MuQg1V1ragJiXTh8VUBAZTWjJ9cUq23SG0F0xavOwZbhiP4J3y20xd6jxKx+xNUhkMAi76Q==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
|
@ -13,8 +13,8 @@
|
||||
"create-release": "gulp create-release",
|
||||
"build-backend": "tsc",
|
||||
"pretest": "tsc",
|
||||
"test": "ng test && nyc mocha --recursive test",
|
||||
"test-backend": "tsc && mocha --recursive test",
|
||||
"test": "ng test && nyc mocha --recursive test --exclude test/cypress/**/*.js",
|
||||
"test-backend": "tsc && mocha --recursive test --exclude test/cypress/**/*.js",
|
||||
"coverage": "nyc report --reporter=lcov",
|
||||
"start": "node ./src/backend/index",
|
||||
"run-dev": "ng build --configuration=dev",
|
||||
@ -113,7 +113,7 @@
|
||||
"codelyzer": "6.0.2",
|
||||
"core-js": "3.29.0",
|
||||
"coveralls": "3.1.1",
|
||||
"cypress": "latest",
|
||||
"cypress": "13.1.0",
|
||||
"deep-equal-in-any-order": "2.0.5",
|
||||
"ejs-loader": "0.5.0",
|
||||
"eslint": "8.36.0",
|
||||
|
@ -7,10 +7,42 @@ const forcedDebug = process.env['NODE_ENV'] === 'debug';
|
||||
|
||||
if (forcedDebug === true) {
|
||||
console.log(
|
||||
'NODE_ENV environmental variable is set to debug, forcing all logs to print'
|
||||
'NODE_ENV environmental variable is set to debug, forcing all logs to print'
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
public static silly(...args: (string | number)[]): void {
|
||||
if (!forcedDebug && Config.Server.Log.level < LogLevel.silly) {
|
||||
@ -55,10 +87,10 @@ export class Logger {
|
||||
const date = new Date().toLocaleString();
|
||||
let LOG_TAG = '';
|
||||
if (
|
||||
args.length > 0 &&
|
||||
typeof args[0] === 'string' &&
|
||||
args[0].startsWith('[') &&
|
||||
args[0].endsWith(']')
|
||||
args.length > 0 &&
|
||||
typeof args[0] === 'string' &&
|
||||
args[0].startsWith('[') &&
|
||||
args[0].endsWith(']')
|
||||
) {
|
||||
LOG_TAG = args[0];
|
||||
args.shift();
|
||||
|
@ -2,28 +2,29 @@ import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import {Config} from '../common/config/private/Config';
|
||||
|
||||
class ProjectPathClass {
|
||||
export class ProjectPathClass {
|
||||
public Root: string;
|
||||
public ImageFolder: string;
|
||||
public TempFolder: string;
|
||||
public TranscodedFolder: string;
|
||||
public FacesFolder: string;
|
||||
public FrontendFolder: string;
|
||||
public ExtensionFolder: string;
|
||||
public DBFolder: string;
|
||||
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
normalizeRelative(pathStr: string): string {
|
||||
public normalizeRelative(pathStr: string): string {
|
||||
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);
|
||||
}
|
||||
|
||||
getRelativePathToImages(pathStr: string): string {
|
||||
public getRelativePathToImages(pathStr: string): string {
|
||||
return path.relative(this.ImageFolder, pathStr);
|
||||
}
|
||||
|
||||
@ -35,6 +36,7 @@ class ProjectPathClass {
|
||||
this.TranscodedFolder = path.join(this.TempFolder, 'tc');
|
||||
this.FacesFolder = path.join(this.TempFolder, 'f');
|
||||
this.DBFolder = this.getAbsolutePath(Config.Database.dbFolder);
|
||||
this.ExtensionFolder = path.join(this.Root, 'extensions');
|
||||
|
||||
// create thumbnail folder if not exist
|
||||
if (!fs.existsSync(this.TempFolder)) {
|
||||
|
@ -11,5 +11,5 @@ if ((process.argv || []).includes('--run-diagnostics')) {
|
||||
process.exit(0);
|
||||
});
|
||||
} else {
|
||||
new Server();
|
||||
Server.getInstance();
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import {SharingDTO} from '../../common/entities/SharingDTO';
|
||||
import {Utils} from '../../common/Utils';
|
||||
import {LoggerRouter} from '../routes/LoggerRouter';
|
||||
import {TAGS} from '../../common/config/public/ClientConfig';
|
||||
import {ExtensionConfigWrapper} from '../model/extension/ExtensionConfigWrapper';
|
||||
|
||||
const forcedDebug = process.env['NODE_ENV'] === 'debug';
|
||||
|
||||
@ -107,7 +108,7 @@ export class RenderingMWs {
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
const originalConf = await Config.original();
|
||||
const originalConf = await ExtensionConfigWrapper.original();
|
||||
// These are sensitive information, do not send to the client side
|
||||
originalConf.Server.sessionSecret = null;
|
||||
const message = new Message<PrivateConfigClass>(
|
||||
|
@ -2,18 +2,19 @@ import {NextFunction, Request, Response} from 'express';
|
||||
import {ErrorCodes, ErrorDTO} from '../../../common/entities/Error';
|
||||
import {ObjectManagers} from '../../model/ObjectManagers';
|
||||
import {StatisticDTO} from '../../../common/entities/settings/StatisticDTO';
|
||||
import {MessengerRepository} from '../../model/messenger/MessengerRepository';
|
||||
|
||||
export class AdminMWs {
|
||||
public static async loadStatistic(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
|
||||
const galleryManager = ObjectManagers.getInstance()
|
||||
.GalleryManager;
|
||||
.GalleryManager;
|
||||
const personManager = ObjectManagers.getInstance()
|
||||
.PersonManager;
|
||||
.PersonManager;
|
||||
try {
|
||||
req.resultPipe = {
|
||||
directories: await galleryManager.countDirectories(),
|
||||
@ -26,87 +27,87 @@ export class AdminMWs {
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error while getting statistic: ' + err.toString(),
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error while getting statistic: ' + err.toString(),
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error while getting statistic',
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error while getting statistic',
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async getDuplicates(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
|
||||
try {
|
||||
req.resultPipe = await ObjectManagers.getInstance()
|
||||
.GalleryManager.getPossibleDuplicates();
|
||||
.GalleryManager.getPossibleDuplicates();
|
||||
return next();
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error while getting duplicates: ' + err.toString(),
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error while getting duplicates: ' + err.toString(),
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error while getting duplicates',
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.GENERAL_ERROR,
|
||||
'Error while getting duplicates',
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async startJob(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> {
|
||||
try {
|
||||
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 allowParallelRun: boolean = req.body.allowParallelRun;
|
||||
await ObjectManagers.getInstance().JobManager.run(
|
||||
id,
|
||||
JobConfig,
|
||||
soloRun,
|
||||
allowParallelRun
|
||||
id,
|
||||
JobConfig,
|
||||
soloRun,
|
||||
allowParallelRun
|
||||
);
|
||||
req.resultPipe = 'ok';
|
||||
return next();
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.JOB_ERROR,
|
||||
'Job error: ' + err.toString(),
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.JOB_ERROR,
|
||||
'Job error: ' + err.toString(),
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.JOB_ERROR,
|
||||
'Job error: ' + JSON.stringify(err, null, ' '),
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.JOB_ERROR,
|
||||
'Job error: ' + JSON.stringify(err, null, ' '),
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -120,56 +121,85 @@ export class AdminMWs {
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.JOB_ERROR,
|
||||
'Job error: ' + err.toString(),
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.JOB_ERROR,
|
||||
'Job error: ' + err.toString(),
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.JOB_ERROR,
|
||||
'Job error: ' + JSON.stringify(err, null, ' '),
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
'Job error: ' + JSON.stringify(err, null, ' '),
|
||||
err
|
||||
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(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void {
|
||||
try {
|
||||
req.resultPipe =
|
||||
ObjectManagers.getInstance().JobManager.getAvailableJobs();
|
||||
ObjectManagers.getInstance().JobManager.getAvailableJobs();
|
||||
return next();
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.JOB_ERROR,
|
||||
'Job error: ' + err.toString(),
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.JOB_ERROR,
|
||||
'Job error: ' + err.toString(),
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.JOB_ERROR,
|
||||
'Job error: ' + JSON.stringify(err, null, ' '),
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.JOB_ERROR,
|
||||
'Job error: ' + JSON.stringify(err, null, ' '),
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static getJobProgresses(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void {
|
||||
try {
|
||||
req.resultPipe = ObjectManagers.getInstance().JobManager.getProgresses();
|
||||
@ -177,19 +207,19 @@ export class AdminMWs {
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.JOB_ERROR,
|
||||
'Job error: ' + err.toString(),
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.JOB_ERROR,
|
||||
'Job error: ' + err.toString(),
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
return next(
|
||||
new ErrorDTO(
|
||||
ErrorCodes.JOB_ERROR,
|
||||
'Job error: ' + JSON.stringify(err, null, ' '),
|
||||
err
|
||||
)
|
||||
new ErrorDTO(
|
||||
ErrorCodes.JOB_ERROR,
|
||||
'Job error: ' + JSON.stringify(err, null, ' '),
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,10 @@ import {ErrorCodes, ErrorDTO} from '../../../common/entities/Error';
|
||||
import {Logger} from '../../Logger';
|
||||
import {Config} from '../../../common/config/private/Config';
|
||||
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 {ObjectManagers} from '../../model/ObjectManagers';
|
||||
import {ExtensionConfigWrapper} from '../../model/extension/ExtensionConfigWrapper';
|
||||
|
||||
const LOG_TAG = '[SettingsMWs]';
|
||||
|
||||
@ -28,7 +29,7 @@ export class SettingsMWs {
|
||||
try {
|
||||
let settings = req.body.settings; // Top level settings JSON
|
||||
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
|
||||
// @ts-ignore
|
||||
transformer[settingsPath] = settings;
|
||||
@ -37,7 +38,7 @@ export class SettingsMWs {
|
||||
settings = ConfigClassBuilder.attachPrivateInterface(transformer[settingsPath]).toJSON({
|
||||
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)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
@ -14,6 +14,7 @@ import {AlbumManager} from './database/AlbumManager';
|
||||
import {PersonManager} from './database/PersonManager';
|
||||
import {SharingManager} from './database/SharingManager';
|
||||
import {IObjectManager} from './database/IObjectManager';
|
||||
import {ExtensionManager} from './extension/ExtensionManager';
|
||||
|
||||
const LOG_TAG = '[ObjectManagers]';
|
||||
|
||||
@ -32,11 +33,99 @@ export class ObjectManagers {
|
||||
private jobManager: JobManager;
|
||||
private locationManager: LocationManager;
|
||||
private albumManager: AlbumManager;
|
||||
private extensionManager: ExtensionManager;
|
||||
private initDone = false;
|
||||
|
||||
constructor() {
|
||||
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 {
|
||||
return this.versionManager;
|
||||
}
|
||||
@ -169,62 +258,15 @@ export class ObjectManagers {
|
||||
this.managers.push(this.jobManager as IObjectManager);
|
||||
}
|
||||
|
||||
public static getInstance(): ObjectManagers {
|
||||
if (this.instance === null) {
|
||||
this.instance = new ObjectManagers();
|
||||
}
|
||||
return this.instance;
|
||||
get ExtensionManager(): ExtensionManager {
|
||||
return this.extensionManager;
|
||||
}
|
||||
|
||||
public static async reset(): Promise<void> {
|
||||
Logger.silly(LOG_TAG, 'Object manager reset begin');
|
||||
if (
|
||||
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);
|
||||
}
|
||||
set ExtensionManager(value: ExtensionManager) {
|
||||
if (this.extensionManager) {
|
||||
this.managers.splice(this.managers.indexOf(this.extensionManager as IObjectManager), 1);
|
||||
}
|
||||
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> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
const cover =
|
||||
await ObjectManagers.getInstance().CoverManager.getAlbumCover(album);
|
||||
await ObjectManagers.getInstance().CoverManager.getCoverForAlbum(album);
|
||||
const count = await
|
||||
ObjectManagers.getInstance().SearchManager.getCount((album as SavedSearchDTO).searchQuery);
|
||||
|
||||
|
@ -14,6 +14,7 @@ import {CoverPhotoDTO} from '../../../common/entities/PhotoDTO';
|
||||
import {IObjectManager} from './IObjectManager';
|
||||
import {Logger} from '../../Logger';
|
||||
import {SearchManager} from './SearchManager';
|
||||
import {ExtensionDecorator} from '../extension/ExtensionDecorator';
|
||||
|
||||
const LOG_TAG = '[CoverManager]';
|
||||
|
||||
@ -29,21 +30,22 @@ export class CoverManager implements IObjectManager {
|
||||
public async resetCovers(): Promise<void> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
await connection
|
||||
.createQueryBuilder()
|
||||
.update(DirectoryEntity)
|
||||
.set({validCover: false})
|
||||
.execute();
|
||||
.createQueryBuilder()
|
||||
.update(DirectoryEntity)
|
||||
.set({validCover: false})
|
||||
.execute();
|
||||
}
|
||||
|
||||
public async onNewDataVersion(changedDir: ParentDirectoryDTO): Promise<void> {
|
||||
@ExtensionDecorator(e => e.gallery.CoverManager.invalidateDirectoryCovers)
|
||||
protected async invalidateDirectoryCovers(dir: ParentDirectoryDTO) {
|
||||
// Invalidating Album cover
|
||||
let fullPath = DiskManager.normalizeDirPath(
|
||||
path.join(changedDir.path, changedDir.name)
|
||||
path.join(dir.path, dir.name)
|
||||
);
|
||||
const query = (await SQLConnection.getConnection())
|
||||
.createQueryBuilder()
|
||||
.update(DirectoryEntity)
|
||||
.set({validCover: false});
|
||||
.createQueryBuilder()
|
||||
.update(DirectoryEntity)
|
||||
.set({validCover: false});
|
||||
|
||||
let i = 0;
|
||||
const root = DiskManager.pathFromRelativeDirName('.');
|
||||
@ -53,62 +55,67 @@ export class CoverManager implements IObjectManager {
|
||||
fullPath = parentPath;
|
||||
++i;
|
||||
query.orWhere(
|
||||
new Brackets((q: WhereExpression) => {
|
||||
const param: { [key: string]: string } = {};
|
||||
param['name' + i] = name;
|
||||
param['path' + i] = parentPath;
|
||||
q.where(`path = :path${i}`, param);
|
||||
q.andWhere(`name = :name${i}`, param);
|
||||
})
|
||||
new Brackets((q: WhereExpression) => {
|
||||
const param: { [key: string]: string } = {};
|
||||
param['name' + i] = name;
|
||||
param['path' + i] = parentPath;
|
||||
q.where(`path = :path${i}`, param);
|
||||
q.andWhere(`name = :name${i}`, param);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
++i;
|
||||
query.orWhere(
|
||||
new Brackets((q: WhereExpression) => {
|
||||
const param: { [key: string]: string } = {};
|
||||
param['name' + i] = DiskManager.dirName('.');
|
||||
param['path' + i] = DiskManager.pathFromRelativeDirName('.');
|
||||
q.where(`path = :path${i}`, param);
|
||||
q.andWhere(`name = :name${i}`, param);
|
||||
})
|
||||
new Brackets((q: WhereExpression) => {
|
||||
const param: { [key: string]: string } = {};
|
||||
param['name' + i] = DiskManager.dirName('.');
|
||||
param['path' + i] = DiskManager.pathFromRelativeDirName('.');
|
||||
q.where(`path = :path${i}`, param);
|
||||
q.andWhere(`name = :name${i}`, param);
|
||||
})
|
||||
);
|
||||
|
||||
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;
|
||||
}): Promise<CoverPhotoDTOWithID> {
|
||||
const albumQuery: Brackets = await
|
||||
ObjectManagers.getInstance().SearchManager.prepareAndBuildWhereQuery(album.searchQuery);
|
||||
ObjectManagers.getInstance().SearchManager.prepareAndBuildWhereQuery(album.searchQuery);
|
||||
const connection = await SQLConnection.getConnection();
|
||||
|
||||
const coverQuery = (): SelectQueryBuilder<MediaEntity> => {
|
||||
const query = connection
|
||||
.getRepository(MediaEntity)
|
||||
.createQueryBuilder('media')
|
||||
.innerJoin('media.directory', 'directory')
|
||||
.select(['media.name', 'media.id', ...CoverManager.DIRECTORY_SELECT])
|
||||
.where(albumQuery);
|
||||
.getRepository(MediaEntity)
|
||||
.createQueryBuilder('media')
|
||||
.innerJoin('media.directory', 'directory')
|
||||
.select(['media.name', 'media.id', ...CoverManager.DIRECTORY_SELECT])
|
||||
.where(albumQuery);
|
||||
SearchManager.setSorting(query, Config.AlbumCover.Sorting);
|
||||
return query;
|
||||
};
|
||||
let coverMedia = null;
|
||||
if (
|
||||
Config.AlbumCover.SearchQuery &&
|
||||
!Utils.equalsFilter(Config.AlbumCover.SearchQuery, {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: '',
|
||||
} as TextSearch)
|
||||
Config.AlbumCover.SearchQuery &&
|
||||
!Utils.equalsFilter(Config.AlbumCover.SearchQuery, {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: '',
|
||||
} as TextSearch)
|
||||
) {
|
||||
try {
|
||||
const coverFilterQuery = await
|
||||
ObjectManagers.getInstance().SearchManager.prepareAndBuildWhereQuery(Config.AlbumCover.SearchQuery);
|
||||
ObjectManagers.getInstance().SearchManager.prepareAndBuildWhereQuery(Config.AlbumCover.SearchQuery);
|
||||
coverMedia = await coverQuery()
|
||||
.andWhere(coverFilterQuery)
|
||||
.limit(1)
|
||||
.getOne();
|
||||
.andWhere(coverFilterQuery)
|
||||
.limit(1)
|
||||
.getOne();
|
||||
} catch (e) {
|
||||
Logger.error(LOG_TAG, 'Cant get album cover using:', JSON.stringify(album.searchQuery), JSON.stringify(Config.AlbumCover.SearchQuery));
|
||||
throw e;
|
||||
@ -127,52 +134,53 @@ export class CoverManager implements IObjectManager {
|
||||
}
|
||||
|
||||
public async getPartialDirsWithoutCovers(): Promise<
|
||||
{ id: number; name: string; path: string }[]
|
||||
{ id: number; name: string; path: string }[]
|
||||
> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
return await connection
|
||||
.getRepository(DirectoryEntity)
|
||||
.createQueryBuilder('directory')
|
||||
.where('directory.validCover = :validCover', {validCover: 0}) // 0 === false
|
||||
.select(['name', 'id', 'path'])
|
||||
.getRawMany();
|
||||
.getRepository(DirectoryEntity)
|
||||
.createQueryBuilder('directory')
|
||||
.where('directory.validCover = :validCover', {validCover: 0}) // 0 === false
|
||||
.select(['name', 'id', 'path'])
|
||||
.getRawMany();
|
||||
}
|
||||
|
||||
public async setAndGetCoverForDirectory(dir: {
|
||||
@ExtensionDecorator(e => e.gallery.CoverManager.getCoverForDirectory)
|
||||
protected async getCoverForDirectory(dir: {
|
||||
id: number;
|
||||
name: string;
|
||||
path: string;
|
||||
}): Promise<CoverPhotoDTOWithID> {
|
||||
}) {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
const coverQuery = (): SelectQueryBuilder<MediaEntity> => {
|
||||
const query = connection
|
||||
.getRepository(MediaEntity)
|
||||
.createQueryBuilder('media')
|
||||
.innerJoin('media.directory', 'directory')
|
||||
.select(['media.name', 'media.id', ...CoverManager.DIRECTORY_SELECT])
|
||||
.where(
|
||||
new Brackets((q: WhereExpression) => {
|
||||
q.where('media.directory = :dir', {
|
||||
dir: dir.id,
|
||||
});
|
||||
if (Config.Database.type === DatabaseType.mysql) {
|
||||
q.orWhere('directory.path like :path || \'%\'', {
|
||||
path: DiskManager.pathFromParent(dir),
|
||||
});
|
||||
} else {
|
||||
q.orWhere('directory.path GLOB :path', {
|
||||
path: DiskManager.pathFromParent(dir)
|
||||
// glob escaping. see https://github.com/bpatrik/pigallery2/issues/621
|
||||
.replaceAll('[', '[[]') + '*',
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
.getRepository(MediaEntity)
|
||||
.createQueryBuilder('media')
|
||||
.innerJoin('media.directory', 'directory')
|
||||
.select(['media.name', 'media.id', ...CoverManager.DIRECTORY_SELECT])
|
||||
.where(
|
||||
new Brackets((q: WhereExpression) => {
|
||||
q.where('media.directory = :dir', {
|
||||
dir: dir.id,
|
||||
});
|
||||
if (Config.Database.type === DatabaseType.mysql) {
|
||||
q.orWhere('directory.path like :path || \'%\'', {
|
||||
path: DiskManager.pathFromParent(dir),
|
||||
});
|
||||
} else {
|
||||
q.orWhere('directory.path GLOB :path', {
|
||||
path: DiskManager.pathFromParent(dir)
|
||||
// glob escaping. see https://github.com/bpatrik/pigallery2/issues/621
|
||||
.replaceAll('[', '[[]') + '*',
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
// Select from the directory if any otherwise from any subdirectories.
|
||||
// (There is no priority between subdirectories)
|
||||
query.orderBy(
|
||||
`CASE WHEN directory.id = ${dir.id} THEN 0 ELSE 1 END`,
|
||||
'ASC'
|
||||
`CASE WHEN directory.id = ${dir.id} THEN 0 ELSE 1 END`,
|
||||
'ASC'
|
||||
);
|
||||
|
||||
SearchManager.setSorting(query, Config.AlbumCover.Sorting);
|
||||
@ -181,33 +189,43 @@ export class CoverManager implements IObjectManager {
|
||||
|
||||
let coverMedia: CoverPhotoDTOWithID = null;
|
||||
if (
|
||||
Config.AlbumCover.SearchQuery &&
|
||||
!Utils.equalsFilter(Config.AlbumCover.SearchQuery, {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: '',
|
||||
} as TextSearch)
|
||||
Config.AlbumCover.SearchQuery &&
|
||||
!Utils.equalsFilter(Config.AlbumCover.SearchQuery, {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: '',
|
||||
} as TextSearch)
|
||||
) {
|
||||
coverMedia = await coverQuery()
|
||||
.andWhere(
|
||||
await ObjectManagers.getInstance().SearchManager.prepareAndBuildWhereQuery(Config.AlbumCover.SearchQuery)
|
||||
)
|
||||
.limit(1)
|
||||
.getOne();
|
||||
.andWhere(
|
||||
await ObjectManagers.getInstance().SearchManager.prepareAndBuildWhereQuery(Config.AlbumCover.SearchQuery)
|
||||
)
|
||||
.limit(1)
|
||||
.getOne();
|
||||
}
|
||||
|
||||
if (!coverMedia) {
|
||||
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)
|
||||
await connection
|
||||
.createQueryBuilder()
|
||||
.update(DirectoryEntity)
|
||||
.set({cover: coverMedia, validCover: true})
|
||||
.where('id = :dir', {
|
||||
dir: dir.id,
|
||||
})
|
||||
.execute();
|
||||
.createQueryBuilder()
|
||||
.update(DirectoryEntity)
|
||||
.set({cover: coverMedia, validCover: true})
|
||||
.where('id = :dir', {
|
||||
dir: dir.id,
|
||||
})
|
||||
.execute();
|
||||
|
||||
return coverMedia || null;
|
||||
}
|
||||
|
@ -2,4 +2,6 @@ import {ParentDirectoryDTO} from '../../../common/entities/DirectoryDTO';
|
||||
|
||||
export interface IObjectManager {
|
||||
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] };
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@ -37,10 +69,10 @@ export class SQLConnection {
|
||||
const options = this.getDriver(Config.Database);
|
||||
|
||||
Logger.debug(
|
||||
LOG_TAG,
|
||||
'Creating connection: ' + DatabaseType[Config.Database.type],
|
||||
', with driver:',
|
||||
options.type
|
||||
LOG_TAG,
|
||||
'Creating connection: ' + DatabaseType[Config.Database.type],
|
||||
', with driver:',
|
||||
options.type
|
||||
);
|
||||
this.connection = await this.createConnection(options);
|
||||
await SQLConnection.schemeSync(this.connection);
|
||||
@ -49,7 +81,7 @@ export class SQLConnection {
|
||||
}
|
||||
|
||||
public static async tryConnection(
|
||||
config: ServerDataBaseConfig
|
||||
config: ServerDataBaseConfig
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await getConnection('test').close();
|
||||
@ -73,8 +105,8 @@ export class SQLConnection {
|
||||
// Adding enforced users to the db
|
||||
const userRepository = connection.getRepository(UserEntity);
|
||||
if (
|
||||
Array.isArray(Config.Users.enforcedUsers) &&
|
||||
Config.Users.enforcedUsers.length > 0
|
||||
Array.isArray(Config.Users.enforcedUsers) &&
|
||||
Config.Users.enforcedUsers.length > 0
|
||||
) {
|
||||
for (let i = 0; i < Config.Users.enforcedUsers.length; ++i) {
|
||||
const uc = Config.Users.enforcedUsers[i];
|
||||
@ -106,12 +138,12 @@ export class SQLConnection {
|
||||
role: UserRoles.Admin,
|
||||
});
|
||||
if (
|
||||
defAdmin &&
|
||||
PasswordHelper.comparePassword('admin', defAdmin.password)
|
||||
defAdmin &&
|
||||
PasswordHelper.comparePassword('admin', defAdmin.password)
|
||||
) {
|
||||
NotificationManager.error(
|
||||
'Using default admin user!',
|
||||
'You are using the default admin/admin user/password, please change or remove it.'
|
||||
'Using default admin user!',
|
||||
'You are using the default admin/admin user/password, please change or remove it.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -128,12 +160,39 @@ 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 {
|
||||
return path.join(ProjectPath.getAbsolutePath(config.dbFolder), 'sqlite.db');
|
||||
}
|
||||
|
||||
private static async createConnection(
|
||||
options: DataSourceOptions
|
||||
options: DataSourceOptions
|
||||
): Promise<Connection> {
|
||||
if (options.type === 'sqlite' || options.type === 'better-sqlite3') {
|
||||
return await createConnection(options);
|
||||
@ -149,7 +208,7 @@ export class SQLConnection {
|
||||
delete tmpOption.database;
|
||||
const tmpConn = await createConnection(tmpOption);
|
||||
await tmpConn.query(
|
||||
'CREATE DATABASE IF NOT EXISTS ' + options.database
|
||||
'CREATE DATABASE IF NOT EXISTS ' + options.database
|
||||
);
|
||||
await tmpConn.close();
|
||||
return await createConnection(options);
|
||||
@ -177,9 +236,9 @@ export class SQLConnection {
|
||||
let users: UserEntity[] = [];
|
||||
try {
|
||||
users = await connection
|
||||
.getRepository(UserEntity)
|
||||
.createQueryBuilder('user')
|
||||
.getMany();
|
||||
.getRepository(UserEntity)
|
||||
.createQueryBuilder('user')
|
||||
.getMany();
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (ex) {
|
||||
}
|
||||
@ -193,9 +252,9 @@ export class SQLConnection {
|
||||
await connection.synchronize();
|
||||
await connection.getRepository(VersionEntity).save(version);
|
||||
Logger.warn(
|
||||
LOG_TAG,
|
||||
'Could not move users to the new db scheme, deleting them. Details:' +
|
||||
e.toString()
|
||||
LOG_TAG,
|
||||
'Could not move users to the new db scheme, deleting them. Details:' +
|
||||
e.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -217,26 +276,12 @@ export class SQLConnection {
|
||||
driver = {
|
||||
type: 'better-sqlite3',
|
||||
database: path.join(
|
||||
ProjectPath.getAbsolutePath(config.dbFolder),
|
||||
config.sqlite.DBFileName
|
||||
ProjectPath.getAbsolutePath(config.dbFolder),
|
||||
config.sqlite.DBFileName
|
||||
),
|
||||
};
|
||||
}
|
||||
driver.entities = [
|
||||
UserEntity,
|
||||
FileEntity,
|
||||
MDFileEntity,
|
||||
PersonJunctionTable,
|
||||
PersonEntry,
|
||||
MediaEntity,
|
||||
PhotoEntity,
|
||||
VideoEntity,
|
||||
DirectoryEntity,
|
||||
SharingEntity,
|
||||
AlbumBaseEntity,
|
||||
SavedSearchEntity,
|
||||
VersionEntity,
|
||||
];
|
||||
driver.entities = this.entries;
|
||||
driver.synchronize = false;
|
||||
if (Config.Server.Log.sqlLevel !== SQLLogLevel.none) {
|
||||
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 {MetadataLoader} from './MetadataLoader';
|
||||
import {NotificationManager} from '../NotifocationManager';
|
||||
import {ExtensionDecorator} from '../extension/ExtensionDecorator';
|
||||
|
||||
|
||||
const LOG_TAG = '[DiskManager]';
|
||||
@ -101,6 +102,7 @@ export class DiskManager {
|
||||
)) as ParentDirectoryDTO<FileDTO>;
|
||||
}
|
||||
|
||||
@ExtensionDecorator(e => e.gallery.DiskManager.scanDirectory)
|
||||
public static async scanDirectory(
|
||||
relativeDirectoryName: string,
|
||||
settings: DirectoryScanSettings = {}
|
||||
|
@ -12,6 +12,7 @@ import {IptcParser} from 'ts-node-iptc';
|
||||
import {FFmpegFactory} from '../FFmpegFactory';
|
||||
import {FfprobeData} from 'fluent-ffmpeg';
|
||||
import {Utils} from '../../../common/Utils';
|
||||
import { ExtensionDecorator } from '../extension/ExtensionDecorator';
|
||||
import * as exifr from 'exifr';
|
||||
import * as path from 'path';
|
||||
|
||||
@ -19,6 +20,8 @@ const LOG_TAG = '[MetadataLoader]';
|
||||
const ffmpeg = FFmpegFactory.get();
|
||||
|
||||
export class MetadataLoader {
|
||||
|
||||
@ExtensionDecorator(e=>e.gallery.MetadataLoader.loadVideoMetadata)
|
||||
public static loadVideoMetadata(fullPath: string): Promise<VideoMetadata> {
|
||||
return new Promise<VideoMetadata>((resolve) => {
|
||||
const metadata: VideoMetadata = {
|
||||
@ -153,6 +156,7 @@ export class MetadataLoader {
|
||||
fileSize: 0,
|
||||
};
|
||||
|
||||
@ExtensionDecorator(e=>e.gallery.MetadataLoader.loadPhotoMetadata)
|
||||
public static loadPhotoMetadata(fullPath: string): Promise<PhotoMetadata> {
|
||||
return new Promise<PhotoMetadata>((resolve, reject) => {
|
||||
try {
|
||||
@ -189,7 +193,7 @@ export class MetadataLoader {
|
||||
fullPathWithoutExt + '.xmp',
|
||||
fullPathWithoutExt + '.XMP',
|
||||
];
|
||||
|
||||
|
||||
for (const sidecarPath of sidecarPaths) {
|
||||
if (fs.existsSync(sidecarPath)) {
|
||||
const sidecarData = exifr.sidecar(sidecarPath);
|
||||
|
@ -5,6 +5,7 @@ import {Logger} from '../../Logger';
|
||||
import {FfmpegCommand, FfprobeData} from 'fluent-ffmpeg';
|
||||
import {FFmpegFactory} from '../FFmpegFactory';
|
||||
import * as path from 'path';
|
||||
import {ExtensionDecorator} from '../extension/ExtensionDecorator';
|
||||
|
||||
|
||||
sharp.cache(false);
|
||||
@ -129,6 +130,7 @@ export class VideoRendererFactory {
|
||||
|
||||
export class ImageRendererFactory {
|
||||
|
||||
@ExtensionDecorator(e=>e.gallery.ImageRenderer.render)
|
||||
public static async render(input: MediaRendererInput | SvgRendererInput): Promise<void> {
|
||||
|
||||
let image: Sharp;
|
||||
|
@ -10,14 +10,15 @@ import {JobProgress} from './jobs/JobProgress';
|
||||
import {JobProgressManager} from './JobProgressManager';
|
||||
import {JobDTOUtils} from '../../../common/entities/job/JobDTO';
|
||||
import {Utils} from '../../../common/Utils';
|
||||
import {IObjectManager} from '../database/IObjectManager';
|
||||
|
||||
const LOG_TAG = '[JobManager]';
|
||||
|
||||
export class JobManager implements IJobListener {
|
||||
export class JobManager implements IJobListener, IObjectManager {
|
||||
protected timers: { schedule: JobScheduleDTO; timer: NodeJS.Timeout }[] = [];
|
||||
protected progressManager: JobProgressManager = null;
|
||||
|
||||
constructor() {
|
||||
async init(){
|
||||
this.progressManager = new JobProgressManager();
|
||||
this.runSchedules();
|
||||
}
|
||||
@ -49,7 +50,7 @@ export class JobManager implements IJobListener {
|
||||
return prg;
|
||||
}
|
||||
|
||||
public async run<T>(
|
||||
public async run<T extends Record<string, unknown>>(
|
||||
jobName: string,
|
||||
config: T,
|
||||
soloRun: boolean,
|
||||
@ -85,7 +86,7 @@ export class JobManager implements IJobListener {
|
||||
};
|
||||
|
||||
onJobFinished = async (
|
||||
job: IJob<unknown>,
|
||||
job: IJob,
|
||||
state: JobProgressStates,
|
||||
soloRun: boolean
|
||||
): Promise<void> => {
|
||||
@ -120,11 +121,16 @@ export class JobManager implements IJobListener {
|
||||
}
|
||||
};
|
||||
|
||||
getAvailableJobs(): IJob<unknown>[] {
|
||||
getAvailableJobs(): IJob[] {
|
||||
return JobRepository.Instance.getAvailableJobs();
|
||||
}
|
||||
|
||||
public async cleanUp() {
|
||||
this.stopSchedules();
|
||||
}
|
||||
|
||||
public stopSchedules(): void {
|
||||
Logger.silly(LOG_TAG, 'Stopping all schedules');
|
||||
this.timers.forEach((t): void => clearTimeout(t.timer));
|
||||
this.timers = [];
|
||||
}
|
||||
@ -138,7 +144,7 @@ export class JobManager implements IJobListener {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ import {AlbumCoverRestJob} from './jobs/AlbumCoverResetJob';
|
||||
|
||||
export class JobRepository {
|
||||
private static instance: JobRepository = null;
|
||||
availableJobs: { [key: string]: IJob<unknown> } = {};
|
||||
availableJobs: { [key: string]: IJob } = {};
|
||||
|
||||
public static get Instance(): JobRepository {
|
||||
if (JobRepository.instance == null) {
|
||||
@ -23,11 +23,11 @@ export class JobRepository {
|
||||
return JobRepository.instance;
|
||||
}
|
||||
|
||||
getAvailableJobs(): IJob<unknown>[] {
|
||||
getAvailableJobs(): IJob[] {
|
||||
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') {
|
||||
throw new Error('Job already exist:' + job.Name);
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import {ObjectManagers} from '../../ObjectManagers';
|
||||
import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
||||
import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
|
||||
import {Job} from './Job';
|
||||
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||
|
||||
export class AlbumCoverFillingJob extends Job {
|
||||
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;
|
||||
status: 'Persons' | 'Albums' | 'Directory' = 'Persons';
|
||||
|
||||
@ -20,7 +21,7 @@ export class AlbumCoverFillingJob extends Job {
|
||||
if (!this.directoryToSetCover) {
|
||||
this.Progress.log('Loading Directories to process');
|
||||
this.directoryToSetCover =
|
||||
await ObjectManagers.getInstance().CoverManager.getPartialDirsWithoutCovers();
|
||||
await ObjectManagers.getInstance().CoverManager.getPartialDirsWithoutCovers();
|
||||
this.Progress.Left = this.directoryToSetCover.length + 2;
|
||||
return true;
|
||||
}
|
||||
@ -57,7 +58,7 @@ export class AlbumCoverFillingJob extends Job {
|
||||
private async stepDirectoryCover(): Promise<boolean> {
|
||||
if (this.directoryToSetCover.length === 0) {
|
||||
this.directoryToSetCover =
|
||||
await ObjectManagers.getInstance().CoverManager.getPartialDirsWithoutCovers();
|
||||
await ObjectManagers.getInstance().CoverManager.getPartialDirsWithoutCovers();
|
||||
// double check if there is really no more
|
||||
if (this.directoryToSetCover.length > 0) {
|
||||
return true; // continue
|
||||
@ -70,7 +71,7 @@ export class AlbumCoverFillingJob extends Job {
|
||||
this.Progress.Left = this.directoryToSetCover.length;
|
||||
|
||||
await ObjectManagers.getInstance().CoverManager.setAndGetCoverForDirectory(
|
||||
directory
|
||||
directory
|
||||
);
|
||||
this.Progress.Processed++;
|
||||
return true;
|
||||
|
@ -1,10 +1,11 @@
|
||||
import {ObjectManagers} from '../../ObjectManagers';
|
||||
import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
||||
import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
|
||||
import {Job} from './Job';
|
||||
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||
|
||||
export class AlbumCoverRestJob extends Job {
|
||||
public readonly Name = DefaultsJobs[DefaultsJobs['Album Cover Reset']];
|
||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = null;
|
||||
public readonly ConfigTemplate: DynamicConfig[] = null;
|
||||
protected readonly IsInstant = true;
|
||||
|
||||
public get Supported(): boolean {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import {ObjectManagers} from '../../ObjectManagers';
|
||||
import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
||||
import {DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
||||
import {Job} from './Job';
|
||||
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||
|
||||
export class AlbumRestJob extends Job {
|
||||
public readonly Name = DefaultsJobs[DefaultsJobs['Album Reset']];
|
||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = null;
|
||||
public readonly ConfigTemplate: DynamicConfig[] = null;
|
||||
protected readonly IsInstant = true;
|
||||
|
||||
public get Supported(): boolean {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import {ConfigTemplateEntry} from '../../../../common/entities/job/JobDTO';
|
||||
import {Job} from './Job';
|
||||
import * as path from 'path';
|
||||
import {Logger} from '../../../Logger';
|
||||
@ -13,6 +12,7 @@ import {ProjectPath} from '../../../ProjectPath';
|
||||
import {FileEntity} from '../../database/enitites/FileEntity';
|
||||
import {DirectoryBaseDTO, DirectoryDTOUtils} from '../../../../common/entities/DirectoryDTO';
|
||||
import {DirectoryScanSettings, DiskManager} from '../../fileaccess/DiskManager';
|
||||
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||
|
||||
const LOG_TAG = '[FileJob]';
|
||||
|
||||
@ -20,7 +20,7 @@ const LOG_TAG = '[FileJob]';
|
||||
* Abstract class for thumbnail creation, file deleting etc.
|
||||
*/
|
||||
export abstract class FileJob<S extends { indexedOnly?: boolean } = { indexedOnly?: boolean }> extends Job<S> {
|
||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = [];
|
||||
public readonly ConfigTemplate: DynamicConfig[] = [];
|
||||
directoryQueue: string[] = [];
|
||||
fileQueue: string[] = [];
|
||||
DBProcessing = {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import {ObjectManagers} from '../../ObjectManagers';
|
||||
import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
||||
import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
|
||||
import {Job} from './Job';
|
||||
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||
|
||||
export class GalleryRestJob extends Job {
|
||||
public readonly Name = DefaultsJobs[DefaultsJobs['Gallery Reset']];
|
||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = null;
|
||||
public readonly ConfigTemplate: DynamicConfig[] = null;
|
||||
protected readonly IsInstant = true;
|
||||
|
||||
public get Supported(): boolean {
|
||||
|
@ -2,7 +2,7 @@ import {JobDTO} from '../../../../common/entities/job/JobDTO';
|
||||
import {JobProgress} from './JobProgress';
|
||||
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;
|
||||
Supported: boolean;
|
||||
Progress: JobProgress;
|
||||
|
@ -4,7 +4,7 @@ import {JobProgressStates} from '../../../../common/entities/job/JobProgressDTO'
|
||||
|
||||
export interface IJobListener {
|
||||
onJobFinished(
|
||||
job: IJob<unknown>,
|
||||
job: IJob,
|
||||
state: JobProgressStates,
|
||||
soloRun: boolean
|
||||
): void;
|
||||
|
@ -2,7 +2,7 @@ import {ObjectManagers} from '../../ObjectManagers';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
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 {ProjectPath} from '../../../ProjectPath';
|
||||
import {backendTexts} from '../../../../common/BackendTexts';
|
||||
@ -10,6 +10,7 @@ import {ParentDirectoryDTO} from '../../../../common/entities/DirectoryDTO';
|
||||
import {Logger} from '../../../Logger';
|
||||
import {FileDTO} from '../../../../common/entities/FileDTO';
|
||||
import {DiskManager} from '../../fileaccess/DiskManager';
|
||||
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||
|
||||
const LOG_TAG = '[IndexingJob]';
|
||||
|
||||
@ -18,7 +19,7 @@ export class IndexingJob<
|
||||
> extends Job<S> {
|
||||
public readonly Name = DefaultsJobs[DefaultsJobs.Indexing];
|
||||
directoriesToIndex: string[] = [];
|
||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = [
|
||||
public readonly ConfigTemplate: DynamicConfig[] = [
|
||||
{
|
||||
id: 'indexChangesOnly',
|
||||
type: 'boolean',
|
||||
|
@ -1,9 +1,10 @@
|
||||
import {Logger} from '../../../Logger';
|
||||
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 {IJobListener} from './IJobListener';
|
||||
import {JobProgressStates} from '../../../../common/entities/job/JobProgressDTO';
|
||||
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||
|
||||
declare const process: { nextTick: (_: unknown) => 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 ConfigTemplate(): ConfigTemplateEntry[];
|
||||
public abstract get ConfigTemplate(): DynamicConfig[];
|
||||
|
||||
public get Progress(): JobProgress {
|
||||
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 fs from 'fs';
|
||||
import {Job} from './Job';
|
||||
@ -6,10 +6,11 @@ import {ProjectPath} from '../../../ProjectPath';
|
||||
import {GPXProcessing} from '../../fileaccess/fileprocessing/GPXProcessing';
|
||||
import {PhotoProcessing} from '../../fileaccess/fileprocessing/PhotoProcessing';
|
||||
import {VideoProcessing} from '../../fileaccess/fileprocessing/VideoProcessing';
|
||||
import { DynamicConfig } from '../../../../common/entities/DynamicConfig';
|
||||
|
||||
export class TempFolderCleaningJob extends Job {
|
||||
public readonly Name = DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']];
|
||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = null;
|
||||
public readonly ConfigTemplate: DynamicConfig[] = null;
|
||||
public readonly Supported: boolean = true;
|
||||
directoryQueue: string[] = [];
|
||||
private tempRootCleaned = false;
|
||||
|
@ -35,7 +35,7 @@ export class ThumbnailGenerationJob extends FileJob<{
|
||||
): Promise<void> {
|
||||
if (!config || !config.sizes || !Array.isArray(config.sizes) || config.sizes.length === 0) {
|
||||
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) {
|
||||
if (Config.Media.Thumbnail.thumbnailSizes.indexOf(item) === -1) {
|
||||
|
@ -1,63 +1,67 @@
|
||||
import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
||||
import {DefaultMessengers, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
||||
import {Job} from './Job';
|
||||
import {backendTexts} from '../../../../common/BackendTexts';
|
||||
import {SortByTypes} from '../../../../common/entities/SortingMethods';
|
||||
import {DatePatternFrequency, DatePatternSearch, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO';
|
||||
import {ObjectManagers} from '../../ObjectManagers';
|
||||
import {PhotoEntity} from '../../database/enitites/PhotoEntity';
|
||||
import {EmailMediaMessenger} from '../../mediamessengers/EmailMediaMessenger';
|
||||
import {MediaPickDTO} from '../../../../common/entities/MediaPickDTO';
|
||||
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<{
|
||||
mediaPick: MediaPickDTO[],
|
||||
messenger: string,
|
||||
emailTo: string,
|
||||
emailFrom: string,
|
||||
emailSubject: string,
|
||||
emailText: string,
|
||||
}> {
|
||||
public readonly Name = DefaultsJobs[DefaultsJobs['Top Pick Sending']];
|
||||
public readonly Supported: boolean = true;
|
||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = [
|
||||
{
|
||||
id: 'mediaPick',
|
||||
type: 'MediaPickDTO-array',
|
||||
name: backendTexts.mediaPick.name,
|
||||
description: backendTexts.mediaPick.description,
|
||||
defaultValue: [{
|
||||
searchQuery: {
|
||||
type: SearchQueryTypes.date_pattern,
|
||||
daysLength: 7,
|
||||
frequency: DatePatternFrequency.every_year
|
||||
} as DatePatternSearch,
|
||||
sortBy: [{method: SortByTypes.Rating, ascending: false},
|
||||
{method: SortByTypes.PersonCount, ascending: false}],
|
||||
pick: 5
|
||||
}] as MediaPickDTO[],
|
||||
}, {
|
||||
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:',
|
||||
},
|
||||
];
|
||||
public readonly ConfigTemplate: DynamicConfig[];
|
||||
private status: 'Listing' | 'Sending' = 'Listing';
|
||||
private mediaList: PhotoEntity[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.ConfigTemplate = [
|
||||
{
|
||||
id: 'mediaPick',
|
||||
type: 'MediaPickDTO-array',
|
||||
name: backendTexts.mediaPick.name,
|
||||
description: backendTexts.mediaPick.description,
|
||||
defaultValue: [{
|
||||
searchQuery: {
|
||||
type: SearchQueryTypes.date_pattern,
|
||||
daysLength: 7,
|
||||
frequency: DatePatternFrequency.every_year
|
||||
} as DatePatternSearch,
|
||||
sortBy: [{method: SortByTypes.Rating, ascending: false},
|
||||
{method: SortByTypes.PersonCount, ascending: false}],
|
||||
pick: 5
|
||||
}] as MediaPickDTO[],
|
||||
}, {
|
||||
id: 'messenger',
|
||||
type: 'messenger',
|
||||
name: backendTexts.messenger.name,
|
||||
description: backendTexts.messenger.description,
|
||||
defaultValue: DefaultMessengers[DefaultMessengers.Email]
|
||||
}
|
||||
];
|
||||
|
||||
// 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> {
|
||||
this.status = 'Listing';
|
||||
@ -85,15 +89,15 @@ export class TopPickSendJob extends Job<{
|
||||
this.mediaList = [];
|
||||
for (let i = 0; i < this.config.mediaPick.length; ++i) {
|
||||
const media = await ObjectManagers.getInstance().SearchManager
|
||||
.getNMedia(this.config.mediaPick[i].searchQuery, this.config.mediaPick[i].sortBy, this.config.mediaPick[i].pick);
|
||||
.getNMedia(this.config.mediaPick[i].searchQuery, this.config.mediaPick[i].sortBy, this.config.mediaPick[i].pick);
|
||||
this.Progress.log('Find ' + media.length + ' photos and videos from ' + (i + 1) + '. load');
|
||||
this.mediaList = this.mediaList.concat(media);
|
||||
}
|
||||
|
||||
// make the list unique
|
||||
this.mediaList = this.mediaList
|
||||
.filter((value, index, arr) =>
|
||||
arr.findIndex(m => MediaDTOUtils.equals(m, value)) === index);
|
||||
.filter((value, index, arr) =>
|
||||
arr.findIndex(m => MediaDTOUtils.equals(m, value)) === index);
|
||||
|
||||
this.Progress.Processed++;
|
||||
// console.log(this.mediaList);
|
||||
@ -102,17 +106,16 @@ export class TopPickSendJob extends Job<{
|
||||
|
||||
private async stepSending(): Promise<boolean> {
|
||||
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++;
|
||||
return false;
|
||||
}
|
||||
this.Progress.log('Sending emails of ' + this.mediaList.length + ' photos.');
|
||||
const messenger = new EmailMediaMessenger();
|
||||
await messenger.sendMedia({
|
||||
to: this.config.emailTo,
|
||||
subject: this.config.emailSubject,
|
||||
text: this.config.emailText
|
||||
}, this.mediaList);
|
||||
const msgr = MessengerRepository.Instance.get(this.config.messenger);
|
||||
if (!msgr) {
|
||||
throw new Error('Can\t find "' + this.config.messenger + '" messenger.');
|
||||
}
|
||||
this.Progress.log('Sending ' + this.mediaList.length + ' photos.');
|
||||
await msgr.send(this.config, this.mediaList);
|
||||
this.Progress.Processed++;
|
||||
return false;
|
||||
}
|
||||
|
@ -1,18 +1,40 @@
|
||||
import {createTransport, Transporter} from 'nodemailer';
|
||||
import {MediaDTO, MediaDTOUtils} from '../../../common/entities/MediaDTO';
|
||||
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 {Utils} from '../../../common/Utils';
|
||||
import {QueryParams} from '../../../common/QueryParams';
|
||||
import {MediaDTOWithThPath, Messenger} from './Messenger';
|
||||
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;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.transporter = createTransport({
|
||||
host: Config.Messaging.Email.smtp.host,
|
||||
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: {
|
||||
to: string,
|
||||
subject: string,
|
||||
text: string
|
||||
}, media: MediaDTO[]) {
|
||||
protected async sendMedia(mailSettings: {
|
||||
emailTo: string,
|
||||
emailSubject: string,
|
||||
emailText: string
|
||||
}, media: MediaDTOWithThPath[]) {
|
||||
|
||||
const attachments = [];
|
||||
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' +
|
||||
' <tbody>\n';
|
||||
const htmlEnd = ' </tr>\n' +
|
||||
@ -51,9 +65,6 @@ export class EmailMediaMessenger {
|
||||
let htmlMiddle = '';
|
||||
const numberOfColumns = media.length >= 6 ? 3 : 2;
|
||||
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 ?
|
||||
(media[i].metadata as PhotoMetadata).positionData?.country :
|
||||
((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 : '');
|
||||
attachments.push({
|
||||
filename: media[i].name,
|
||||
path: thPath,
|
||||
path: media[i].thumbnailPath,
|
||||
cid: 'img' + i
|
||||
});
|
||||
if (i % numberOfColumns == 0) {
|
||||
htmlMiddle += '<tr>';
|
||||
}
|
||||
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 +
|
||||
' </td>\n';
|
||||
|
||||
@ -79,8 +90,8 @@ export class EmailMediaMessenger {
|
||||
|
||||
return await this.transporter.sendMail({
|
||||
from: Config.Messaging.Email.emailFrom,
|
||||
to: mailSettings.to,
|
||||
subject: mailSettings.subject,
|
||||
to: mailSettings.emailTo,
|
||||
subject: mailSettings.emailSubject,
|
||||
html: htmlStart + htmlMiddle + htmlEnd,
|
||||
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.addGetDuplicates(app);
|
||||
this.addJobs(app);
|
||||
this.addMessengers(app);
|
||||
}
|
||||
|
||||
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 {
|
||||
app.get(
|
||||
Config.Server.apiPath + '/admin/jobs/available',
|
||||
|
@ -32,9 +32,18 @@ const LOG_TAG = '[server]';
|
||||
|
||||
export class Server {
|
||||
public onStarted = new Event<void>();
|
||||
private app: express.Express;
|
||||
public app: express.Express;
|
||||
private server: HttpServer;
|
||||
|
||||
static instance: Server = null;
|
||||
|
||||
public static getInstance(): Server {
|
||||
if (!this.instance) {
|
||||
this.instance = new Server();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
if (!(process.env.NODE_ENV === 'production')) {
|
||||
Logger.info(
|
||||
@ -45,11 +54,16 @@ export class Server {
|
||||
this.init().catch(console.error);
|
||||
}
|
||||
|
||||
get App(): any {
|
||||
get Server(): HttpServer {
|
||||
return this.server;
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
|
||||
this.app = express();
|
||||
LoggerRouter.route(this.app);
|
||||
this.app.set('view engine', 'ejs');
|
||||
|
||||
Logger.info(LOG_TAG, 'running diagnostics...');
|
||||
await ConfigDiagnostics.runDiagnostics();
|
||||
Logger.verbose(
|
||||
@ -61,13 +75,14 @@ export class Server {
|
||||
).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
|
||||
@ -115,7 +130,7 @@ export class Server {
|
||||
Localizations.init();
|
||||
|
||||
this.app.use(locale(Config.Server.languages, 'en'));
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
await ObjectManagers.getInstance().init();
|
||||
|
||||
Router.route(this.app);
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
export type backendText = number;
|
||||
// keep the numbering sparse to support later addition
|
||||
export const backendTexts = {
|
||||
indexedFilesOnly: {name: 10, description: 12},
|
||||
sizeToGenerate: {name: 20, description: 22},
|
||||
@ -6,6 +7,7 @@ export const backendTexts = {
|
||||
mediaPick: {name: 40, description: 42},
|
||||
emailTo: {name: 70, description: 72},
|
||||
emailSubject: {name: 90, description: 92},
|
||||
emailText: {name: 100, description: 102}
|
||||
emailText: {name: 100, description: 102},
|
||||
messenger: {name: 110,description: 112}
|
||||
|
||||
};
|
||||
|
@ -13,7 +13,7 @@ const upTime = new Date().toISOString();
|
||||
// TODO: Refactor Config to be injectable globally.
|
||||
// This is a bad habit to let the Config know if its in a testing env.
|
||||
const isTesting = process.env['NODE_ENV'] == true || ['afterEach', 'after', 'beforeEach', 'before', 'describe', 'it']
|
||||
.every((fn) => (global as any)[fn] instanceof Function);
|
||||
.every((fn) => (global as any)[fn] instanceof Function);
|
||||
|
||||
@ConfigClass<IConfigClass<TAGS> & ServerConfig>({
|
||||
configPath: path.join(__dirname, !isTesting ? './../../../../config.json' : './../../../../test/backend/tmp/config.json'),
|
||||
@ -76,30 +76,20 @@ export class PrivateConfigClass extends ServerConfig {
|
||||
}
|
||||
|
||||
this.Environment.appVersion =
|
||||
require('../../../../package.json').version;
|
||||
require('../../../../package.json').version;
|
||||
this.Environment.buildTime =
|
||||
require('../../../../package.json').buildTime;
|
||||
require('../../../../package.json').buildTime;
|
||||
this.Environment.buildCommitHash =
|
||||
require('../../../../package.json').buildCommitHash;
|
||||
require('../../../../package.json').buildCommitHash;
|
||||
this.Environment.upTime = upTime;
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const Config = ConfigClassBuilder.attachInterface(
|
||||
new PrivateConfigClass()
|
||||
new PrivateConfigClass()
|
||||
);
|
||||
try {
|
||||
Config.loadSync();
|
||||
|
@ -1,7 +1,6 @@
|
||||
/* 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 {ConfigProperty} from '../../../../node_modules/typeconfig/src/decorators/property/ConfigPropoerty';
|
||||
|
||||
declare let $localize: (s: TemplateStringsArray) => string;
|
||||
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
} from '../../entities/job/JobScheduleDTO';
|
||||
import {
|
||||
ClientConfig,
|
||||
ClientExtensionsConfig,
|
||||
ClientGPXCompressingConfig,
|
||||
ClientMediaConfig,
|
||||
ClientMetaFileConfig,
|
||||
@ -25,8 +26,7 @@ import {
|
||||
ConfigPriority,
|
||||
TAGS
|
||||
} from '../public/ClientConfig';
|
||||
import {SubConfigClass} from 'typeconfig/src/decorators/class/SubConfigClass';
|
||||
import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty';
|
||||
import {ConfigProperty, SubConfigClass} from 'typeconfig/common';
|
||||
import {DefaultsJobs} from '../../entities/job/JobDTO';
|
||||
import {SearchQueryDTO, SearchQueryTypes, TextSearch,} from '../../entities/SearchQueryDTO';
|
||||
import {SortByTypes} from '../../entities/SortingMethods';
|
||||
@ -1013,6 +1013,25 @@ export class ServerServiceConfig extends ClientServiceConfig {
|
||||
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})
|
||||
export class ServerEnvironmentConfig {
|
||||
@ConfigProperty({volatile: true})
|
||||
@ -1133,6 +1152,15 @@ export class ServerConfig extends ClientConfig {
|
||||
})
|
||||
Messaging: MessagingConfig = new MessagingConfig();
|
||||
|
||||
|
||||
@ConfigProperty({
|
||||
tags: {
|
||||
name: $localize`Extensions`,
|
||||
uiIcon: 'ionCloudOutline'
|
||||
} as TAGS,
|
||||
})
|
||||
Extensions: ServerExtensionsConfig = new ServerExtensionsConfig();
|
||||
|
||||
@ConfigProperty({
|
||||
tags: {
|
||||
name: $localize`Jobs`,
|
||||
|
@ -1424,6 +1424,16 @@ export class ClientUserConfig {
|
||||
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})
|
||||
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.`
|
||||
})
|
||||
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';
|
||||
|
||||
export type fieldType = 'string' | 'string-array' | 'number' | 'boolean' | 'number-array' | 'MediaPickDTO-array';
|
||||
import {DynamicConfig} from '../DynamicConfig';
|
||||
|
||||
export enum DefaultsJobs {
|
||||
Indexing = 1,
|
||||
@ -17,17 +15,16 @@ export enum DefaultsJobs {
|
||||
'Top Pick Sending' = 12
|
||||
}
|
||||
|
||||
export interface ConfigTemplateEntry {
|
||||
id: string;
|
||||
name: backendText;
|
||||
description: backendText;
|
||||
type: fieldType;
|
||||
defaultValue: any;
|
||||
|
||||
export enum DefaultMessengers {
|
||||
Email = 1,
|
||||
Stdout = 2
|
||||
}
|
||||
|
||||
|
||||
export interface JobDTO {
|
||||
Name: string;
|
||||
ConfigTemplate: ConfigTemplateEntry[];
|
||||
ConfigTemplate: DynamicConfig[];
|
||||
}
|
||||
|
||||
export const JobDTOUtils = {
|
||||
|
@ -5,7 +5,10 @@ import {DefaultsJobs} from '../../../common/entities/job/JobDTO';
|
||||
@Injectable()
|
||||
export class BackendtextService {
|
||||
|
||||
public get(id: backendText): string {
|
||||
public get(id: backendText | string): string {
|
||||
if (typeof id === 'string') {
|
||||
return id;
|
||||
}
|
||||
switch (id) {
|
||||
case backendTexts.sizeToGenerate.name:
|
||||
return $localize`Size to generate`;
|
||||
@ -35,6 +38,10 @@ export class BackendtextService {
|
||||
return $localize`Message`;
|
||||
case backendTexts.emailText.description:
|
||||
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:
|
||||
return null;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import {ConfigStyle, SettingsService} from '../settings/settings.service';
|
||||
import {ConfigPriority} from '../../../../common/config/public/ClientConfig';
|
||||
import {WebConfig} from '../../../../common/config/private/WebConfig';
|
||||
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 {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 {OnChanges} from '../../../../../../node_modules/@angular/core';
|
||||
import {map, Observable} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
|
@ -4,7 +4,7 @@ import {AutoCompleteService} from '../autocomplete.service';
|
||||
import {SearchQueryDTO} from '../../../../../../common/entities/SearchQueryDTO';
|
||||
import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator,} from '@angular/forms';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
|
@ -3,9 +3,10 @@ import {BehaviorSubject} from 'rxjs';
|
||||
import {JobProgressDTO, JobProgressStates, OnTimerJobProgressDTO,} from '../../../../common/entities/job/JobProgressDTO';
|
||||
import {NetworkService} from '../../model/network/network.service';
|
||||
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 {NotificationService} from '../../model/notification.service';
|
||||
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
|
||||
|
||||
@Injectable()
|
||||
export class ScheduledJobsService {
|
||||
@ -13,6 +14,7 @@ export class ScheduledJobsService {
|
||||
public onJobFinish: EventEmitter<string> = new EventEmitter<string>();
|
||||
timer: number = null;
|
||||
public availableJobs: BehaviorSubject<JobDTO[]>;
|
||||
public availableMessengers: BehaviorSubject<string[]>;
|
||||
public jobStartingStopping: { [key: string]: boolean } = {};
|
||||
private subscribers = 0;
|
||||
|
||||
@ -23,6 +25,7 @@ export class ScheduledJobsService {
|
||||
) {
|
||||
this.progress = 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(
|
||||
(t) => t.Name === JobName
|
||||
);
|
||||
|
@ -10,7 +10,7 @@ import {CookieService} from 'ngx-cookie-service';
|
||||
import {DefaultsJobs} from '../../../../common/entities/job/JobDTO';
|
||||
import {StatisticDTO} from '../../../../common/entities/settings/StatisticDTO';
|
||||
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 {
|
||||
|
@ -16,7 +16,7 @@ import {ConfigStyle, SettingsService} from '../../settings.service';
|
||||
import {WebConfig} from '../../../../../../common/config/private/WebConfig';
|
||||
import {JobScheduleConfig, UserConfig} from '../../../../../../common/config/private/PrivateConfig';
|
||||
import {enumToTranslatedArray} from '../../../EnumTranslations';
|
||||
import {BsModalService} from '../../../../../../../node_modules/ngx-bootstrap/modal';
|
||||
import {BsModalService} from 'ngx-bootstrap/modal';
|
||||
import {CustomSettingsEntries} from '../CustomSettingsEntries';
|
||||
import {GroupByTypes, SortByTypes} from '../../../../../../common/entities/SortingMethods';
|
||||
|
||||
@ -59,7 +59,7 @@ interface IState {
|
||||
],
|
||||
})
|
||||
export class SettingsEntryComponent
|
||||
implements ControlValueAccessor, Validator, OnChanges {
|
||||
implements ControlValueAccessor, Validator, OnChanges {
|
||||
name: string;
|
||||
required: boolean;
|
||||
dockerWarning: boolean;
|
||||
@ -79,7 +79,10 @@ export class SettingsEntryComponent
|
||||
public arrayType: string;
|
||||
public uiType: string;
|
||||
newThemeModalRef: any;
|
||||
iconModal: { ref?: any, error?: string };
|
||||
iconModal: {
|
||||
ref?: any,
|
||||
error?: string
|
||||
};
|
||||
@Input() noChangeDetection = false;
|
||||
public readonly ConfigStyle = ConfigStyle;
|
||||
protected readonly SortByTypes = SortByTypes;
|
||||
@ -101,9 +104,9 @@ export class SettingsEntryComponent
|
||||
for (let i = 0; i < this.state.value?.length; ++i) {
|
||||
for (const k of Object.keys(this.state.value[i].__state)) {
|
||||
if (!Utils.equalsFilter(
|
||||
this.state.value[i]?.__state[k]?.value,
|
||||
this.state.default[i] ? this.state.default[i][k] : undefined,
|
||||
['default', '__propPath', '__created', '__prototype', '__rootConfig'])) {
|
||||
this.state.value[i]?.__state[k]?.value,
|
||||
this.state.default[i] ? this.state.default[i][k] : undefined,
|
||||
['default', '__propPath', '__created', '__prototype', '__rootConfig'])) {
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -129,7 +132,7 @@ export class SettingsEntryComponent
|
||||
get defaultStr(): string {
|
||||
if (this.type === 'SearchQuery') {
|
||||
return (
|
||||
'\'' + this.searchQueryParserService.stringify(this.state.default) + '\''
|
||||
'\'' + this.searchQueryParserService.stringify(this.state.default) + '\''
|
||||
);
|
||||
}
|
||||
|
||||
@ -143,8 +146,8 @@ export class SettingsEntryComponent
|
||||
|
||||
get StringValue(): string {
|
||||
if (
|
||||
this.state.type === 'array' &&
|
||||
(this.state.arrayType === 'string' || this.isNumberArray)
|
||||
this.state.type === 'array' &&
|
||||
(this.state.arrayType === 'string' || this.isNumberArray)
|
||||
) {
|
||||
return (this.state.value || []).join(';');
|
||||
}
|
||||
@ -162,8 +165,8 @@ export class SettingsEntryComponent
|
||||
|
||||
set StringValue(value: string) {
|
||||
if (
|
||||
this.state.type === 'array' &&
|
||||
(this.state.arrayType === 'string' || this.isNumberArray)
|
||||
this.state.type === 'array' &&
|
||||
(this.state.arrayType === 'string' || this.isNumberArray)
|
||||
) {
|
||||
value = value.replace(new RegExp(',', 'g'), ';');
|
||||
if (!this.allowSpaces) {
|
||||
@ -172,14 +175,14 @@ export class SettingsEntryComponent
|
||||
this.state.value = value.split(';').filter((v: string) => v !== '');
|
||||
if (this.isNumberArray) {
|
||||
this.state.value = this.state.value
|
||||
.map((v: string) => parseFloat(v))
|
||||
.filter((v: number) => !isNaN(v));
|
||||
.map((v: string) => parseFloat(v))
|
||||
.filter((v: number) => !isNaN(v));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this.state.value === 'object') {
|
||||
this.state.value = JSON.parse(value);
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.value = value;
|
||||
@ -194,11 +197,13 @@ export class SettingsEntryComponent
|
||||
key: 'default',
|
||||
value: $localize`default`
|
||||
}, ...(this.state.rootConfig as any).__state.availableThemes.value
|
||||
.map((th: ThemeConfig) => ({key: th.name, value: th.name}))];
|
||||
.map((th: ThemeConfig) => ({key: th.name, value: th.name}))];
|
||||
}
|
||||
|
||||
|
||||
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'};
|
||||
}
|
||||
|
||||
@ -241,16 +246,16 @@ export class SettingsEntryComponent
|
||||
this.uiType = CustomSettingsEntries.getFullName(this.state);
|
||||
}
|
||||
if (!this.state.isEnumType &&
|
||||
!this.state.isEnumArrayType &&
|
||||
this.type !== 'boolean' &&
|
||||
this.type !== 'SearchQuery' &&
|
||||
!CustomSettingsEntries.iS(this.state) &&
|
||||
this.arrayType !== 'MapLayers' &&
|
||||
this.arrayType !== 'NavigationLinkConfig' &&
|
||||
this.arrayType !== 'MapPathGroupConfig' &&
|
||||
this.arrayType !== 'MapPathGroupThemeConfig' &&
|
||||
this.arrayType !== 'JobScheduleConfig' &&
|
||||
this.arrayType !== 'UserConfig') {
|
||||
!this.state.isEnumArrayType &&
|
||||
this.type !== 'boolean' &&
|
||||
this.type !== 'SearchQuery' &&
|
||||
!CustomSettingsEntries.iS(this.state) &&
|
||||
this.arrayType !== 'MapLayers' &&
|
||||
this.arrayType !== 'NavigationLinkConfig' &&
|
||||
this.arrayType !== 'MapPathGroupConfig' &&
|
||||
this.arrayType !== 'MapPathGroupThemeConfig' &&
|
||||
this.arrayType !== 'JobScheduleConfig' &&
|
||||
this.arrayType !== 'UserConfig') {
|
||||
this.uiType = 'StringInput';
|
||||
}
|
||||
if (this.type === this.state.tags?.uiType) {
|
||||
@ -273,18 +278,18 @@ export class SettingsEntryComponent
|
||||
this.name = this.state?.tags?.name;
|
||||
if (this.name) {
|
||||
this.idName =
|
||||
this.GUID + this.name.toLowerCase().replace(new RegExp(' ', 'gm'), '-');
|
||||
this.GUID + this.name.toLowerCase().replace(new RegExp(' ', 'gm'), '-');
|
||||
}
|
||||
this.isNumberArray =
|
||||
this.state.arrayType === 'unsignedInt' ||
|
||||
this.state.arrayType === 'integer' ||
|
||||
this.state.arrayType === 'float' ||
|
||||
this.state.arrayType === 'positiveFloat';
|
||||
this.state.arrayType === 'unsignedInt' ||
|
||||
this.state.arrayType === 'integer' ||
|
||||
this.state.arrayType === 'float' ||
|
||||
this.state.arrayType === 'positiveFloat';
|
||||
this.isNumber =
|
||||
this.state.type === 'unsignedInt' ||
|
||||
this.state.type === 'integer' ||
|
||||
this.state.type === 'float' ||
|
||||
this.state.type === 'positiveFloat';
|
||||
this.state.type === 'unsignedInt' ||
|
||||
this.state.type === 'integer' ||
|
||||
this.state.type === 'float' ||
|
||||
this.state.type === 'positiveFloat';
|
||||
|
||||
|
||||
if (this.isNumber) {
|
||||
@ -306,11 +311,16 @@ 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) {
|
||||
const eClass = state.isEnumType
|
||||
? state.type
|
||||
: state.arrayType;
|
||||
? state.type
|
||||
: state.arrayType;
|
||||
if (state.tags?.uiOptions) {
|
||||
state.optionsView = state.tags?.uiOptions.map(o => ({
|
||||
key: o,
|
||||
@ -325,11 +335,11 @@ export class SettingsEntryComponent
|
||||
|
||||
validate(): ValidationErrors {
|
||||
if (
|
||||
!this.required ||
|
||||
(this.state &&
|
||||
typeof this.state.value !== 'undefined' &&
|
||||
this.state.value !== null &&
|
||||
this.state.value !== '')
|
||||
!this.required ||
|
||||
(this.state &&
|
||||
typeof this.state.value !== 'undefined' &&
|
||||
this.state.value !== null &&
|
||||
this.state.value !== '')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@ -386,8 +396,8 @@ export class SettingsEntryComponent
|
||||
|
||||
removeLayer(layer: MapLayers): void {
|
||||
this.state.value.splice(
|
||||
this.state.value.indexOf(layer),
|
||||
1
|
||||
this.state.value.indexOf(layer),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
@ -395,7 +405,7 @@ export class SettingsEntryComponent
|
||||
addNewTheme(): void {
|
||||
const availableThemes = (this.state.rootConfig as any).__state.availableThemes;
|
||||
if (!this.newThemeName ||
|
||||
(availableThemes.value as ThemeConfig[]).find(th => th.name === this.newThemeName)) {
|
||||
(availableThemes.value as ThemeConfig[]).find(th => th.name === this.newThemeName)) {
|
||||
return;
|
||||
}
|
||||
this.state.value = this.newThemeName;
|
||||
|
@ -5,12 +5,12 @@ import {
|
||||
NG_VALUE_ACCESSOR,
|
||||
ValidationErrors,
|
||||
Validator
|
||||
} from '../../../../../../../../node_modules/@angular/forms';
|
||||
} from '@angular/forms';
|
||||
import {SortByDirectionalTypes, SortingMethod} from '../../../../../../../common/entities/SortingMethods';
|
||||
import {enumToTranslatedArray} from '../../../../EnumTranslations';
|
||||
import {AutoCompleteService} from '../../../../gallery/search/autocomplete.service';
|
||||
import {RouterLink} from '../../../../../../../../node_modules/@angular/router';
|
||||
import {forwardRef} from '../../../../../../../../node_modules/@angular/core';
|
||||
import {RouterLink} from '@angular/router';
|
||||
import {forwardRef} from '@angular/core';
|
||||
import {Utils} from '../../../../../../../common/Utils';
|
||||
|
||||
@Component({
|
||||
|
@ -9,11 +9,11 @@ import {JobDTOUtils} from '../../../../../common/entities/job/JobDTO';
|
||||
import {ScheduledJobsService} from '../scheduled-jobs.service';
|
||||
import {UntypedFormControl} from '@angular/forms';
|
||||
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 {Utils} from '../../../../../common/Utils';
|
||||
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 {ISettingsComponent} from './ISettingsComponent';
|
||||
import {CustomSettingsEntries} from './CustomSettingsEntries';
|
||||
|
@ -170,6 +170,7 @@
|
||||
<div *ngFor="let configEntry of jobsService.getConfigTemplate(schedule.jobName)">
|
||||
|
||||
<div class="mb-1 row"
|
||||
*ngIf="!configEntry.validIf || schedule.config[configEntry.validIf.configFiled] == configEntry.validIf.equalsValue"
|
||||
[class.mb-3]="settingsService.configStyle == ConfigStyle.full">
|
||||
<label class="col-md-2 control-label"
|
||||
[for]="configEntry.id+'_'+i">{{backendTextService.get(configEntry.name)}}</label>
|
||||
@ -227,6 +228,18 @@
|
||||
placeholder="Search Query">
|
||||
</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 *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 {
|
||||
this.jobsService.subscribeToProgress();
|
||||
this.jobsService.getAvailableJobs().catch(console.error);
|
||||
this.jobsService.getAvailableMessengers().catch(console.error);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@ -128,7 +129,7 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni
|
||||
schedule.config = schedule.config || {};
|
||||
if (job.ConfigTemplate) {
|
||||
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 || {};
|
||||
if (job.ConfigTemplate) {
|
||||
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();
|
||||
|
@ -2,7 +2,7 @@ import {Config} from '../../src/common/config/private/Config';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
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 {DirectoryBaseDTO, ParentDirectoryDTO, SubDirectoryDTO} from '../../src/common/entities/DirectoryDTO';
|
||||
import {ObjectManagers} from '../../src/backend/model/ObjectManagers';
|
||||
@ -111,7 +111,7 @@ export class DBTestHelper {
|
||||
}
|
||||
|
||||
public static async persistTestDir(directory: DirectoryBaseDTO): Promise<ParentDirectoryDTO> {
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
await ObjectManagers.getInstance().init();
|
||||
const connection = await SQLConnection.getConnection();
|
||||
ObjectManagers.getInstance().IndexingManager.indexDirectory = () => Promise.resolve(null);
|
||||
|
||||
@ -122,20 +122,20 @@ export class DBTestHelper {
|
||||
// await im.saveToDB(subDir2);
|
||||
|
||||
if (ObjectManagers.getInstance().IndexingManager &&
|
||||
ObjectManagers.getInstance().IndexingManager.IsSavingInProgress) {
|
||||
ObjectManagers.getInstance().IndexingManager.IsSavingInProgress) {
|
||||
await ObjectManagers.getInstance().IndexingManager.SavingReady;
|
||||
}
|
||||
|
||||
const gm = new GalleryManagerTest();
|
||||
|
||||
const dir = await gm.getParentDirFromId(connection,
|
||||
(await gm.getDirIdAndTime(connection, directory.name, path.join(directory.path, path.sep))).id);
|
||||
(await gm.getDirIdAndTime(connection, directory.name, path.join(directory.path, path.sep))).id);
|
||||
|
||||
const populateDir = async (d: DirectoryBaseDTO) => {
|
||||
for (let i = 0; i < d.directories.length; i++) {
|
||||
d.directories[i] = await gm.getParentDirFromId(connection,
|
||||
(await gm.getDirIdAndTime(connection, d.directories[i].name,
|
||||
path.join(DiskManager.pathFromParent(d), path.sep))).id);
|
||||
(await gm.getDirIdAndTime(connection, d.directories[i].name,
|
||||
path.join(DiskManager.pathFromParent(d), path.sep))).id);
|
||||
await populateDir(d.directories[i]);
|
||||
}
|
||||
};
|
||||
@ -147,6 +147,7 @@ export class DBTestHelper {
|
||||
|
||||
public async initDB(): Promise<void> {
|
||||
await Config.load();
|
||||
Config.Extensions.enabled = false; // make all tests clean
|
||||
if (this.dbType === DatabaseType.sqlite) {
|
||||
await this.initSQLite();
|
||||
} else if (this.dbType === DatabaseType.mysql) {
|
||||
@ -197,7 +198,7 @@ export class DBTestHelper {
|
||||
const conn = await SQLConnection.getConnection();
|
||||
await conn.query('CREATE DATABASE IF NOT EXISTS ' + conn.options.database);
|
||||
await SQLConnection.close();
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
await ObjectManagers.getInstance().init();
|
||||
}
|
||||
|
||||
private async clearUpMysql(): Promise<void> {
|
||||
@ -218,7 +219,7 @@ export class DBTestHelper {
|
||||
private async resetSQLite(): Promise<void> {
|
||||
Logger.debug(LOG_TAG, 'resetting sqlite');
|
||||
await this.clearUpSQLite();
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
await ObjectManagers.getInstance().init();
|
||||
}
|
||||
|
||||
private async clearUpSQLite(): Promise<void> {
|
||||
|
@ -48,7 +48,7 @@ describe('GalleryRouter', (sqlHelper: DBTestHelper) => {
|
||||
afterEach(tearDown);
|
||||
|
||||
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/');
|
||||
|
||||
(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 () => {
|
||||
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');
|
||||
|
||||
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');
|
||||
|
||||
(result.should as any).have.status(200);
|
||||
@ -80,7 +80,7 @@ describe('GalleryRouter', (sqlHelper: DBTestHelper) => {
|
||||
afterEach(tearDown);
|
||||
|
||||
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');
|
||||
|
||||
(result.should as any).have.status(200);
|
||||
|
@ -41,7 +41,7 @@ describe('PublicRouter', () => {
|
||||
server = new Server();
|
||||
await server.onStarted.wait();
|
||||
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
await ObjectManagers.getInstance().init();
|
||||
await ObjectManagers.getInstance().UserManager.createUser(Utils.clone(testUser));
|
||||
await SQLConnection.close();
|
||||
};
|
||||
@ -71,7 +71,7 @@ describe('PublicRouter', () => {
|
||||
afterEach(tearDown);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
|
@ -42,7 +42,7 @@ describe('SharingRouter', () => {
|
||||
server = new Server();
|
||||
await server.onStarted.wait();
|
||||
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
await ObjectManagers.getInstance().init();
|
||||
await ObjectManagers.getInstance().UserManager.createUser(Utils.clone(testUser));
|
||||
await SQLConnection.close();
|
||||
};
|
||||
@ -62,7 +62,7 @@ describe('SharingRouter', () => {
|
||||
};
|
||||
|
||||
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)
|
||||
.send({password});
|
||||
|
||||
|
@ -42,7 +42,7 @@ describe('UserRouter', () => {
|
||||
|
||||
server = new Server();
|
||||
await server.onStarted.wait();
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
await ObjectManagers.getInstance().init();
|
||||
await ObjectManagers.getInstance().UserManager.createUser(Utils.clone(testUser));
|
||||
await SQLConnection.close();
|
||||
};
|
||||
@ -62,7 +62,7 @@ describe('UserRouter', () => {
|
||||
};
|
||||
|
||||
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')
|
||||
.send({
|
||||
loginCredential: {
|
||||
@ -87,7 +87,7 @@ describe('UserRouter', () => {
|
||||
});
|
||||
it('it skip login', async () => {
|
||||
Config.Users.authenticationRequired = false;
|
||||
const result = await chai.request(server.App)
|
||||
const result = await chai.request(server.Server)
|
||||
.post(Config.Server.apiPath + '/user/login');
|
||||
|
||||
result.res.should.have.status(404);
|
||||
@ -105,7 +105,7 @@ describe('UserRouter', () => {
|
||||
|
||||
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')
|
||||
.set('Cookie', loginRes.res.headers['set-cookie'])
|
||||
.set('CSRF-Token', loginRes.body.result.csrfToken);
|
||||
@ -116,7 +116,7 @@ describe('UserRouter', () => {
|
||||
it('it should not authenticate', async () => {
|
||||
Config.Users.authenticationRequired = true;
|
||||
|
||||
const result = await chai.request(server.App)
|
||||
const result = await chai.request(server.Server)
|
||||
.get(Config.Server.apiPath + '/user/me');
|
||||
|
||||
result.res.should.have.status(401);
|
||||
@ -133,7 +133,7 @@ describe('UserRouter', () => {
|
||||
const loginRes = await login(server);
|
||||
const q: Record<string, string> = {};
|
||||
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)
|
||||
.set('Cookie', loginRes.res.headers['set-cookie'])
|
||||
.set('CSRF-Token', loginRes.body.result.csrfToken);
|
||||
@ -152,7 +152,7 @@ describe('UserRouter', () => {
|
||||
|
||||
const q: Record<string, string> = {};
|
||||
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);
|
||||
|
||||
checkUserResult(result, RouteTestingHelper.getExpectedSharingUser(sharing));
|
||||
@ -167,7 +167,7 @@ describe('UserRouter', () => {
|
||||
|
||||
const q: Record<string, string> = {};
|
||||
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);
|
||||
|
||||
result.should.have.status(401);
|
||||
@ -179,7 +179,7 @@ describe('UserRouter', () => {
|
||||
it('it should authenticate as guest', async () => {
|
||||
Config.Users.authenticationRequired = false;
|
||||
|
||||
const result = await chai.request(server.App)
|
||||
const result = await chai.request(server.Server)
|
||||
.get(Config.Server.apiPath + '/user/me');
|
||||
|
||||
const expectedGuestUser = {
|
||||
|
@ -7,6 +7,7 @@ import {ProjectPath} from '../../../../../src/backend/ProjectPath';
|
||||
import {TAGS} from '../../../../../src/common/config/public/ClientConfig';
|
||||
import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers';
|
||||
import {UserRoles} from '../../../../../src/common/entities/UserDTO';
|
||||
import {ExtensionConfigWrapper} from '../../../../../src/backend/model/extension/ExtensionConfigWrapper';
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
const chai: any = require('chai');
|
||||
@ -34,10 +35,10 @@ describe('SettingsRouter', () => {
|
||||
it('it should GET the settings', async () => {
|
||||
Config.Users.authenticationRequired = false;
|
||||
Config.Users.unAuthenticatedUserRole = UserRoles.Admin;
|
||||
const originalSettings = await Config.original();
|
||||
const originalSettings = await ExtensionConfigWrapper.original();
|
||||
const srv = new Server();
|
||||
await srv.onStarted.wait();
|
||||
const result = await chai.request(srv.App)
|
||||
const result = await chai.request(srv.Server)
|
||||
.get(Config.Server.apiPath + '/settings');
|
||||
|
||||
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 {Config} from '../../../../../src/common/config/private/Config';
|
||||
import {UserRoles} from '../../../../../src/common/entities/UserDTO';
|
||||
import {ConfigClassBuilder} from '../../../../../node_modules/typeconfig/node';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {ExtensionConfigWrapper} from '../../../../../src/backend/model/extension/ExtensionConfigWrapper';
|
||||
import {ConfigClassBuilder} from 'typeconfig/node';
|
||||
|
||||
|
||||
declare const describe: any;
|
||||
@ -21,7 +22,7 @@ describe('Settings middleware', () => {
|
||||
beforeEach(async () => {
|
||||
await ObjectManagers.reset();
|
||||
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) => {
|
||||
@ -74,7 +75,7 @@ describe('Settings middleware', () => {
|
||||
expect(Config.Users.enforcedUsers.length).to.be.equal(1);
|
||||
expect(Config.Users.enforcedUsers[0].name).to.be.equal('Apple');
|
||||
expect(Config.Users.enforcedUsers.length).to.be.equal(1);
|
||||
Config.original().then((cfg) => {
|
||||
ExtensionConfigWrapper.original().then((cfg) => {
|
||||
try {
|
||||
expect(cfg.Users.enforcedUsers.length).to.be.equal(1);
|
||||
expect(cfg.Users.enforcedUsers[0].name).to.be.equal('Apple');
|
||||
|
@ -33,7 +33,7 @@ describe('AlbumManager', (sqlHelper: DBTestHelper) => {
|
||||
const setUpSqlDB = async () => {
|
||||
await sqlHelper.initDB();
|
||||
await sqlHelper.setUpTestGallery();
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
await ObjectManagers.getInstance().init();
|
||||
};
|
||||
|
||||
|
||||
|
@ -122,7 +122,7 @@ describe('CoverManager', (sqlHelper: DBTestHelper) => {
|
||||
const setUpSqlDB = async () => {
|
||||
await sqlHelper.initDB();
|
||||
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 () => {
|
||||
const pm = new CoverManager();
|
||||
Config.AlbumCover.SearchQuery = null;
|
||||
expect(Utils.clone(await pm.getAlbumCover({
|
||||
expect(Utils.clone(await pm.getCoverForAlbum({
|
||||
searchQuery: {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: 'sw'
|
||||
} as TextSearch
|
||||
}))).to.deep.equalInAnyOrder(previewifyMedia(p4));
|
||||
Config.AlbumCover.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Boba'} as TextSearch;
|
||||
expect(Utils.clone(await pm.getAlbumCover({
|
||||
expect(Utils.clone(await pm.getCoverForAlbum({
|
||||
searchQuery: {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: 'sw'
|
||||
} as TextSearch
|
||||
}))).to.deep.equalInAnyOrder(previewifyMedia(p));
|
||||
Config.AlbumCover.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Derem'} as TextSearch;
|
||||
expect(Utils.clone(await pm.getAlbumCover({
|
||||
expect(Utils.clone(await pm.getCoverForAlbum({
|
||||
searchQuery: {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: 'sw'
|
||||
@ -234,7 +234,7 @@ describe('CoverManager', (sqlHelper: DBTestHelper) => {
|
||||
}))).to.deep.equalInAnyOrder(previewifyMedia(p2));
|
||||
// Having a preview search query that does not return valid result
|
||||
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: {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: 'Derem'
|
||||
@ -242,7 +242,7 @@ describe('CoverManager', (sqlHelper: DBTestHelper) => {
|
||||
}))).to.deep.equalInAnyOrder(previewifyMedia(p2));
|
||||
// having a saved search that does not have any image
|
||||
Config.AlbumCover.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Derem'} as TextSearch;
|
||||
expect(Utils.clone(await pm.getAlbumCover({
|
||||
expect(Utils.clone(await pm.getCoverForAlbum({
|
||||
searchQuery: {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: 'wont find it'
|
||||
|
@ -146,7 +146,7 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => {
|
||||
const setUpSqlDB = async () => {
|
||||
await sqlHelper.initDB();
|
||||
await setUpTestGallery();
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
await ObjectManagers.getInstance().init();
|
||||
};
|
||||
|
||||
|
||||
|
@ -15,6 +15,7 @@ describe('DiskMangerWorker', () => {
|
||||
Config.Database.type = DatabaseType.sqlite;
|
||||
Config.Faces.enabled = true;
|
||||
Config.Faces.keywordsToPersons = true;
|
||||
Config.Extensions.enabled = false;
|
||||
});
|
||||
|
||||
|
||||
@ -24,6 +25,7 @@ describe('DiskMangerWorker', () => {
|
||||
const dir = await DiskManager.scanDirectory('/');
|
||||
// should match the number of media (photo/video) files in the assets folder
|
||||
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 i = dir.media.findIndex(m => m.name === '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.Faces.enabled = true;
|
||||
Config.Faces.keywordsToPersons = true;
|
||||
Config.Extensions.enabled = false;
|
||||
});
|
||||
|
||||
|
||||
@ -66,7 +67,7 @@ describe('MetadataLoader', () => {
|
||||
});
|
||||
it('jpg 2', async () => {
|
||||
const data = await MetadataLoader.loadPhotoMetadata(
|
||||
path.join(__dirname, '/../../../assets/orientation/broken_orientation_exif2.jpg'));
|
||||
path.join(__dirname, '/../../../assets/orientation/broken_orientation_exif2.jpg'));
|
||||
const expected = require(path.join(__dirname, '/../../../assets/orientation/broken_orientation_exif2.json'));
|
||||
expect(Utils.clone(data)).to.be.deep.equal(expected);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user