1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-01-02 03:37:54 +02:00

Fix extension loading #784, #847, fixes #855

This commit is contained in:
Patrik J. Braun 2024-03-25 21:38:09 +01:00
parent f551509fee
commit d4d8dcfcdb
21 changed files with 306 additions and 182 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "pigallery2-extension-kit", "name": "pigallery2-extension-kit",
"version": "2.0.3-edge4", "version": "2.0.3-edge6",
"description": "Interfaces for developing extensions for pigallery2", "description": "Interfaces for developing extensions for pigallery2",
"author": "Patrik J. Braun", "author": "Patrik J. Braun",
"homepage": "https://github.com/bpatrik/pigallery2", "homepage": "https://github.com/bpatrik/pigallery2",

View File

@ -12,7 +12,7 @@ import * as child_process from 'child_process';
// @ts-ignore // @ts-ignore
import * as jeditor from 'gulp-json-editor'; import * as jeditor from 'gulp-json-editor';
import {XLIFF} from 'xlf-google-translate'; import {XLIFF} from 'xlf-google-translate';
import {PrivateConfigClass} from './src/common/config/private/Config'; import {PrivateConfigClass} from './src/common/config/private/PrivateConfigClass';
import {ConfigClassBuilder} from 'typeconfig/src/decorators/builders/ConfigClassBuilder'; import {ConfigClassBuilder} from 'typeconfig/src/decorators/builders/ConfigClassBuilder';
const execPr = util.promisify(child_process.exec); const execPr = util.promisify(child_process.exec);

View File

