2023-06-06 08:59:51 -05:00
|
|
|
import express from 'express'
|
|
|
|
import { LRUCache } from 'lru-cache'
|
2023-06-19 08:42:29 -05:00
|
|
|
import { Model } from 'sequelize'
|
2023-06-06 08:59:51 -05:00
|
|
|
import { logger } from '@server/helpers/logger'
|
2023-06-19 08:42:29 -05:00
|
|
|
import { CachePromise } from '@server/helpers/promise-cache'
|
2023-06-06 08:59:51 -05:00
|
|
|
import { LRU_CACHE, STATIC_MAX_AGE } from '@server/initializers/constants'
|
|
|
|
import { downloadImageFromWorker } from '@server/lib/worker/parent-process'
|
|
|
|
import { HttpStatusCode } from '@shared/models'
|
|
|
|
|
|
|
|
type ImageModel = {
|
|
|
|
fileUrl: string
|
|
|
|
filename: string
|
|
|
|
onDisk: boolean
|
|
|
|
|
|
|
|
isOwned (): boolean
|
|
|
|
getPath (): string
|
|
|
|
|
|
|
|
save (): Promise<Model>
|
|
|
|
}
|
|
|
|
|
|
|
|
export abstract class AbstractPermanentFileCache <M extends ImageModel> {
|
|
|
|
// Unsafe because it can return paths that do not exist anymore
|
|
|
|
private readonly filenameToPathUnsafeCache = new LRUCache<string, string>({
|
|
|
|
max: LRU_CACHE.FILENAME_TO_PATH_PERMANENT_FILE_CACHE.MAX_SIZE
|
|
|
|
})
|
|
|
|
|
|
|
|
protected abstract getImageSize (image: M): { width: number, height: number }
|
|
|
|
protected abstract loadModel (filename: string): Promise<M>
|
|
|
|
|
|
|
|
constructor (private readonly directory: string) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
async lazyServe (options: {
|
|
|
|
filename: string
|
|
|
|
res: express.Response
|
|
|
|
next: express.NextFunction
|
|
|
|
}) {
|
|
|
|
const { filename, res, next } = options
|
|
|
|
|
|
|
|
if (this.filenameToPathUnsafeCache.has(filename)) {
|
|
|
|
return res.sendFile(this.filenameToPathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER })
|
|
|
|
}
|
|
|
|
|
2023-06-19 08:42:29 -05:00
|
|
|
const image = await this.lazyLoadIfNeeded(filename)
|
2023-06-06 08:59:51 -05:00
|
|
|
if (!image) return res.status(HttpStatusCode.NOT_FOUND_404).end()
|
|
|
|
|
2023-06-19 08:42:29 -05:00
|
|
|
const path = image.getPath()
|
|
|
|
this.filenameToPathUnsafeCache.set(filename, path)
|
|
|
|
|
|
|
|
return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }, (err: any) => {
|
|
|
|
if (!err) return
|
|
|
|
|
|
|
|
this.onServeError({ err, image, next, filename })
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
@CachePromise({
|
|
|
|
keyBuilder: filename => filename
|
|
|
|
})
|
|
|
|
private async lazyLoadIfNeeded (filename: string) {
|
|
|
|
const image = await this.loadModel(filename)
|
|
|
|
if (!image) return undefined
|
|
|
|
|
2023-06-06 08:59:51 -05:00
|
|
|
if (image.onDisk === false) {
|
2023-06-19 08:42:29 -05:00
|
|
|
if (!image.fileUrl) return undefined
|
2023-06-06 08:59:51 -05:00
|
|
|
|
|
|
|
try {
|
|
|
|
await this.downloadRemoteFile(image)
|
|
|
|
} catch (err) {
|
|
|
|
logger.warn('Cannot process remote image %s.', image.fileUrl, { err })
|
|
|
|
|
2023-06-19 08:42:29 -05:00
|
|
|
return undefined
|
2023-06-06 08:59:51 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-19 08:42:29 -05:00
|
|
|
return image
|
2023-06-06 08:59:51 -05:00
|
|
|
}
|
|
|
|
|
2023-06-19 02:56:12 -05:00
|
|
|
async downloadRemoteFile (image: M) {
|
2023-06-06 08:59:51 -05:00
|
|
|
logger.info('Download remote image %s lazily.', image.fileUrl)
|
|
|
|
|
2023-06-19 02:56:12 -05:00
|
|
|
const destination = await this.downloadImage({
|
2023-06-06 08:59:51 -05:00
|
|
|
filename: image.filename,
|
|
|
|
fileUrl: image.fileUrl,
|
|
|
|
size: this.getImageSize(image)
|
|
|
|
})
|
|
|
|
|
|
|
|
image.onDisk = true
|
|
|
|
image.save()
|
|
|
|
.catch(err => logger.error('Cannot save new image disk state.', { err }))
|
2023-06-19 02:56:12 -05:00
|
|
|
|
|
|
|
return destination
|
2023-06-06 08:59:51 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
private onServeError (options: {
|
|
|
|
err: any
|
|
|
|
image: M
|
|
|
|
filename: string
|
|
|
|
next: express.NextFunction
|
|
|
|
}) {
|
|
|
|
const { err, image, filename, next } = options
|
|
|
|
|
|
|
|
// It seems this actor image is not on the disk anymore
|
|
|
|
if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) {
|
|
|
|
logger.error('Cannot lazy serve image %s.', filename, { err })
|
|
|
|
|
|
|
|
this.filenameToPathUnsafeCache.delete(filename)
|
|
|
|
|
|
|
|
image.onDisk = false
|
|
|
|
image.save()
|
|
|
|
.catch(err => logger.error('Cannot save new image disk state.', { err }))
|
|
|
|
}
|
|
|
|
|
|
|
|
return next(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
private downloadImage (options: {
|
|
|
|
fileUrl: string
|
|
|
|
filename: string
|
|
|
|
size: { width: number, height: number }
|
|
|
|
}) {
|
|
|
|
const downloaderOptions = {
|
|
|
|
url: options.fileUrl,
|
|
|
|
destDir: this.directory,
|
|
|
|
destName: options.filename,
|
|
|
|
size: options.size
|
|
|
|
}
|
|
|
|
|
|
|
|
return downloadImageFromWorker(downloaderOptions)
|
|
|
|
}
|
|
|
|
}
|