// page.js // Copyright (C) 2022,2023 DTP Technologies, LLC // All Rights Reserved 'use strict'; import fs from 'node:fs'; import express from 'express'; import mongoose from 'mongoose'; import { SiteController, SiteError } from '../../lib/site-lib.js'; export default class ImageController extends SiteController { static get name ( ) { return 'ImageController'; } static get slug ( ) { return 'image'; } constructor (dtp) { super(dtp, ImageController); } async start ( ) { const { dtp } = this; const { limiter: limiterService } = dtp.services; const limiterConfig = limiterService.config.image; const router = express.Router(); dtp.app.use('/image', router); const imageUpload = this.createMulter(ImageController.slug, { limits: { fileSize: 1024 * 1000 * 5, }, }); router.use(async (req, res, next) => { res.locals.currentView = 'image'; return next(); }); router.param('imageId', this.populateImage.bind(this)); router.post('/', limiterService.create(limiterConfig.postCreateImage), imageUpload.single('file'), this.postCreateImage.bind(this), ); router.get('/proxy', limiterService.create(limiterConfig.getProxyImage), this.getProxyImage.bind(this), ); router.get('/:imageId', limiterService.create(limiterConfig.getImage), this.getHostCacheImage.bind(this), // this.getImage.bind(this), ); router.delete('/:imageId', limiterService.create(limiterConfig.deleteImage), this.deleteImage.bind(this), ); } async populateImage (req, res, next, imageId) { const { image: imageService } = this.dtp.services; try { res.locals.imageId = mongoose.Types.ObjectId.createFromHexString(imageId); res.locals.image = await imageService.getImageById(res.locals.imageId); if (!res.locals.image) { throw new SiteError(404, 'Image not found'); } return next(); } catch (error) { this.log.error('failed to populate image', { error }); return next(error); } } async postCreateImage (req, res, next) { const { image: imageService } = this.dtp.services; try { res.locals.image = await imageService.create(req.user, req.body, req.file); res.status(200).json({ success: true, imageId: res.locals.image._id.toString(), }); } catch (error) { this.log.error('failed to create image', { error }); return next(error); } } async getProxyImage (req, res) { const { hostCache: hostCacheService } = this.dtp.services; try { if (!req.query || !req.query.url) { throw new SiteError(400, 'Missing url parameter'); } const fileInfo = await hostCacheService.fetchUrl(req.query.url); const stream = fs.createReadStream(fileInfo.file.path); res.setHeader('Cache-Control', 'public, maxage=86400, s-maxage=86400, immutable'); res.setHeader('Content-Type', fileInfo.file.meta.contentType); res.setHeader('Content-Length', fileInfo.file.stats.size); res.status(200); stream.pipe(res); } catch (error) { this.log.error('failed to fetch web resource', { url: req.query.url, error }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async getHostCacheImage (req, res) { const { hostCache: hostCacheService } = this.dtp.services; const { image } = res.locals; try { const fileInfo = await hostCacheService.getFile(image.file.bucket, image.file.key); const stream = fs.createReadStream(fileInfo.file.path); res.setHeader('Cache-Control', 'public, maxage=86400, s-maxage=86400, immutable'); res.setHeader('Content-Type', image.type); res.setHeader('Content-Length', fileInfo.file.stats.size); res.status(200); stream.pipe(res); } catch (error) { this.log.error('failed to fetch image', { image, error }); return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async getImage (req, res) { const { minio: minioService } = this.dtp.services; try { const stream = await minioService.openDownloadStream({ bucket: res.locals.image.file.bucket, key: res.locals.image.file.key }); res.setHeader('Cache-Control', 'public, maxage=86400, s-maxage=86400, immutable'); res.setHeader('Content-Type', res.locals.image.type); res.setHeader('Content-Length', res.locals.image.size); res.status(200); stream.pipe(res); } catch (error) { return res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } async deleteImage (req, res) { const { image: imageService } = this.dtp.services; try { if (!req.user) { throw new SiteError(403, 'Must be logged in to delete images you own'); } if (!req.user._id.equals(res.locals.image.owner._id)) { throw new SiteError(403, 'You are not the owner of the requested image'); } this.log.debug('deleting image', { imageId: res.locals.image._id }); await imageService.deleteImage(res.locals.image); res.status(200).json({ success: true }); } catch (error) { this.log.error('failed to delete image', { error }); res.status(error.statusCode || 500).json({ success: false, message: error.message, }); } } }