1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-12-23 01:27:14 +02:00

Merge branches 'master' and 'bugfix/offset-or-ignore' of https://github.com/grasdk/pigallery2 into bugfix/offset-or-ignore

This commit is contained in:
grasdk 2024-05-11 15:27:50 +02:00
commit a44b7f8d15
9 changed files with 65 additions and 67 deletions

View File

@ -7,7 +7,7 @@ See sample extension at https://github.com/bpatrik/pigallery2-sample-extension.
# Extension Usage # Extension Usage
Extension folder can be set through config. For the docker-ised version, Extension folder can be set through config. For the docker-ised version,
they live under the `config/extension` folder in their own subdirectory. they live under the `config/extension` folder in their own subdirectory.
# Extension development # Extension development
@ -21,16 +21,20 @@ You need at least a `server.js` in your extension folder that exports a `init(ex
<path to the extension fodler>/myextension/package.js <- this is optional. You can add extra npm packages here <path to the extension fodler>/myextension/package.js <- this is optional. You can add extra npm packages here
<path to the extension fodler>/myextension/server.js <- this is needed <path to the extension fodler>/myextension/server.js <- this is needed
``` ```
Where `<path to the extension fodler>` is what you set in the config and `myextension` is the name of your extension. Where `<path to the extension fodler>` is what you set in the config and `myextension` is the name of your extension.
Note: you do not need to add your `node_modules` folder. The app will call `npm intall` when initializing your extension. Note: you do not need to add your `node_modules` folder. The app will call `npm install` when initializing your extension.
## Extension environment ## Extension environment
The app runs the extension the following way: The app runs the extension the following way:
* It reads all extensions in `<path to the extension fodler>/**` folder
* Checks if `package.js` is present. If yes installs the packages - It reads all extensions in `<path to the extension fodler>/**` folder
* Checks if `server.js` is present. If yes, calls the `init` function. - Checks if `package.js` is present. If yes installs the packages
- Checks if `server.js` is present. If yes, calls the `init` function.
### Init and cleanup lifecycle ### Init and cleanup lifecycle
There is also a `cleanUp` function that you can implement in your extension. There is also a `cleanUp` function that you can implement in your extension.
The app can call your `init` and `cleanUp` functions any time. The app can call your `init` and `cleanUp` functions any time.
Always calls the `init` first then `cleanUp` later. Always calls the `init` first then `cleanUp` later.
@ -39,8 +43,7 @@ Main use-case: `init` is called on app startup. `cleanUp` and `init` called late
## Extension interface ## Extension interface
The app calls the `init` and `cleanUp` function with a `IExtensionObject` object. The app calls the `init` and `cleanUp` function with a `IExtensionObject` object.
See https://github.com/bpatrik/pigallery2/blob/master/src/backend/model/extension/IExtension.ts for details. See https://github.com/bpatrik/pigallery2/blob/master/src/backend/model/extension/IExtension.ts for details.
`IExtensionObject` exposes lifecycle events, configs, RestAPis with some limitation. `IExtensionObject` exposes lifecycle events, configs, RestAPis with some limitation.
Changes made during the these public apis you do not need to clean up in the `cleanUp` function. Changes made during the these public apis you do not need to clean up in the `cleanUp` function.
@ -50,7 +53,7 @@ App also exposes private `_app` object to provide access to low level API. Any c
See sample server.js at https://github.com/bpatrik/pigallery2-sample-extension. See sample server.js at https://github.com/bpatrik/pigallery2-sample-extension.
It is recommended to do the development in `ts`, so creating a `server.ts`. It is recommended to do the development in `ts`, so creating a `server.ts`.
Note: You need to manually transpile your `server.ts` file to `server.js` as the app does not do that for you. Note: You need to manually transpile your `server.ts` file to `server.js` as the app does not do that for you.
This doc assumes you do the development in `ts`. This doc assumes you do the development in `ts`.
@ -59,23 +62,23 @@ This doc assumes you do the development in `ts`.
You can import package from both the main app package.json and from your extension package.json. You can import package from both the main app package.json and from your extension package.json.
To import packages from the main app, you import as usual. To import packages from the main app, you import as usual.
For packages from the extension, you always need to write relative path. i.e.: prefix with `./node_modules` For packages from the extension, you always need to write relative path. i.e.: prefix with `./node_modules`
```ts ```ts
// Including dev-kit interfaces. It is not necessary, only helps development with types. // Including dev-kit interfaces. It is not necessary, only helps development with types.
// You need to prefix them with ./node_modules // You need to prefix them with ./node_modules
import {IExtensionObject} from './node_modules/pigallery2-extension-kit'; import { IExtensionObject } from "./node_modules/pigallery2-extension-kit";
// Including prod extension packages. You need to prefix them with ./node_modules // Including prod extension packages. You need to prefix them with ./node_modules
// lodash does not have types // lodash does not have types
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
import * as _ from './node_modules/lodash'; import * as _ from "./node_modules/lodash";
// Importing packages that are available in the main app (listed in the packages.json in pigallery2) // Importing packages that are available in the main app (listed in the packages.json in pigallery2)
import {Column, Entity, Index, PrimaryGeneratedColumn} from 'typeorm'; import { Column, Entity, Index, PrimaryGeneratedColumn } from "typeorm";
``` ```
#### pigallery2 extension dev-kit
#### pigallery2 extension dev-kit
It is recommended to use the `pigallery2-extension-kit` node package. It is recommended to use the `pigallery2-extension-kit` node package.
`npm install pigallery2-extension-kit --save` to your extension. `npm install pigallery2-extension-kit --save` to your extension.
@ -87,17 +90,14 @@ You can then `import {IExtensionObject} from './node_modules/pigallery2-extensio
See https://github.com/bpatrik/pigallery2/blob/master/src/backend/model/extension/IExtension.ts to understand what contains `IExtensionObject`. See https://github.com/bpatrik/pigallery2/blob/master/src/backend/model/extension/IExtension.ts to understand what contains `IExtensionObject`.
NOTE: this is not needed to create an extension it only helps your IDE and your development. These type definitions are removed when you compile `ts` to `js`. NOTE: this is not needed to create an extension it only helps your IDE and your development. These type definitions are removed when you compile `ts` to `js`.
#### `init` function #### `init` function
You need to implement the `init` function for a working extension: You need to implement the `init` function for a working extension:
```ts ```ts
export const init = async (extension: IExtensionObject<void>): Promise<void> => {};
export const init = async (extension: IExtensionObject<void>): Promise<void> => {
}
``` ```
#### pigallery2 lifecycle `events` #### pigallery2 lifecycle `events`
@ -105,5 +105,4 @@ export const init = async (extension: IExtensionObject<void>): Promise<void> =>
Tha app exposes multiple interfaces for the extensions to interact with the main app. `events` are one of the main interfaces. Tha app exposes multiple interfaces for the extensions to interact with the main app. `events` are one of the main interfaces.
Here are their flow: Here are their flow:
![events_lifecycle](events.png) ![events_lifecycle](events.png)

14
package-lock.json generated
View File

@ -27,7 +27,7 @@
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"sharp": "0.31.3", "sharp": "0.31.3",
"ts-node-iptc": "1.0.11", "ts-node-iptc": "1.0.11",
"typeconfig": "2.2.15", "typeconfig": "2.3.1",
"typeorm": "0.3.12", "typeorm": "0.3.12",
"xml2js": "0.6.2" "xml2js": "0.6.2"
}, },
@ -20362,9 +20362,9 @@
} }
}, },
"node_modules/typeconfig": { "node_modules/typeconfig": {
"version": "2.2.15", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.2.15.tgz", "resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.3.1.tgz",
"integrity": "sha512-aqiuT5BtV0/0MYMMG78c1IqeJrF85r1W1pJckkGolPjHpE0ajA3oOgnRtX5DRDHsn3YzsY5FKMxj1B3J+ISx1g==", "integrity": "sha512-xBdsf0tK/PcXyZzWq/U2xLNDNrVFkRQ9d8V9x3B5eu1LG5GmeDDK7zLScz79zbl0dCZzhjnvx4RRggY6DZAAZA==",
"dependencies": { "dependencies": {
"minimist": "1.2.8" "minimist": "1.2.8"
} }
@ -35283,9 +35283,9 @@
} }
}, },
"typeconfig": { "typeconfig": {
"version": "2.2.15", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.2.15.tgz", "resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.3.1.tgz",
"integrity": "sha512-aqiuT5BtV0/0MYMMG78c1IqeJrF85r1W1pJckkGolPjHpE0ajA3oOgnRtX5DRDHsn3YzsY5FKMxj1B3J+ISx1g==", "integrity": "sha512-xBdsf0tK/PcXyZzWq/U2xLNDNrVFkRQ9d8V9x3B5eu1LG5GmeDDK7zLScz79zbl0dCZzhjnvx4RRggY6DZAAZA==",
"requires": { "requires": {
"minimist": "1.2.8" "minimist": "1.2.8"
} }

View File

@ -54,7 +54,7 @@
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"sharp": "0.31.3", "sharp": "0.31.3",
"ts-node-iptc": "1.0.11", "ts-node-iptc": "1.0.11",
"typeconfig": "2.2.15", "typeconfig": "2.3.1",
"typeorm": "0.3.12", "typeorm": "0.3.12",
"xml2js": "0.6.2" "xml2js": "0.6.2"
}, },

View File

@ -1,6 +1,6 @@
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import {Config} from '../common/config/private/Config'; import {PrivateConfigClass} from '../common/config/private/PrivateConfigClass';
export class ProjectPathClass { export class ProjectPathClass {
public Root: string; public Root: string;
@ -11,8 +11,10 @@ export class ProjectPathClass {
public FrontendFolder: string; public FrontendFolder: string;
public ExtensionFolder: string; public ExtensionFolder: string;
public DBFolder: string; public DBFolder: string;
private cfg: PrivateConfigClass;
constructor() { init(cfg: PrivateConfigClass) {
this.cfg = cfg;
this.reset(); this.reset();
} }
@ -31,12 +33,12 @@ export class ProjectPathClass {
reset(): void { reset(): void {
this.Root = path.join(__dirname, '/../../'); this.Root = path.join(__dirname, '/../../');
this.FrontendFolder = path.join(this.Root, 'dist'); this.FrontendFolder = path.join(this.Root, 'dist');
this.ImageFolder = this.getAbsolutePath(Config.Media.folder); this.ImageFolder = this.getAbsolutePath(this.cfg.Media.folder);
this.TempFolder = this.getAbsolutePath(Config.Media.tempFolder); this.TempFolder = this.getAbsolutePath(this.cfg.Media.tempFolder);
this.TranscodedFolder = path.join(this.TempFolder, 'tc'); this.TranscodedFolder = path.join(this.TempFolder, 'tc');
this.FacesFolder = path.join(this.TempFolder, 'f'); this.FacesFolder = path.join(this.TempFolder, 'f');
this.DBFolder = this.getAbsolutePath(Config.Database.dbFolder); this.DBFolder = this.getAbsolutePath(this.cfg.Database.dbFolder);
this.ExtensionFolder = this.getAbsolutePath(Config.Extensions.folder); this.ExtensionFolder = this.getAbsolutePath(this.cfg.Extensions.folder);
// create thumbnail folder if not exist // create thumbnail folder if not exist
if (!fs.existsSync(this.TempFolder)) { if (!fs.existsSync(this.TempFolder)) {

View File

@ -1,5 +1,6 @@
import {IExtensionConfig} from './IExtension'; import {IExtensionConfig} from './IExtension';
import {Config} from '../../../common/config/private/Config'; import {Config} from '../../../common/config/private/Config';
import {ServerExtensionsEntryConfig} from '../../../common/config/private/subconfigs/ServerExtensionsConfig';
export class ExtensionConfig<C> implements IExtensionConfig<C> { export class ExtensionConfig<C> implements IExtensionConfig<C> {
@ -8,8 +9,7 @@ export class ExtensionConfig<C> implements IExtensionConfig<C> {
public getConfig(): C { public getConfig(): C {
const c = (Config.Extensions.extensions || []) const c = Config.Extensions.extensions[this.extensionFolder] as ServerExtensionsEntryConfig;
.find(e => e.path === this.extensionFolder);
return c?.configs as C; return c?.configs as C;
} }

View File

@ -2,11 +2,8 @@ import {PrivateConfigClass} from '../../../common/config/private/PrivateConfigCl
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import {ServerExtensionsEntryConfig} from '../../../common/config/private/subconfigs/ServerExtensionsConfig'; import {ServerExtensionsEntryConfig} from '../../../common/config/private/subconfigs/ServerExtensionsConfig';
import * as child_process from 'child_process'; import {ProjectPath} from '../../ProjectPath';
const execSync = child_process.execSync;
const LOG_TAG = '[ExtensionConfigTemplateLoader]';
/** /**
* This class decouples the extension management and the config. * This class decouples the extension management and the config.
@ -16,7 +13,6 @@ const LOG_TAG = '[ExtensionConfigTemplateLoader]';
export class ExtensionConfigTemplateLoader { export class ExtensionConfigTemplateLoader {
private static instance: ExtensionConfigTemplateLoader; private static instance: ExtensionConfigTemplateLoader;
private extensionsFolder: string;
private loaded = false; private loaded = false;
private extensionList: string[] = []; private extensionList: string[] = [];
@ -31,29 +27,26 @@ export class ExtensionConfigTemplateLoader {
} }
init(extensionsFolder: string) {
this.extensionsFolder = extensionsFolder;
}
public loadExtensionTemplates(config: PrivateConfigClass) { public loadExtensionTemplates(config: PrivateConfigClass) {
if (!this.extensionsFolder) { if (!ProjectPath.ExtensionFolder) {
throw new Error('Unknown extensions folder.'); throw new Error('Unknown extensions folder.');
} }
// already loaded // already loaded
if (!this.loaded) { if (!this.loaded) {
this.extensionTemplates = []; this.extensionTemplates = [];
if (fs.existsSync(this.extensionsFolder)) { if (fs.existsSync(ProjectPath.ExtensionFolder)) {
this.extensionList = (fs this.extensionList = (fs
.readdirSync(this.extensionsFolder)) .readdirSync(ProjectPath.ExtensionFolder))
.filter((f): boolean => .filter((f): boolean =>
fs.statSync(path.join(this.extensionsFolder, f)).isDirectory() fs.statSync(path.join(ProjectPath.ExtensionFolder, f)).isDirectory()
); );
this.extensionList.sort(); this.extensionList.sort();
for (let i = 0; i < this.extensionList.length; ++i) { for (let i = 0; i < this.extensionList.length; ++i) {
const extFolder = this.extensionList[i]; const extFolder = this.extensionList[i];
const extPath = path.join(this.extensionsFolder, extFolder); const extPath = path.join(ProjectPath.ExtensionFolder, extFolder);
const configExtPath = path.join(extPath, 'config.js'); const configExtPath = path.join(extPath, 'config.js');
const serverExtPath = path.join(extPath, 'server.js'); const serverExtPath = path.join(extPath, 'server.js');
@ -92,16 +85,20 @@ export class ExtensionConfigTemplateLoader {
} }
const ePaths = this.extensionTemplates.map(et => et.folder); const ePaths = this.extensionTemplates.map(et => et.folder);
// delete not existing extensions // delete not existing extensions
config.Extensions.extensions = config.Extensions.extensions for (const prop of config.Extensions.extensions.keys()) {
.filter(ec => ePaths.indexOf(ec.path) !== -1); if (ePaths.indexOf(prop) > -1) {
continue;
}
config.Extensions.extensions.removeProperty(prop);
}
for (let i = 0; i < this.extensionTemplates.length; ++i) { for (let i = 0; i < this.extensionTemplates.length; ++i) {
const ext = this.extensionTemplates[i]; const ext = this.extensionTemplates[i];
let c = (config.Extensions.extensions || []) let c = config.Extensions.extensions[ext.folder];
.find(e => e.path === ext.folder);
// set the new structure with the new def values // set the new structure with the new def values
if (!c) { if (!c) {
@ -109,9 +106,7 @@ export class ExtensionConfigTemplateLoader {
if (ext.template) { if (ext.template) {
c.configs = new ext.template(); c.configs = new ext.template();
} }
// TODO: this does not hold if the order of the extensions mixes up. config.Extensions.extensions.addProperty(ext.folder, {type: ServerExtensionsEntryConfig}, c);
// TODO: experiment with a map instead of an array
config.Extensions.extensions.push(c);
} }
} }

View File

@ -12,6 +12,7 @@ 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,7 +81,7 @@ export class ExtensionManager implements IObjectManager {
extList.sort(); extList.sort();
Logger.debug(LOG_TAG, 'Extensions found: ', JSON.stringify(Config.Extensions.extensions.map(ec => ec.path))); Logger.debug(LOG_TAG, 'Extensions found: ', JSON.stringify(Config.Extensions.extensions.keys()));
} }
private createUniqueExtensionObject(name: string, folder: string): IExtensionObject<unknown> { private createUniqueExtensionObject(name: string, folder: string): IExtensionObject<unknown> {
@ -99,11 +100,12 @@ export class ExtensionManager implements IObjectManager {
private async initExtensions() { private async initExtensions() {
for (let i = 0; i < Config.Extensions.extensions.length; ++i) { for (const prop of Config.Extensions.extensions.keys()) {
const extFolder = Config.Extensions.extensions[i].path; const extConf: ServerExtensionsEntryConfig = Config.Extensions.extensions[prop] as ServerExtensionsEntryConfig;
const extFolder = extConf.path;
let extName = extFolder; let extName = extFolder;
if (Config.Extensions.extensions[i].enabled === false) { if (extConf.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,8 +1,7 @@
import {ExtensionConfigWrapper} from '../../../backend/model/extension/ExtensionConfigWrapper'; import {ExtensionConfigWrapper} from '../../../backend/model/extension/ExtensionConfigWrapper';
import {PrivateConfigClass} from './PrivateConfigClass'; import {PrivateConfigClass} from './PrivateConfigClass';
import {ConfigClassBuilder} from 'typeconfig/node'; import {ConfigClassBuilder} from 'typeconfig/node';
import {ExtensionConfigTemplateLoader} from '../../../backend/model/extension/ExtensionConfigTemplateLoader'; import {ProjectPath} from '../../../backend/ProjectPath';
import * as path from 'path';
// we need to know the location of the extensions to load the full config (including the extensions) // we need to know the location of the extensions to load the full config (including the extensions)
const pre = ConfigClassBuilder.attachPrivateInterface(new PrivateConfigClass()); const pre = ConfigClassBuilder.attachPrivateInterface(new PrivateConfigClass());
@ -10,6 +9,9 @@ try {
pre.loadSync({preventSaving: true}); pre.loadSync({preventSaving: true});
} catch (e) { /* empty */ } catch (e) { /* empty */
} }
ExtensionConfigTemplateLoader.Instance.init(path.join(__dirname, '/../../../../', pre.Extensions.folder)); // load extension paths before full config load
ProjectPath.init(pre);
export const Config = ExtensionConfigWrapper.originalSync(true); export const Config = ExtensionConfigWrapper.originalSync(true);
// set actual config
ProjectPath.init(Config);

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-inferrable-types */ /* eslint-disable @typescript-eslint/no-inferrable-types */
import {ConfigProperty, SubConfigClass} from 'typeconfig/common'; import {ConfigMap, ConfigProperty, IConfigMap, 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';
@ -59,16 +59,14 @@ export class ServerExtensionsConfig extends ClientExtensionsConfig {
}) })
folder: string = 'extensions'; folder: string = 'extensions';
// TODO: this does not hold if the order of the extensions mixes up.
// TODO: experiment with a map instead of an array
@ConfigProperty({ @ConfigProperty({
arrayType: ServerExtensionsEntryConfig, type: ConfigMap,
tags: { tags: {
name: $localize`Installed extensions`, name: $localize`Installed extensions`,
priority: ConfigPriority.advanced priority: ConfigPriority.advanced
} }
}) })
extensions: ServerExtensionsEntryConfig[] = []; extensions: IConfigMap<ServerExtensionsEntryConfig> = new ConfigMap();
@ConfigProperty({ @ConfigProperty({