From 71feba9ce8143d8e0a5758abf78a907293618e53 Mon Sep 17 00:00:00 2001 From: Alan Grainger Date: Mon, 4 Nov 2024 20:23:34 +0100 Subject: [PATCH] Add #10 - Single item as a gallery --- .env.example | 3 --- README.md | 19 +++++--------- .eslintrc => app/.eslintrc | 0 app/config.json | 7 +++++ app/package.json | 2 +- app/src/functions.ts | 54 ++++++++++++++++++++++++++++++++++++++ app/src/immich.ts | 13 ++++----- app/src/index.ts | 27 ++++--------------- app/src/render.ts | 11 +++----- docker-compose.yml | 5 ++-- 10 files changed, 87 insertions(+), 54 deletions(-) delete mode 100644 .env.example rename .eslintrc => app/.eslintrc (100%) create mode 100644 app/src/functions.ts diff --git a/.env.example b/.env.example deleted file mode 100644 index e6241f1..0000000 --- a/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -IMMICH_URL=http://localhost:2283 -PORT=3000 -CACHE_AGE=2592000 diff --git a/README.md b/README.md index 97c7f6b..b1d236a 100644 --- a/README.md +++ b/README.md @@ -48,17 +48,7 @@ Here is an example setup for [securing Immich behind mTLS](./docs/securing-immic 1. Download the [docker-compose.yml](https://github.com/alangrainger/immich-public-proxy/blob/main/docker-compose.yml) file. -2. Create a `.env` file to configure the app: - -``` -IMMICH_URL=http://localhost:2283 -PORT=3000 -CACHE_AGE=2592000 -``` - -- `IMMICH_URL` is the URL to access Immich in your local network. This is not your public URL. -- `PORT` is the external port you want for the docker container. -- `CACHE_AGE` this is setting the Cache-Control header, to tell the visitor's browser to cache the assets. Set to 0 to disable caching. By default this is 30 days. +2. Update the value for `IMMICH_URL` in your docker-compose file to point to your local URL for Immich. This should not be a public URL. 3. Start the docker container: @@ -102,7 +92,7 @@ If the shared link has expired or any of the assets have been put in the Immich ## Additional configuration -The gallery is created using [lightGallery](https://github.com/sachinchoolur/lightGallery). You can adjust various settings to customise how your gallery displays. +There are some additional configuration options you can change, for example the way the gallery is set up. 1. Make a copy of [config.json](https://github.com/alangrainger/immich-public-proxy/blob/main/app/config.json) in the same folder as your `docker-compose.yml`. @@ -115,10 +105,13 @@ The gallery is created using [lightGallery](https://github.com/sachinchoolur/lig 3. Restart your container and your custom configuration should be active. +### lightGallery + +The gallery is created using [lightGallery](https://github.com/sachinchoolur/lightGallery). You can find all of lightGallery's settings here: https://www.lightgalleryjs.com/docs/settings/ -For example, to disable the download button for images, you would change `download` to `false`: +For example, to disable the download button for images, you would edit the `lightGallery` section and change `download` to `false`: ```json { diff --git a/.eslintrc b/app/.eslintrc similarity index 100% rename from .eslintrc rename to app/.eslintrc diff --git a/app/config.json b/app/config.json index 0faf968..d519c94 100644 --- a/app/config.json +++ b/app/config.json @@ -1,4 +1,11 @@ { + "ipp": { + "responseHeaders": { + "Cache-Control": "public, max-age=2592000" + }, + "singleImageGallery": false, + "singleItemAutoOpen": true + }, "lightGallery": { "controls": true, "download": true, diff --git a/app/package.json b/app/package.json index 516d102..401c900 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "immich-public-proxy", - "version": "1.3.6", + "version": "1.3.7", "scripts": { "dev": "ts-node src/index.ts", "build": "npx tsc", diff --git a/app/src/functions.ts b/app/src/functions.ts new file mode 100644 index 0000000..1b9ea65 --- /dev/null +++ b/app/src/functions.ts @@ -0,0 +1,54 @@ +import dayjs from 'dayjs' +import { Request, Response } from 'express-serve-static-core' +import { ImageSize } from './types' + +let config = {} +try { + const configJson = require('../config.json') + if (typeof configJson === 'object') config = configJson +} catch (e) { } + +/** + * Get a configuration option using dotted notation. + * + * @param path + * @param [defaultOption] - Specify a default option to return if no configuation value is found + * + * @example + * getConfigOption('ipp.singleImageGallery') + */ +export const getConfigOption = (path: string, defaultOption?: unknown) => { + const value = path.split('.').reduce((obj: { [key: string]: unknown }, key) => (obj || {})[key], config) + if (value === undefined) { + return defaultOption + } else { + return value + } +} + +/** + * Output a console.log message with timestamp + */ +export const log = (message: string) => console.log(dayjs().format() + ' ' + message) + +/** + * Sanitise the data for an incoming query string `size` parameter + * e.g. https://example.com/share/abc...xyz?size=thumbnail + */ +export function getSize (req: Request) { + return req.query?.size === 'thumbnail' ? ImageSize.thumbnail : ImageSize.original +} + +export function toString (value: unknown) { + return typeof value === 'string' ? value : '' +} + +/** + * Add response headers from config.json + */ +export function addResponseHeaders (res: Response) { + Object.entries(getConfigOption('ipp.responseHeaders', {}) as { [key: string]: string }) + .forEach(([header, value]) => { + res.set(header, value) + }) +} diff --git a/app/src/immich.ts b/app/src/immich.ts index 9c523d6..d2c8757 100644 --- a/app/src/immich.ts +++ b/app/src/immich.ts @@ -1,6 +1,6 @@ import { Asset, AssetType, ImageSize, IncomingShareRequest, SharedLink, SharedLinkResult } from './types' import dayjs from 'dayjs' -import { log } from './index' +import { getConfigOption, log } from './functions' import render from './render' import { Response } from 'express-serve-static-core' import { encrypt } from './encrypt' @@ -70,12 +70,13 @@ class Immich { // This is an individual item (not a gallery) log('Serving link ' + request.key) const asset = link.assets[0] - if (asset.type === AssetType.image) { - // For photos, output the image directly + if (asset.type === AssetType.image && !getConfigOption('ipp.singleImageGallery')) { + // For photos, output the image directly unless configured to show a gallery await render.assetBuffer(res, link.assets[0], request.size) - } else if (asset.type === AssetType.video) { - // For videos, show the video as a web player - await render.gallery(res, link, 1) + } else { + // Show a gallery page + const openItem = getConfigOption('ipp.singleItemAutoOpen', true) ? 1 : 0 + await render.gallery(res, link, openItem) } } else { // Multiple images - render as a gallery diff --git a/app/src/index.ts b/app/src/index.ts index cf4c60c..03e9133 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -2,19 +2,19 @@ import express from 'express' import immich from './immich' import render from './render' import dayjs from 'dayjs' -import { AssetType, ImageSize } from './types' -import { Request } from 'express-serve-static-core' +import { AssetType } from './types' import { decrypt } from './encrypt' +import { log, getSize, toString, addResponseHeaders } from './functions' require('dotenv').config() const app = express() // Add the EJS view engine, to render the gallery page app.set('view engine', 'ejs') -// Serve static assets from the /public folder -app.use(express.static('public')) // For parsing the password unlock form app.use(express.json()) +// Serve static assets from the /public folder +app.use(express.static('public', { setHeaders: addResponseHeaders })) // An incoming request for a shared link app.get('/share/:key', async (req, res) => { @@ -34,7 +34,7 @@ app.post('/unlock', async (req, res) => { // Output the buffer data for a photo or video app.get('/:type(photo|video)/:key/:id', async (req, res) => { - res.set('Cache-Control', 'public, max-age=' + process.env.CACHE_AGE) + addResponseHeaders(res) // Check for valid key and ID if (immich.isKey(req.params.key) && immich.isId(req.params.id)) { let password @@ -83,23 +83,6 @@ app.get('*', (req, res) => { res.status(404).send() }) -/** - * Output a console.log message with timestamp - */ -export const log = (message: string) => console.log(dayjs().format() + ' ' + message) - -/** - * Sanitise the data for an incoming query string `size` parameter - * e.g. https://example.com/share/abc...xyz?size=thumbnail - */ -const getSize = (req: Request) => { - return req.query?.size === 'thumbnail' ? ImageSize.thumbnail : ImageSize.original -} - -const toString = (value: unknown) => { - return typeof value === 'string' ? value : '' -} - app.listen(3000, () => { console.log(dayjs().format() + ' Server started') }) diff --git a/app/src/render.ts b/app/src/render.ts index 43da334..4a83e56 100644 --- a/app/src/render.ts +++ b/app/src/render.ts @@ -1,16 +1,13 @@ import immich from './immich' import { Response } from 'express-serve-static-core' import { Asset, AssetType, ImageSize, SharedLink } from './types' +import { getConfigOption } from './functions' class Render { - lgConfig = {} + lgConfig constructor () { - try { - // Import user-provided lightGallery config (if exists) - const config = require('../config.json') - if (typeof config === 'object' && config.lightGallery) this.lgConfig = config.lightGallery - } catch (e) { } + this.lgConfig = getConfigOption('lightGallery', {}) } async assetBuffer (res: Response, asset: Asset, size?: ImageSize) { @@ -61,7 +58,7 @@ class Render { items, openItem, title: this.title(share), - lgConfig: this.lgConfig + lgConfig: getConfigOption('lightGallery', {}) }) } diff --git a/docker-compose.yml b/docker-compose.yml index dc3f6ac..8033994 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,9 @@ services: container_name: immich-public-proxy restart: always ports: - - ${PORT}:3000 - env_file: .env + - "3000:3000" + environment: + - IMMICH_URL=http://localhost:2283 healthcheck: test: wget -q http://localhost:3000/healthcheck || exit 1 start_period: 10s