immich-public-proxy/app/src/index.ts
2024-11-04 11:15:00 +01:00

106 lines
3.1 KiB
TypeScript

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 { decrypt } from './encrypt'
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())
// An incoming request for a shared link
app.get('/share/:key', async (req, res) => {
await immich.handleShareRequest({
key: req.params.key,
size: getSize(req)
}, res)
})
// Receive an unlock request from the password page
app.post('/unlock', async (req, res) => {
await immich.handleShareRequest({
key: toString(req.body.key),
password: toString(req.body.password)
}, 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)
// Check for valid key and ID
if (immich.isKey(req.params.key) && immich.isId(req.params.id)) {
let password
// Validate the password payload, if one was provided
if (req.query?.cr && req.query?.iv) {
try {
const payload = JSON.parse(decrypt({
iv: toString(req.query.iv),
cr: toString(req.query.cr)
}))
if (payload?.expires && dayjs(payload.expires) > dayjs()) {
password = payload.password
} else {
log(`Attempted to load assets from ${req.params.key} with an expired decryption token`)
}
} catch (e) { }
}
// Check if the key is a valid share link
const sharedLink = (await immich.getShareByKey(req.params.key, password))?.link
if (sharedLink?.assets.length) {
// Check that the requested asset exists in this share
const asset = sharedLink.assets.find(x => x.id === req.params.id)
if (asset) {
asset.type = req.params.type === 'video' ? AssetType.video : AssetType.image
render.assetBuffer(res, asset, getSize(req)).then()
return
}
}
}
log('No asset found for ' + req.path)
res.status(404).send()
})
// Healthcheck
app.get('/healthcheck', async (_req, res) => {
if (await immich.accessible()) {
res.send('ok')
} else {
res.status(502).send()
}
})
// Send a 404 for all other routes
app.get('*', (req, res) => {
log('Invalid route ' + req.path)
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')
})