aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/files-cache/shared
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-06-06 15:59:51 +0200
committerChocobozzz <me@florianbigard.com>2023-06-29 10:19:33 +0200
commitf162d32da098aa55f6de2367142faa166edb7c08 (patch)
tree31c6a96972994171853cb6c4e0b88b63241f8979 /server/lib/files-cache/shared
parenta673d9e848e51186602548a621e05925663b98be (diff)
downloadPeerTube-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.ts119
-rw-r--r--server/lib/files-cache/shared/abstract-simple-file-cache.ts30
-rw-r--r--server/lib/files-cache/shared/index.ts2
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 @@
1import express from 'express'
2import { LRUCache } from 'lru-cache'
3import { logger } from '@server/helpers/logger'
4import { LRU_CACHE, STATIC_MAX_AGE } from '@server/initializers/constants'
5import { downloadImageFromWorker } from '@server/lib/worker/parent-process'
6import { HttpStatusCode } from '@shared/models'
7import { Model } from 'sequelize'
8
9type ImageModel = {
10 fileUrl: string
11 filename: string
12 onDisk: boolean
13
14 isOwned (): boolean
15 getPath (): string
16
17 save (): Promise<Model>
18}
19
20export 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 @@
1import { remove } from 'fs-extra'
2import { logger } from '../../../helpers/logger'
3import memoizee from 'memoizee'
4
5type GetFilePathResult = { isOwned: boolean, path: string, downloadName?: string } | undefined
6
7export 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 @@
1export * from './abstract-permanent-file-cache'
2export * from './abstract-simple-file-cache'