diff options
Diffstat (limited to 'server/lib/files-cache')
-rw-r--r-- | server/lib/files-cache/abstract-video-static-file-cache.ts | 52 | ||||
-rw-r--r-- | server/lib/files-cache/actor-follow-score-cache.ts | 46 | ||||
-rw-r--r-- | server/lib/files-cache/index.ts | 3 | ||||
-rw-r--r-- | server/lib/files-cache/videos-caption-cache.ts | 53 | ||||
-rw-r--r-- | server/lib/files-cache/videos-preview-cache.ts | 42 |
5 files changed, 196 insertions, 0 deletions
diff --git a/server/lib/files-cache/abstract-video-static-file-cache.ts b/server/lib/files-cache/abstract-video-static-file-cache.ts new file mode 100644 index 000000000..7512f2b9d --- /dev/null +++ b/server/lib/files-cache/abstract-video-static-file-cache.ts | |||
@@ -0,0 +1,52 @@ | |||
1 | import * as AsyncLRU from 'async-lru' | ||
2 | import { createWriteStream, remove } from 'fs-extra' | ||
3 | import { logger } from '../../helpers/logger' | ||
4 | import { VideoModel } from '../../models/video/video' | ||
5 | import { fetchRemoteVideoStaticFile } from '../activitypub' | ||
6 | |||
7 | export abstract class AbstractVideoStaticFileCache <T> { | ||
8 | |||
9 | protected lru | ||
10 | |||
11 | abstract getFilePath (params: T): Promise<string> | ||
12 | |||
13 | // Load and save the remote file, then return the local path from filesystem | ||
14 | protected abstract loadRemoteFile (key: string): Promise<string> | ||
15 | |||
16 | init (max: number, maxAge: number) { | ||
17 | this.lru = new AsyncLRU({ | ||
18 | max, | ||
19 | maxAge, | ||
20 | load: (key, cb) => { | ||
21 | this.loadRemoteFile(key) | ||
22 | .then(res => cb(null, res)) | ||
23 | .catch(err => cb(err)) | ||
24 | } | ||
25 | }) | ||
26 | |||
27 | this.lru.on('evict', (obj: { key: string, value: string }) => { | ||
28 | remove(obj.value) | ||
29 | .then(() => logger.debug('%s evicted from %s', obj.value, this.constructor.name)) | ||
30 | }) | ||
31 | } | ||
32 | |||
33 | protected loadFromLRU (key: string) { | ||
34 | return new Promise<string>((res, rej) => { | ||
35 | this.lru.get(key, (err, value) => { | ||
36 | err ? rej(err) : res(value) | ||
37 | }) | ||
38 | }) | ||
39 | } | ||
40 | |||
41 | protected saveRemoteVideoFileAndReturnPath (video: VideoModel, remoteStaticPath: string, destPath: string) { | ||
42 | return new Promise<string>((res, rej) => { | ||
43 | const req = fetchRemoteVideoStaticFile(video, remoteStaticPath, rej) | ||
44 | |||
45 | const stream = createWriteStream(destPath) | ||
46 | |||
47 | req.pipe(stream) | ||
48 | .on('error', (err) => rej(err)) | ||
49 | .on('finish', () => res(destPath)) | ||
50 | }) | ||
51 | } | ||
52 | } | ||
diff --git a/server/lib/files-cache/actor-follow-score-cache.ts b/server/lib/files-cache/actor-follow-score-cache.ts new file mode 100644 index 000000000..d070bde09 --- /dev/null +++ b/server/lib/files-cache/actor-follow-score-cache.ts | |||
@@ -0,0 +1,46 @@ | |||
1 | import { ACTOR_FOLLOW_SCORE } from '../../initializers' | ||
2 | import { logger } from '../../helpers/logger' | ||
3 | |||
4 | // Cache follows scores, instead of writing them too often in database | ||
5 | // Keep data in memory, we don't really need Redis here as we don't really care to loose some scores | ||
6 | class ActorFollowScoreCache { | ||
7 | |||
8 | private static instance: ActorFollowScoreCache | ||
9 | private pendingFollowsScore: { [ url: string ]: number } = {} | ||
10 | |||
11 | private constructor () {} | ||
12 | |||
13 | static get Instance () { | ||
14 | return this.instance || (this.instance = new this()) | ||
15 | } | ||
16 | |||
17 | updateActorFollowsScore (goodInboxes: string[], badInboxes: string[]) { | ||
18 | if (goodInboxes.length === 0 && badInboxes.length === 0) return | ||
19 | |||
20 | logger.info('Updating %d good actor follows and %d bad actor follows scores in cache.', goodInboxes.length, badInboxes.length) | ||
21 | |||
22 | for (const goodInbox of goodInboxes) { | ||
23 | if (this.pendingFollowsScore[goodInbox] === undefined) this.pendingFollowsScore[goodInbox] = 0 | ||
24 | |||
25 | this.pendingFollowsScore[goodInbox] += ACTOR_FOLLOW_SCORE.BONUS | ||
26 | } | ||
27 | |||
28 | for (const badInbox of badInboxes) { | ||
29 | if (this.pendingFollowsScore[badInbox] === undefined) this.pendingFollowsScore[badInbox] = 0 | ||
30 | |||
31 | this.pendingFollowsScore[badInbox] += ACTOR_FOLLOW_SCORE.PENALTY | ||
32 | } | ||
33 | } | ||
34 | |||
35 | getPendingFollowsScoreCopy () { | ||
36 | return this.pendingFollowsScore | ||
37 | } | ||
38 | |||
39 | clearPendingFollowsScore () { | ||
40 | this.pendingFollowsScore = {} | ||
41 | } | ||
42 | } | ||
43 | |||
44 | export { | ||
45 | ActorFollowScoreCache | ||
46 | } | ||
diff --git a/server/lib/files-cache/index.ts b/server/lib/files-cache/index.ts new file mode 100644 index 000000000..e921d04a7 --- /dev/null +++ b/server/lib/files-cache/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './actor-follow-score-cache' | ||
2 | export * from './videos-preview-cache' | ||
3 | export * from './videos-caption-cache' | ||
diff --git a/server/lib/files-cache/videos-caption-cache.ts b/server/lib/files-cache/videos-caption-cache.ts new file mode 100644 index 000000000..fe5b441af --- /dev/null +++ b/server/lib/files-cache/videos-caption-cache.ts | |||
@@ -0,0 +1,53 @@ | |||
1 | import { join } from 'path' | ||
2 | import { FILES_CACHE, CONFIG } from '../../initializers' | ||
3 | import { VideoModel } from '../../models/video/video' | ||
4 | import { VideoCaptionModel } from '../../models/video/video-caption' | ||
5 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' | ||
6 | |||
7 | type GetPathParam = { videoId: string, language: string } | ||
8 | |||
9 | class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> { | ||
10 | |||
11 | private static readonly KEY_DELIMITER = '%' | ||
12 | private static instance: VideosCaptionCache | ||
13 | |||
14 | private constructor () { | ||
15 | super() | ||
16 | } | ||
17 | |||
18 | static get Instance () { | ||
19 | return this.instance || (this.instance = new this()) | ||
20 | } | ||
21 | |||
22 | async getFilePath (params: GetPathParam) { | ||
23 | const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(params.videoId, params.language) | ||
24 | if (!videoCaption) return undefined | ||
25 | |||
26 | if (videoCaption.isOwned()) return join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.getCaptionName()) | ||
27 | |||
28 | const key = params.videoId + VideosCaptionCache.KEY_DELIMITER + params.language | ||
29 | return this.loadFromLRU(key) | ||
30 | } | ||
31 | |||
32 | protected async loadRemoteFile (key: string) { | ||
33 | const [ videoId, language ] = key.split(VideosCaptionCache.KEY_DELIMITER) | ||
34 | |||
35 | const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(videoId, language) | ||
36 | if (!videoCaption) return undefined | ||
37 | |||
38 | if (videoCaption.isOwned()) throw new Error('Cannot load remote caption of owned video.') | ||
39 | |||
40 | // Used to fetch the path | ||
41 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | ||
42 | if (!video) return undefined | ||
43 | |||
44 | const remoteStaticPath = videoCaption.getCaptionStaticPath() | ||
45 | const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName()) | ||
46 | |||
47 | return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) | ||
48 | } | ||
49 | } | ||
50 | |||
51 | export { | ||
52 | VideosCaptionCache | ||
53 | } | ||
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts new file mode 100644 index 000000000..01cd3647e --- /dev/null +++ b/server/lib/files-cache/videos-preview-cache.ts | |||
@@ -0,0 +1,42 @@ | |||
1 | import { join } from 'path' | ||
2 | import { FILES_CACHE, CONFIG, STATIC_PATHS } from '../../initializers' | ||
3 | import { VideoModel } from '../../models/video/video' | ||
4 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' | ||
5 | |||
6 | class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { | ||
7 | |||
8 | private static instance: VideosPreviewCache | ||
9 | |||
10 | private constructor () { | ||
11 | super() | ||
12 | } | ||
13 | |||
14 | static get Instance () { | ||
15 | return this.instance || (this.instance = new this()) | ||
16 | } | ||
17 | |||
18 | async getFilePath (videoUUID: string) { | ||
19 | const video = await VideoModel.loadByUUIDWithFile(videoUUID) | ||
20 | if (!video) return undefined | ||
21 | |||
22 | if (video.isOwned()) return join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()) | ||
23 | |||
24 | return this.loadFromLRU(videoUUID) | ||
25 | } | ||
26 | |||
27 | protected async loadRemoteFile (key: string) { | ||
28 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(key) | ||
29 | if (!video) return undefined | ||
30 | |||
31 | if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.') | ||
32 | |||
33 | const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreviewName()) | ||
34 | const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, video.getPreviewName()) | ||
35 | |||
36 | return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) | ||
37 | } | ||
38 | } | ||
39 | |||
40 | export { | ||
41 | VideosPreviewCache | ||
42 | } | ||