diff options
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.ts | 132 |
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 @@ | |||
1 | import express from 'express' | ||
2 | import { LRUCache } from 'lru-cache' | ||
3 | import { Model } from 'sequelize' | ||
4 | import { logger } from '@server/helpers/logger.js' | ||
5 | import { CachePromise } from '@server/helpers/promise-cache.js' | ||
6 | import { LRU_CACHE, STATIC_MAX_AGE } from '@server/initializers/constants.js' | ||
7 | import { downloadImageFromWorker } from '@server/lib/worker/parent-process.js' | ||
8 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
9 | |||
10 | type ImageModel = { | ||
11 | fileUrl: string | ||
12 | filename: string | ||
13 | onDisk: boolean | ||
14 | |||
15 | isOwned (): boolean | ||
16 | getPath (): string | ||
17 | |||
18 | save (): Promise<Model> | ||
19 | } | ||
20 | |||
21 | export 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 | } | ||