@ -1,7 +1,7 @@
import {NextFunction, Request, Response} from 'express'; import {NextFunction, Request, Response} from 'express';
import {ErrorCodes, ErrorDTO} from '../../common/entities/Error'; import {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
import {Message} from '../../common/entities/Message'; import {Message} from '../../common/entities/Message';
import {Config, PrivateConfigClass} from '../../common/config/private/Config'; import {PrivateConfigClass} from '../../common/config/private/PrivateConfigClass';
import {UserDTO, UserRoles} from '../../common/entities/UserDTO'; import {UserDTO, UserRoles} from '../../common/entities/UserDTO';
import {NotificationManager} from '../model/NotifocationManager'; import {NotificationManager} from '../model/NotifocationManager';
import {Logger} from '../Logger'; import {Logger} from '../Logger';

View File

@ -51,8 +51,8 @@ export class SettingsMWs {
await ConfigDiagnostics.runDiagnostics(); await ConfigDiagnostics.runDiagnostics();
// restart all schedule timers. In case they have changed // restart all schedule timers. In case they have changed
ObjectManagers.getInstance().JobManager.runSchedules(); ObjectManagers.getInstance().JobManager.runSchedules();
Logger.info(LOG_TAG, 'new config:'); // Logger.info(LOG_TAG, 'new config:');
Logger.info(LOG_TAG, JSON.stringify(Config.toJSON({attachDescription: false}), null, '\t')); // Logger.info(LOG_TAG, JSON.stringify(Config.toJSON({attachDescription: false}), null, '\t'));
return next(); return next();
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {

View File

@ -1,4 +1,4 @@
import {Config, PrivateConfigClass} from '../../../common/config/private/Config'; import {PrivateConfigClass} from '../../../common/config/private/PrivateConfigClass';
import {Logger} from '../../Logger'; import {Logger} from '../../Logger';
import {NotificationManager} from '../NotifocationManager'; import {NotificationManager} from '../NotifocationManager';
import {SQLConnection} from '../database/SQLConnection'; import {SQLConnection} from '../database/SQLConnection';
@ -27,8 +27,8 @@ import {SearchQueryParser} from '../../../common/SearchQueryParser';
import {SearchQueryTypes, TextSearch,} from '../../../common/entities/SearchQueryDTO'; import {SearchQueryTypes, TextSearch,} from '../../../common/entities/SearchQueryDTO';
import {Utils} from '../../../common/Utils'; import {Utils} from '../../../common/Utils';
import {JobRepository} from '../jobs/JobRepository'; import {JobRepository} from '../jobs/JobRepository';
import {ExtensionConfig} from '../extension/ExtensionConfigWrapper';
import {ConfigClassBuilder} from '../../../../node_modules/typeconfig/node'; import {ConfigClassBuilder} from '../../../../node_modules/typeconfig/node';
import { Config } from '../../../common/config/private/Config';
const LOG_TAG = '[ConfigDiagnostics]'; const LOG_TAG = '[ConfigDiagnostics]';

View File

@ -0,0 +1,18 @@
import {IExtensionConfig} from './IExtension';
import {Config} from '../../../common/config/private/Config';
export class ExtensionConfig<C> implements IExtensionConfig<C> {
constructor(private readonly extensionFolder: string) {
}
public getConfig(): C {
const c = (Config.Extensions.extensions || [])
.find(e => e.path === this.extensionFolder);
return c?.configs as C;
}
}

View File

@ -0,0 +1,110 @@
import {PrivateConfigClass} from '../../../common/config/private/PrivateConfigClass';
import * as fs from 'fs';
import * as path from 'path';
import {ServerExtensionsEntryConfig} from '../../../common/config/private/subconfigs/ServerExtensionsConfig';
const LOG_TAG = '[ExtensionConfigTemplateLoader]';
/**
* This class decouples the extension management and the config.
* It helps to solve the "chicken and the egg" which should load first:
* Config or the extension as they have a circular dependency
*/
export class ExtensionConfigTemplateLoader {
private static instance: ExtensionConfigTemplateLoader;
private extensionsFolder: string;
private loaded = false;
private extensionList: string[] = [];
private extensionTemplates: { folder: string, template?: { new(): unknown } }[] = [];
public static get Instance() {
if (!this.instance) {
this.instance = new ExtensionConfigTemplateLoader();
}
return this.instance;
}
init(extensionsFolder: string) {
this.extensionsFolder = extensionsFolder;
}
public loadExtensionTemplates(config: PrivateConfigClass) {
if (!this.extensionsFolder) {
throw new Error('Unknown extensions folder.');
}
// already loaded
if (!this.loaded) {
this.extensionList = (fs
.readdirSync(this.extensionsFolder))
.filter((f): boolean =>
fs.statSync(path.join(this.extensionsFolder, f)).isDirectory()
);
this.extensionList.sort();
this.extensionTemplates = [];
for (let i = 0; i < this.extensionList.length; ++i) {
const extFolder = this.extensionList[i];
const extPath = path.join(this.extensionsFolder, extFolder);
const serverExtPath = path.join(extPath, 'server.js');
if (!fs.existsSync(serverExtPath)) {
continue;
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ext = require(serverExtPath);
if (typeof ext?.initConfig === 'function') {
ext?.initConfig({
setConfigTemplate: (template: { new(): unknown }): void => {
this.extensionTemplates.push({folder: extFolder, template: template});
}
});
} else {
//also create basic config extensions that do not have any
this.extensionTemplates.push({folder: extFolder});
}
}
this.loaded = true;
}
this.setTemplatesToConfig(config);
}
private setTemplatesToConfig(config: PrivateConfigClass) {
if (!this.extensionTemplates) {
return;
}
const ePaths = this.extensionTemplates.map(et => et.folder);
// delete not existing extensions
config.Extensions.extensions = config.Extensions.extensions
.filter(ec => ePaths.indexOf(ec.path) !== -1);
for (let i = 0; i < this.extensionTemplates.length; ++i) {
const ext = this.extensionTemplates[i];
let c = (config.Extensions.extensions || [])
.find(e => e.path === ext.folder);
// set the new structure with the new def values
if (!c) {
c = new ServerExtensionsEntryConfig(ext.folder);
if (ext.template) {
c.configs= new ext.template()
}
config.Extensions.extensions.push(c);
}
}
}
}

View File

@ -1,64 +1,49 @@
import {IConfigClass} from 'typeconfig/common'; import {IConfigClass} from 'typeconfig/common';
import {Config, PrivateConfigClass} from '../../../common/config/private/Config'; import {PrivateConfigClass} from '../../../common/config/private/PrivateConfigClass';
import {ConfigClassBuilder} from 'typeconfig/node'; import {ConfigClassBuilder} from 'typeconfig/node';
import {IExtensionConfig} from './IExtension'; import {ExtensionConfigTemplateLoader} from './ExtensionConfigTemplateLoader';
import {ObjectManagers} from '../ObjectManagers'; import {NotificationManager} from '../NotifocationManager';
import {ServerExtensionsEntryConfig} from '../../../common/config/private/subconfigs/ServerExtensionsConfig';
const LOG_TAG = '[ExtensionConfigWrapper]';
/** /**
* Wraps to original config and makes sure all extension related config is loaded * Wraps to original config and makes sure all extension related config is loaded
*/ */
export class ExtensionConfigWrapper { export class ExtensionConfigWrapper {
static async original(): Promise<PrivateConfigClass & IConfigClass> {
static async original(showError = false): Promise<PrivateConfigClass & IConfigClass> {
const pc = ConfigClassBuilder.attachPrivateInterface(new PrivateConfigClass()); const pc = ConfigClassBuilder.attachPrivateInterface(new PrivateConfigClass());
ExtensionConfigTemplateLoader.Instance.loadExtensionTemplates(pc);
try { try {
await pc.load(); // loading the basic configs but we do not know the extension config hierarchy yet await pc.load(); // loading the basic configs, but we do not know the extension config hierarchy yet
if (ObjectManagers.isReady()) {
for (const ext of Object.values(ObjectManagers.getInstance().ExtensionManager.extObjects)) {
ext.config.loadToConfig(ConfigClassBuilder.attachPrivateInterface(pc));
}
}
await pc.load(); // loading the extension related configs
} catch (e) { } catch (e) {
console.error('Error during loading original config. Reverting to defaults.'); if(showError){
console.error(e); console.error(LOG_TAG,'Error during loading config. Reverting to defaults.');
console.error(LOG_TAG,'This is most likely due to: 1) you added a bad configuration in the server.json OR 2) The configuration changed in the latest release.');
console.error(e);
NotificationManager.error('Can\'t load config. Reverting to default. This is most likely due to: 1) you added a bad configuration in the server.json OR 2) The configuration changed in the latest release.', (e.toString ? e.toString() : JSON.stringify(e)));
}
}
return pc;
}
static originalSync(showError = false): PrivateConfigClass & IConfigClass {
const pc = ConfigClassBuilder.attachPrivateInterface(new PrivateConfigClass());
ExtensionConfigTemplateLoader.Instance.loadExtensionTemplates(pc);
try {
pc.loadSync(); // loading the basic configs, but we do not know the extension config hierarchy yet
} catch (e) {
if(showError){
console.error(LOG_TAG,'Error during loading config. Reverting to defaults.');
console.error(LOG_TAG,'This is most likely due to: 1) you added a bad configuration in the server.json OR 2) The configuration changed in the latest release.');
console.error(e);
NotificationManager.error('Ca\'nt load config. Reverting to default. This is most likely due to: 1) you added a bad configuration in the server.json OR 2) The configuration changed in the latest release.', (e.toString ? e.toString() : JSON.stringify(e)));
}
} }
return pc; return pc;
} }
} }
export class ExtensionConfig<C> implements IExtensionConfig<C> {
public template: new() => C;
constructor(private readonly extensionFolder: string) {
}
private findConfig(config: PrivateConfigClass): ServerExtensionsEntryConfig {
let c = (config.Extensions.extensions || []).find(e => e.path === this.extensionFolder);
if (!c) {
c = new ServerExtensionsEntryConfig(this.extensionFolder);
config.Extensions.extensions.push(c);
}
return c;
}
public getConfig(): C {
return this.findConfig(Config).configs as C;
}
public setTemplate(template: new() => C): void {
this.template = template;
this.loadToConfig(Config);
}
loadToConfig(config: PrivateConfigClass) {
if (!this.template) {
return;
}
const confTemplate = ConfigClassBuilder.attachPrivateInterface(new this.template());
const extConf = this.findConfig(config);
extConf.configs = confTemplate;
}
}

View File

@ -12,7 +12,6 @@ import {SQLConnection} from '../database/SQLConnection';
import {ExtensionObject} from './ExtensionObject'; import {ExtensionObject} from './ExtensionObject';
import {ExtensionDecoratorObject} from './ExtensionDecorator'; import {ExtensionDecoratorObject} from './ExtensionDecorator';
import * as util from 'util'; import * as util from 'util';
import {ServerExtensionsEntryConfig} from '../../../common/config/private/subconfigs/ServerExtensionsConfig';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const exec = util.promisify(require('child_process').exec); const exec = util.promisify(require('child_process').exec);
@ -80,15 +79,8 @@ export class ExtensionManager implements IObjectManager {
); );
extList.sort(); extList.sort();
// delete not existing extensions
Config.Extensions.extensions = Config.Extensions.extensions.filter(ec => extList.indexOf(ec.path) !== -1);
// Add new extensions Logger.debug(LOG_TAG, 'Extensions found: ', JSON.stringify(Config.Extensions.extensions.map(ec => ec.path)));
const ePaths = Config.Extensions.extensions.map(ec => ec.path);
extList.filter(ep => ePaths.indexOf(ep) === -1).forEach(ep =>
Config.Extensions.extensions.push(new ServerExtensionsEntryConfig(ep)));
Logger.debug(LOG_TAG, 'Extensions found ', JSON.stringify(Config.Extensions.extensions.map(ec => ec.path)));
} }
private createUniqueExtensionObject(name: string, folder: string): IExtensionObject<unknown> { private createUniqueExtensionObject(name: string, folder: string): IExtensionObject<unknown> {
@ -111,7 +103,7 @@ export class ExtensionManager implements IObjectManager {
const extFolder = Config.Extensions.extensions[i].path; const extFolder = Config.Extensions.extensions[i].path;
let extName = extFolder; let extName = extFolder;
if(Config.Extensions.extensions[i].enabled === false){ if (Config.Extensions.extensions[i].enabled === false) {
Logger.silly(LOG_TAG, `Skipping ${extFolder} initiation. Extension is disabled.`); Logger.silly(LOG_TAG, `Skipping ${extFolder} initiation. Extension is disabled.`);
} }
const extPath = path.join(ProjectPath.ExtensionFolder, extFolder); const extPath = path.join(ProjectPath.ExtensionFolder, extFolder);

View File

@ -1,12 +1,12 @@
import {IExtensionEvents, IExtensionObject} from './IExtension'; import {IExtensionEvents, IExtensionObject} from './IExtension';
import {ExtensionApp} from './ExtensionApp'; import {ExtensionApp} from './ExtensionApp';
import {ExtensionConfig} from './ExtensionConfigWrapper';
import {ExtensionDB} from './ExtensionDB'; import {ExtensionDB} from './ExtensionDB';
import {ProjectPath} from '../../ProjectPath'; import {ProjectPath} from '../../ProjectPath';
import {ExpressRouterWrapper} from './ExpressRouterWrapper'; import {ExpressRouterWrapper} from './ExpressRouterWrapper';
import {createLoggerWrapper} from '../../Logger'; import {createLoggerWrapper} from '../../Logger';
import * as express from 'express'; import * as express from 'express';
import {ExtensionMessengerHandler} from './ExtensionMessengerHandler'; import {ExtensionMessengerHandler} from './ExtensionMessengerHandler';
import {ExtensionConfig} from './ExtensionConfig';
export class ExtensionObject<C> implements IExtensionObject<C> { export class ExtensionObject<C> implements IExtensionObject<C> {

View File

@ -1,6 +1,6 @@
import * as express from 'express'; import * as express from 'express';
import {NextFunction, Request, Response} from 'express'; import {NextFunction, Request, Response} from 'express';
import {PrivateConfigClass} from '../../../common/config/private/Config'; import {PrivateConfigClass} from '../../../common/config/private/PrivateConfigClass';
import {ObjectManagers} from '../ObjectManagers'; import {ObjectManagers} from '../ObjectManagers';
import {ProjectPathClass} from '../../ProjectPath'; import {ProjectPathClass} from '../../ProjectPath';
import {ILogger} from '../../Logger'; import {ILogger} from '../../Logger';
@ -141,8 +141,6 @@ export interface IExtensionDB {
} }
export interface IExtensionConfig<C> { export interface IExtensionConfig<C> {
setTemplate(template: new() => C): void;
getConfig(): C; getConfig(): C;
} }
@ -210,11 +208,25 @@ export interface IExtensionObject<C = void> {
messengers: IExtensionMessengers; messengers: IExtensionMessengers;
} }
export interface IExtensionConfigInit<C> {
/**
* Sets the config tempalte class
* @param template
*/
setConfigTemplate(template: new() => C): void;
}
/** /**
* Extension interface. All extension is expected to implement and export these methods * Extension interface. All extension is expected to implement and export these methods
*/ */
export interface IServerExtension<C> { export interface IServerExtension<C> {
/**
* This function can be called any time. It should only set the config template class
* @param extension
*/
initConfig(extension: IExtensionConfigInit<C>): void;
/** /**
* Extension init function. Extension should at minimum expose this function. * Extension init function. Extension should at minimum expose this function.
* @param extension * @param extension

View File

@ -1,101 +1,13 @@
/* eslint-disable @typescript-eslint/no-var-requires */ import {ExtensionConfigWrapper} from '../../../backend/model/extension/ExtensionConfigWrapper';
import {ServerConfig} from './PrivateConfig'; import {PrivateConfigClass} from './PrivateConfigClass';
import * as crypto from 'crypto'; import {ConfigClassBuilder} from 'typeconfig/node';
import {ExtensionConfigTemplateLoader} from '../../../backend/model/extension/ExtensionConfigTemplateLoader';
import * as path from 'path'; import * as path from 'path';
import {ConfigClass, ConfigClassBuilder} from 'typeconfig/node';
import {IConfigClass} from 'typeconfig/common';
import {PasswordHelper} from '../../../backend/model/PasswordHelper';
import {TAGS} from '../public/ClientConfig';
import {NotificationManager} from '../../../backend/model/NotifocationManager';
declare const process: any; const pre = ConfigClassBuilder.attachPrivateInterface(new PrivateConfigClass());
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);
@ConfigClass<IConfigClass<TAGS> & ServerConfig>({
configPath: path.join(__dirname, !isTesting ? './../../../../config.json' : './../../../../test/backend/tmp/config.json'),
crateConfigPathIfNotExists: isTesting,
saveIfNotExist: true,
attachDescription: true,
enumsAsString: true,
softReadonly: true,
cli: {
enable: {
configPath: true,
attachState: true,
attachDescription: true,
rewriteCLIConfig: true,
rewriteENVConfig: true,
enumsAsString: true,
saveIfNotExist: true,
exitOnConfig: true,
},
defaults: {
enabled: true,
},
},
onLoadedSync: async (config) => {
let changed = false;
for (let i = 0; i < config.Users.enforcedUsers.length; ++i) {
const uc = config.Users.enforcedUsers[i];
// encrypt password and save back to the config
if (uc.password) {
if (!uc.encryptedPassword) {
uc.encryptedPassword = PasswordHelper.cryptPassword(uc.password);
}
uc.password = '';
changed = true;
}
if (!uc.encrypted) {
uc.encrypted = !!uc.encryptedPassword;
changed = true;
}
if (!uc.encrypted && !uc.password) {
throw new Error('Password error for enforced user: ' + uc.name);
}
}
if (changed) {
config.saveSync();
}
}
})
export class PrivateConfigClass extends ServerConfig {
constructor() {
super();
if (!this.Server.sessionSecret || this.Server.sessionSecret.length === 0) {
this.Server.sessionSecret = [
crypto.randomBytes(256).toString('hex'),
crypto.randomBytes(256).toString('hex'),
crypto.randomBytes(256).toString('hex'),
];
}
this.Environment.appVersion =
require('../../../../package.json').version;
this.Environment.buildTime =
require('../../../../package.json').buildTime;
this.Environment.buildCommitHash =
require('../../../../package.json').buildCommitHash;
this.Environment.upTime = upTime;
this.Environment.isDocker = !!process.env.PI_DOCKER;
}
}
export const Config = ConfigClassBuilder.attachInterface(
new PrivateConfigClass()
);
try { try {
Config.loadSync(); pre.loadSync();
} catch (e) { } catch (e) { /* empty */ }
console.error('Error during loading config. Reverting to defaults.'); ExtensionConfigTemplateLoader.Instance.init(path.join(__dirname, '/../../../../', pre.Extensions.folder));
console.error('This is most likely due to: 1) you added a bad configuration in the server.json OR 2) The configuration changed in the latest release.');
console.error(e); export const Config = ExtensionConfigWrapper.originalSync(true);
NotificationManager.error('Cant load config. Reverting to default. This is most likely due to: 1) you added a bad configuration in the server.json OR 2) The configuration changed in the latest release.', (e.toString ? e.toString() : JSON.stringify(e)));
}

View File

@ -0,0 +1,89 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import {ServerConfig} from './PrivateConfig';
import * as crypto from 'crypto';
import * as path from 'path';
import {ConfigClass, ConfigClassBuilder} from 'typeconfig/node';
import {IConfigClass} from 'typeconfig/common';
import {PasswordHelper} from '../../../backend/model/PasswordHelper';
import {TAGS} from '../public/ClientConfig';
import {NotificationManager} from '../../../backend/model/NotifocationManager';
declare const process: any;
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);
@ConfigClass<IConfigClass<TAGS> & ServerConfig>({
configPath: path.join(__dirname, !isTesting ? './../../../../config.json' : './../../../../test/backend/tmp/config.json'),
crateConfigPathIfNotExists: isTesting,
saveIfNotExist: true,
attachDescription: true,
enumsAsString: true,
softReadonly: true,
cli: {
enable: {
configPath: true,
attachState: true,
attachDescription: true,
rewriteCLIConfig: true,
rewriteENVConfig: true,
enumsAsString: true,
saveIfNotExist: true,
exitOnConfig: true,
},
defaults: {
enabled: true,
},
},
onLoadedSync: async (config) => {
let changed = false;
for (let i = 0; i < config.Users.enforcedUsers.length; ++i) {
const uc = config.Users.enforcedUsers[i];
// encrypt password and save back to the config
if (uc.password) {
if (!uc.encryptedPassword) {
uc.encryptedPassword = PasswordHelper.cryptPassword(uc.password);
}
uc.password = '';
changed = true;
}
if (!uc.encrypted) {
uc.encrypted = !!uc.encryptedPassword;
changed = true;
}
if (!uc.encrypted && !uc.password) {
throw new Error('Password error for enforced user: ' + uc.name);
}
}
if (changed) {
config.saveSync();
}
}
})
export class PrivateConfigClass extends ServerConfig {
constructor() {
super();
if (!this.Server.sessionSecret || this.Server.sessionSecret.length === 0) {
this.Server.sessionSecret = [
crypto.randomBytes(256).toString('hex'),
crypto.randomBytes(256).toString('hex'),
crypto.randomBytes(256).toString('hex'),
];
}
this.Environment.appVersion =
require('../../../../package.json').version;
this.Environment.buildTime =
require('../../../../package.json').buildTime;
this.Environment.buildCommitHash =
require('../../../../package.json').buildCommitHash;
this.Environment.upTime = upTime;
this.Environment.isDocker = !!process.env.PI_DOCKER;
}
}

View File

@ -2,6 +2,7 @@
import {ConfigProperty, SubConfigClass} from 'typeconfig/common'; import {ConfigProperty, SubConfigClass} from 'typeconfig/common';
import {ClientExtensionsConfig, ConfigPriority, TAGS} from '../../public/ClientConfig'; import {ClientExtensionsConfig, ConfigPriority, TAGS} from '../../public/ClientConfig';
import {GenericConfigType} from 'typeconfig/src/GenericConfigType'; import {GenericConfigType} from 'typeconfig/src/GenericConfigType';
declare let $localize: (s: TemplateStringsArray) => string; declare let $localize: (s: TemplateStringsArray) => string;
if (typeof $localize === 'undefined') { if (typeof $localize === 'undefined') {

View File

@ -107,7 +107,7 @@
[ngModel]="rStates?.value.__state[ck]"> [ngModel]="rStates?.value.__state[ck]">
</app-settings-entry> </app-settings-entry>
<!-- Config entries ---> <!-- Config entries --->
<ng-container *ngIf="isExpandableConfig(rStates.value.__state[ck])"> <ng-container *ngIf="isExpandableConfig(rStates.value.__state[ck]) && rStates.value.__state[ck].value">
<!-- Sub category with header and menu item --> <!-- Sub category with header and menu item -->
<div class="card mt-2 mb-2" *ngIf="topLevel && rStates?.value.__state[ck].tags?.uiIcon" <div class="card mt-2 mb-2" *ngIf="topLevel && rStates?.value.__state[ck].tags?.uiIcon"
[id]="ConfigPath+'.'+ck"> [id]="ConfigPath+'.'+ck">

View File

@ -220,12 +220,12 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
st.original = Utils.clone(st.value); st.original = Utils.clone(st.value);
} }
if (st.isConfigType) { if (st.isConfigType && st.value) {
for (const k of Object.keys(st.value.__state)) { for (const k of Object.keys(st.value.__state)) {
instrument(st.value.__state[k], st); instrument(st.value.__state[k], st);
} }
} }
if (st.isConfigArrayType) { if (st.isConfigArrayType && st.value) {
for (let i = 0; i < st.value?.length; ++i) { for (let i = 0; i < st.value?.length; ++i) {
for (const k of Object.keys(st.value[i].__state)) { for (const k of Object.keys(st.value[i].__state)) {
instrument(st.value[i].__state[k], st); instrument(st.value[i].__state[k], st);
@ -317,6 +317,9 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
if (states.keys) { if (states.keys) {
return states.keys; return states.keys;
} }
if (!states.value) {
return [];
}
const s = states.value.__state; const s = states.value.__state;
const keys = Object.keys(s).sort((a, b) => { const keys = Object.keys(s).sort((a, b) => {
if ((this.isExpandableConfig(s[a]) || s[a].isConfigArrayType) !== (this.isExpandableConfig(s[b]) || s[b].isConfigArrayType)) { if ((this.isExpandableConfig(s[a]) || s[a].isConfigArrayType) !== (this.isExpandableConfig(s[b]) || s[b].isConfigArrayType)) {

View File

@ -20,11 +20,14 @@ import {
import {DirectoryBaseDTO, DirectoryPathDTO} from '../src/common/entities/DirectoryDTO'; import {DirectoryBaseDTO, DirectoryPathDTO} from '../src/common/entities/DirectoryDTO';
import {FileDTO} from '../src/common/entities/FileDTO'; import {FileDTO} from '../src/common/entities/FileDTO';
import {DiskManager} from '../src/backend/model/fileaccess/DiskManager'; import {DiskManager} from '../src/backend/model/fileaccess/DiskManager';
import * as path from 'path';
export class TestHelper { export class TestHelper {
static creationCounter = 0; static creationCounter = 0;
public static readonly TMP_DIR= path.join(__dirname, './tmp');
public static getDirectoryEntry(parent: DirectoryBaseDTO = null, name = 'wars dir'): DirectoryEntity { public static getDirectoryEntry(parent: DirectoryBaseDTO = null, name = 'wars dir'): DirectoryEntity {
const dir = new DirectoryEntity(); const dir = new DirectoryEntity();

View File

@ -47,7 +47,6 @@ export class DBTestHelper {
mysql: process.env.TEST_MYSQL !== 'false' mysql: process.env.TEST_MYSQL !== 'false'
}; };
public static readonly savedDescribe = savedDescribe; public static readonly savedDescribe = savedDescribe;
public tempDir: string;
public readonly testGalleyEntities: { public readonly testGalleyEntities: {
dir: ParentDirectoryDTO, dir: ParentDirectoryDTO,
subDir: SubDirectoryDTO, subDir: SubDirectoryDTO,
@ -80,7 +79,6 @@ export class DBTestHelper {
}; };
constructor(public dbType: DatabaseType) { constructor(public dbType: DatabaseType) {
this.tempDir = path.join(__dirname, './tmp');
} }
static describe(settingsOverride: { static describe(settingsOverride: {
@ -206,7 +204,7 @@ export class DBTestHelper {
await ObjectManagers.reset(); await ObjectManagers.reset();
Config.Database.type = DatabaseType.mysql; Config.Database.type = DatabaseType.mysql;
Config.Database.mysql.database = 'pigallery2_test'; Config.Database.mysql.database = 'pigallery2_test';
await fs.promises.rm(this.tempDir, {recursive: true, force: true}); await fs.promises.rm(TestHelper.TMP_DIR, {recursive: true, force: true});
const conn = await SQLConnection.getConnection(); const conn = await SQLConnection.getConnection();
await conn.query('DROP DATABASE IF EXISTS ' + conn.options.database); await conn.query('DROP DATABASE IF EXISTS ' + conn.options.database);
await SQLConnection.close(); await SQLConnection.close();
@ -225,10 +223,10 @@ export class DBTestHelper {
private async clearUpSQLite(): Promise<void> { private async clearUpSQLite(): Promise<void> {
Logger.debug(LOG_TAG, 'clearing up sqlite'); Logger.debug(LOG_TAG, 'clearing up sqlite');
Config.Database.type = DatabaseType.sqlite; Config.Database.type = DatabaseType.sqlite;
Config.Database.dbFolder = this.tempDir; Config.Database.dbFolder = TestHelper.TMP_DIR;
ProjectPath.reset(); ProjectPath.reset();
await ObjectManagers.reset(); await ObjectManagers.reset();
await fs.promises.rm(this.tempDir, {recursive: true, force: true}); await fs.promises.rm(TestHelper.TMP_DIR, {recursive: true, force: true});
} }
} }

View File

@ -7,6 +7,7 @@ import {SuperAgentStatic} from 'superagent';
import {ProjectPath} from '../../../../src/backend/ProjectPath'; import {ProjectPath} from '../../../../src/backend/ProjectPath';
import {DBTestHelper} from '../../DBTestHelper'; import {DBTestHelper} from '../../DBTestHelper';
import {ReIndexingSensitivity} from '../../../../src/common/config/private/PrivateConfig'; import {ReIndexingSensitivity} from '../../../../src/common/config/private/PrivateConfig';
import {TestHelper} from '../../../TestHelper';
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
@ -25,14 +26,13 @@ describe = DBTestHelper.describe({sqlite: true});
describe('GalleryRouter', (sqlHelper: DBTestHelper) => { describe('GalleryRouter', (sqlHelper: DBTestHelper) => {
describe = tmpDescribe; describe = tmpDescribe;
const tempDir = sqlHelper.tempDir;
let server: Server; let server: Server;
const setUp = async () => { const setUp = async () => {
await sqlHelper.initDB(); await sqlHelper.initDB();
Config.Users.authenticationRequired = false; Config.Users.authenticationRequired = false;
Config.Media.Video.enabled = true; Config.Media.Video.enabled = true;
Config.Media.folder = path.join(__dirname, '../../assets'); Config.Media.folder = path.join(__dirname, '../../assets');
Config.Media.tempFolder = tempDir; Config.Media.tempFolder = TestHelper.TMP_DIR;
ProjectPath.reset(); ProjectPath.reset();
server = new Server(); server = new Server();
await server.onStarted.wait(); await server.onStarted.wait();

View File

@ -19,6 +19,7 @@ describe('SettingsRouter', () => {
const tempDir = path.join(__dirname, '../../tmp'); const tempDir = path.join(__dirname, '../../tmp');
beforeEach(async () => { beforeEach(async () => {
await ObjectManagers.reset();
await fs.promises.rm(tempDir, {recursive: true, force: true}); await fs.promises.rm(tempDir, {recursive: true, force: true});
Config.Database.type = DatabaseType.sqlite; Config.Database.type = DatabaseType.sqlite;
Config.Database.dbFolder = tempDir; Config.Database.dbFolder = tempDir;

View File

@ -177,10 +177,10 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
expect(selected?.media?.length) expect(selected?.media?.length)
.to.be.greaterThan(0); .to.be.greaterThan(0);
if (!fs.existsSync(sqlHelper.tempDir)) { if (!fs.existsSync(TestHelper.TMP_DIR)) {
fs.mkdirSync(sqlHelper.tempDir); fs.mkdirSync(TestHelper.TMP_DIR);
} }
const tmpDir = path.join(sqlHelper.tempDir, '/rnd5sdf_emptyDir'); const tmpDir = path.join(TestHelper.TMP_DIR, '/rnd5sdf_emptyDir');
fs.mkdirSync(tmpDir); fs.mkdirSync(tmpDir);
ProjectPath.ImageFolder = tmpDir; ProjectPath.ImageFolder = tmpDir;
let notFailed = false; let notFailed = false;