diff options
author | Chocobozzz <me@florianbigard.com> | 2023-06-06 15:59:51 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-06-29 10:19:33 +0200 |
commit | f162d32da098aa55f6de2367142faa166edb7c08 (patch) | |
tree | 31c6a96972994171853cb6c4e0b88b63241f8979 /server/lib/files-cache/shared | |
parent | a673d9e848e51186602548a621e05925663b98be (diff) | |
download | PeerTube-f162d32da098aa55f6de2367142faa166edb7c08.tar.gz PeerTube-f162d32da098aa55f6de2367142faa166edb7c08.tar.zst PeerTube-f162d32da098aa55f6de2367142faa166edb7c08.zip |
Support lazy download thumbnails
Diffstat (limited to 'server/lib/files-cache/shared')
-rw-r--r-- | server/lib/files-cache/shared/abstract-permanent-file-cache.ts | 119 | ||||
-rw-r--r-- | server/lib/files-cache/shared/abstract-simple-file-cache.ts | 30 | ||||
-rw-r--r-- | server/lib/files-cache/shared/index.ts | 2 |
3 files changed, 151 insertions, 0 deletions
diff --git a/server/lib/files-cache/shared/abstract-permanent-file-cache.ts b/server/lib/files-cache/shared/abstract-permanent-file-cache.ts new file mode 100644 index 000000000..22596c3eb --- /dev/null +++ b/server/lib/files-cache/shared/abstract-permanent-file-cache.ts | |||
@@ -0,0 +1,119 @@ | |||
1 | import express from 'express' | ||
2 | import { LRUCache } from 'lru-cache' | ||
3 | import { logger } from '@server/helpers/logger' | ||
4 | import { LRU_CACHE, STATIC_MAX_AGE } from '@server/initializers/constants' | ||
5 | import { downloadImageFromWorker } from '@server/lib/worker/parent-process' | ||
6 | import { HttpStatusCode } from '@shared/models' | ||
7 | import { Model } from 'sequelize' | ||
8 | |||
9 | type ImageModel = { | ||
10 | fileUrl: string | ||
11 | filename: string | ||
12 | onDisk: boolean | ||
13 | |||
14 | isOwned (): boolean | ||
15 | getPath (): string | ||
16 | |||
17 | save (): Promise<Model> | ||
18 | } | ||
19 | |||
20 | export abstract class AbstractPermanentFileCache <M extends ImageModel> { | ||
21 | // Unsafe because it can return paths that do not exist anymore | ||
22 | private readonly filenameToPathUnsafeCache = new LRUCache<string, string>({ | ||
23 | max: LRU_CACHE.FILENAME_TO_PATH_PERMANENT_FILE_CACHE.MAX_SIZE | ||
24 | }) | ||
25 | |||
26 | protected abstract getImageSize (image: M): { width: number, height: number } | ||
27 | protected abstract loadModel (filename: string): Promise<M> | ||
28 | |||
29 | constructor (private readonly directory: string) { | ||
30 | |||
31 | } | ||
32 | |||
33 | async lazyServe (options: { | ||
34 | filename: string | ||
35 | res: express.Response | ||
36 | next: express.NextFunction | ||
37 | }) { | ||
38 | const { filename, res, next } = options | ||
39 | |||
40 | if (this.filenameToPathUnsafeCache.has(filename)) { | ||
41 | return res.sendFile(this.filenameToPathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER }) | ||
42 | } | ||
43 | |||
44 | const image = await this.loadModel(filename) | ||
45 | if (!image) return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
46 | |||
47 | if (image.onDisk === false) { | ||
48 | if (!image.fileUrl) return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
49 | |||
50 | try { | ||
51 | await this.downloadRemoteFile(image) | ||
52 | } catch (err) { | ||
53 | logger.warn('Cannot process remote image %s.', image.fileUrl, { err }) | ||
54 | |||
55 | return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
56 | } | ||
57 | } | ||
58 | |||
59 | const path = image.getPath() | ||
60 | this.filenameToPathUnsafeCache.set(filename, path) | ||
61 | |||
62 | return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }, (err: any) => { | ||
63 | if (!err) return | ||
64 | |||
65 | this.onServeError({ err, image, next, filename }) | ||
66 | }) | ||
67 | } | ||
68 | |||
69 | private async downloadRemoteFile (image: M) { | ||
70 | logger.info('Download remote image %s lazily.', image.fileUrl) | ||
71 | |||
72 | await this.downloadImage({ | ||
73 | filename: image.filename, | ||
74 | fileUrl: image.fileUrl, | ||
75 | size: this.getImageSize(image) | ||
76 | }) | ||
77 | |||
78 | image.onDisk = true | ||
79 | image.save() | ||
80 | .catch(err => logger.error('Cannot save new image disk state.', { err })) | ||
81 | } | ||
82 | |||
83 | private onServeError (options: { | ||
84 | err: any | ||
85 | image: M | ||
86 | filename: string | ||
87 | next: express.NextFunction | ||
88 | }) { | ||
89 | const { err, image, filename, next } = options | ||
90 | |||
91 | // It seems this actor image is not on the disk anymore | ||
92 | if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) { | ||
93 | logger.error('Cannot lazy serve image %s.', filename, { err }) | ||
94 | |||
95 | this.filenameToPathUnsafeCache.delete(filename) | ||
96 | |||
97 | image.onDisk = false | ||
98 | image.save() | ||
99 | .catch(err => logger.error('Cannot save new image disk state.', { err })) | ||
100 | } | ||
101 | |||
102 | return next(err) | ||
103 | } | ||
104 | |||
105 | private downloadImage (options: { | ||
106 | fileUrl: string | ||
107 | filename: string | ||
108 | size: { width: number, height: number } | ||
109 | }) { | ||
110 | const downloaderOptions = { | ||
111 | url: options.fileUrl, | ||
112 | destDir: this.directory, | ||
113 | destName: options.filename, | ||
114 | size: options.size | ||
115 | } | ||
116 | |||
117 | return downloadImageFromWorker(downloaderOptions) | ||
118 | } | ||
119 | } | ||
diff --git a/server/lib/files-cache/shared/abstract-simple-file-cache.ts b/server/lib/files-cache/shared/abstract-simple-file-cache.ts new file mode 100644 index 000000000..6fab322cd --- /dev/null +++ b/server/lib/files-cache/shared/abstract-simple-file-cache.ts | |||
@@ -0,0 +1,30 @@ | |||
1 | import { remove } from 'fs-extra' | ||
2 | import { logger } from '../../../helpers/logger' | ||
3 | import memoizee from 'memoizee' | ||
4 | |||
5 | type GetFilePathResult = { isOwned: boolean, path: string, downloadName?: string } | undefined | ||
6 | |||
7 | export abstract class AbstractSimpleFileCache <T> { | ||
8 | |||
9 | getFilePath: (params: T) => Promise<GetFilePathResult> | ||
10 | |||
11 | abstract getFilePathImpl (params: T): Promise<GetFilePathResult> | ||
12 | |||
13 | // Load and save the remote file, then return the local path from filesystem | ||
14 | protected abstract loadRemoteFile (key: string): Promise<GetFilePathResult> | ||
15 | |||
16 | init (max: number, maxAge: number) { | ||
17 | this.getFilePath = memoizee(this.getFilePathImpl, { | ||
18 | maxAge, | ||
19 | max, | ||
20 | promise: true, | ||
21 | dispose: (result?: GetFilePathResult) => { | ||
22 | if (result && result.isOwned !== true) { | ||
23 | remove(result.path) | ||
24 | .then(() => logger.debug('%s removed from %s', result.path, this.constructor.name)) | ||
25 | .catch(err => logger.error('Cannot remove %s from cache %s.', result.path, this.constructor.name, { err })) | ||
26 | } | ||
27 | } | ||
28 | }) | ||
29 | } | ||
30 | } | ||
diff --git a/server/lib/files-cache/shared/index.ts b/server/lib/files-cache/shared/index.ts new file mode 100644 index 000000000..61c4aacc7 --- /dev/null +++ b/server/lib/files-cache/shared/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './abstract-permanent-file-cache' | ||
2 | export * from './abstract-simple-file-cache' | ||