aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/server/lib/files-cache/shared/abstract-permanent-file-cache.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/server/lib/files-cache/shared/abstract-permanent-file-cache.ts')
-rw-r--r--server/server/lib/files-cache/shared/abstract-permanent-file-cache.ts132
1 files changed, 132 insertions, 0 deletions
diff --git a/server/server/lib/files-cache/shared/abstract-permanent-file-cache.ts b/server/server/lib/files-cache/shared/abstract-permanent-file-cache.ts
new file mode 100644
index 000000000..f9e9878a8
--- /dev/null
+++ b/server/server/lib/files-cache/shared/abstract-permanent-file-cache.ts
@@ -0,0 +1,132 @@
1import express from 'express'
2import { LRUCache } from 'lru-cache'
3import { Model } from 'sequelize'
4import { logger } from '@server/helpers/logger.js'
5import { CachePromise } from '@server/helpers/promise-cache.js'
6import { LRU_CACHE, STATIC_MAX_AGE } from '@server/initializers/constants.js'
7import { downloadImageFromWorker } from '@server/lib/worker/parent-process.js'
8import { HttpStatusCode } from '@peertube/peertube-models'
9
10type ImageModel = {
11 fileUrl: string
12 filename: string
13 onDisk: boolean
14
15 isOwned (): boolean
16 getPath (): string
17
18 save (): Promise<Model>
19}
20
21export abstract class AbstractPermanentFileCache <M extends ImageModel> {
22 // Unsafe because it can return paths that do not exist anymore
23 private readonly filenameToPathUnsafeCache = new LRUCache<string, string>({
24 max: LRU_CACHE.FILENAME_TO_PATH_PERMANENT_FILE_CACHE.MAX_SIZE
25 })
26
27 protected abstract getImageSize (image: M): { width: number, height: number }
28 protected abstract loadModel (filename: string): Promise<M>
29
30 constructor (private readonly directory: string) {
31
32 }
33
34 async lazyServe (options: {
35 filename: string
36 res: express.Response
37 next: express.NextFunction
38 }) {
39 const { filename, res, next } = options
40
41 if (this.filenameToPathUnsafeCache.has(filename)) {
42 return res.sendFile(this.filenameToPathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER })
43 }
44
45 const image = await this.lazyLoadIfNeeded(filename)
46 if (!image) return res.status(HttpStatusCode.NOT_FOUND_404).end()
47
48 const path = image.getPath()
49 this.filenameToPathUnsafeCache.set(filename, path)
50
51 return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }, (err: any) => {
52 if (!err) return
53
54 this.onServeError({ err, image, next, filename })
55 })
56 }
57
58 @CachePromise({
59 keyBuilder: filename => filename
60 })
61 private async lazyLoadIfNeeded (filename: string) {
62 const image = await this.loadModel(filename)
63 if (!image) return undefined
64
65 if (image.onDisk === false) {
66 if (!image.fileUrl) return undefined
67
68 try {
69 await this.downloadRemoteFile(image)
70 } catch (err) {
71 logger.warn('Cannot process remote image %s.', image.fileUrl, { err })
72
73 return undefined
74 }
75 }
76
77 return image
78 }
79
80 async downloadRemoteFile (image: M) {
81 logger.info('Download remote image %s lazily.', image.fileUrl)
82
83 const destination = await this.downloadImage({
84 filename: image.filename,
85 fileUrl: image.fileUrl,
86 size: this.getImageSize(image)
87 })
88
89 image.onDisk = true
90 image.save()
91 .catch(err => logger.error('Cannot save new image disk state.', { err }))
92
93 return destination
94 }
95
96 private onServeError (options: {
97 err: any
98 image: M
99 filename: string
100 next: express.NextFunction
101 }) {
102 const { err, image, filename, next } = options
103
104 // It seems this actor image is not on the disk anymore
105 if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) {
106 logger.error('Cannot lazy serve image %s.', filename, { err })
107
108 this.filenameToPathUnsafeCache.delete(filename)
109
110 image.onDisk = false
111 image.save()
112 .catch(err => logger.error('Cannot save new image disk state.', { err }))
113 }
114
115 return next(err)
116 }
117
118 private downloadImage (options: {
119 fileUrl: string
120 filename: string
121 size: { width: number, height: number }
122 }) {
123 const downloaderOptions = {
124 url: options.fileUrl,
125 destDir: this.directory,
126 destName: options.filename,
127 size: options.size
128 }
129
130 return downloadImageFromWorker(downloaderOptions)
131 }
132}