aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/files-cache/shared
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-07-31 14:34:36 +0200
committerChocobozzz <me@florianbigard.com>2023-08-11 15:02:33 +0200
commit3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch)
treee4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/lib/files-cache/shared
parent04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff)
downloadPeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'server/lib/files-cache/shared')
-rw-r--r--server/lib/files-cache/shared/abstract-permanent-file-cache.ts132
-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, 0 insertions, 164 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
deleted file mode 100644
index f990e9872..000000000
--- a/server/lib/files-cache/shared/abstract-permanent-file-cache.ts
+++ /dev/null
@@ -1,132 +0,0 @@
1import express from 'express'
2import { LRUCache } from 'lru-cache'
3import { Model } from 'sequelize'
4import { logger } from '@server/helpers/logger'
5import { CachePromise } from '@server/helpers/promise-cache'
6import { LRU_CACHE, STATIC_MAX_AGE } from '@server/initializers/constants'
7import { downloadImageFromWorker } from '@server/lib/worker/parent-process'
8import { HttpStatusCode } from '@shared/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}
diff --git a/server/lib/files-cache/shared/abstract-simple-file-cache.ts b/server/lib/files-cache/shared/abstract-simple-file-cache.ts
deleted file mode 100644
index 6fab322cd..000000000
--- a/server/lib/files-cache/shared/abstract-simple-file-cache.ts
+++ /dev/null
@@ -1,30 +0,0 @@
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
deleted file mode 100644
index 61c4aacc7..000000000
--- a/server/lib/files-cache/shared/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
1export * from './abstract-permanent-file-cache'
2export * from './abstract-simple-file-cache'