From d74d29ad9e35929491cf37223398d2535ab23de0 Mon Sep 17 00:00:00 2001
From: Chocobozzz <me@florianbigard.com>
Date: Tue, 19 Mar 2019 14:23:17 +0100
Subject: Limit user tokens cache

---
 .../abstract-video-static-file-cache.ts            | 52 +++++++++++++++++++++
 server/lib/files-cache/actor-follow-score-cache.ts | 46 +++++++++++++++++++
 server/lib/files-cache/index.ts                    |  3 ++
 server/lib/files-cache/videos-caption-cache.ts     | 53 ++++++++++++++++++++++
 server/lib/files-cache/videos-preview-cache.ts     | 42 +++++++++++++++++
 5 files changed, 196 insertions(+)
 create mode 100644 server/lib/files-cache/abstract-video-static-file-cache.ts
 create mode 100644 server/lib/files-cache/actor-follow-score-cache.ts
 create mode 100644 server/lib/files-cache/index.ts
 create mode 100644 server/lib/files-cache/videos-caption-cache.ts
 create mode 100644 server/lib/files-cache/videos-preview-cache.ts

(limited to 'server/lib/files-cache')

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 @@
+import * as AsyncLRU from 'async-lru'
+import { createWriteStream, remove } from 'fs-extra'
+import { logger } from '../../helpers/logger'
+import { VideoModel } from '../../models/video/video'
+import { fetchRemoteVideoStaticFile } from '../activitypub'
+
+export abstract class AbstractVideoStaticFileCache <T> {
+
+  protected lru
+
+  abstract getFilePath (params: T): Promise<string>
+
+  // Load and save the remote file, then return the local path from filesystem
+  protected abstract loadRemoteFile (key: string): Promise<string>
+
+  init (max: number, maxAge: number) {
+    this.lru = new AsyncLRU({
+      max,
+      maxAge,
+      load: (key, cb) => {
+        this.loadRemoteFile(key)
+          .then(res => cb(null, res))
+          .catch(err => cb(err))
+      }
+    })
+
+    this.lru.on('evict', (obj: { key: string, value: string }) => {
+      remove(obj.value)
+        .then(() => logger.debug('%s evicted from %s', obj.value, this.constructor.name))
+    })
+  }
+
+  protected loadFromLRU (key: string) {
+    return new Promise<string>((res, rej) => {
+      this.lru.get(key, (err, value) => {
+        err ? rej(err) : res(value)
+      })
+    })
+  }
+
+  protected saveRemoteVideoFileAndReturnPath (video: VideoModel, remoteStaticPath: string, destPath: string) {
+    return new Promise<string>((res, rej) => {
+      const req = fetchRemoteVideoStaticFile(video, remoteStaticPath, rej)
+
+      const stream = createWriteStream(destPath)
+
+      req.pipe(stream)
+         .on('error', (err) => rej(err))
+         .on('finish', () => res(destPath))
+    })
+  }
+}
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 @@
+import { ACTOR_FOLLOW_SCORE } from '../../initializers'
+import { logger } from '../../helpers/logger'
+
+// Cache follows scores, instead of writing them too often in database
+// Keep data in memory, we don't really need Redis here as we don't really care to loose some scores
+class ActorFollowScoreCache {
+
+  private static instance: ActorFollowScoreCache
+  private pendingFollowsScore: { [ url: string ]: number } = {}
+
+  private constructor () {}
+
+  static get Instance () {
+    return this.instance || (this.instance = new this())
+  }
+
+  updateActorFollowsScore (goodInboxes: string[], badInboxes: string[]) {
+    if (goodInboxes.length === 0 && badInboxes.length === 0) return
+
+    logger.info('Updating %d good actor follows and %d bad actor follows scores in cache.', goodInboxes.length, badInboxes.length)
+
+    for (const goodInbox of goodInboxes) {
+      if (this.pendingFollowsScore[goodInbox] === undefined) this.pendingFollowsScore[goodInbox] = 0
+
+      this.pendingFollowsScore[goodInbox] += ACTOR_FOLLOW_SCORE.BONUS
+    }
+
+    for (const badInbox of badInboxes) {
+      if (this.pendingFollowsScore[badInbox] === undefined) this.pendingFollowsScore[badInbox] = 0
+
+      this.pendingFollowsScore[badInbox] += ACTOR_FOLLOW_SCORE.PENALTY
+    }
+  }
+
+  getPendingFollowsScoreCopy () {
+    return this.pendingFollowsScore
+  }
+
+  clearPendingFollowsScore () {
+    this.pendingFollowsScore = {}
+  }
+}
+
+export {
+  ActorFollowScoreCache
+}
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 @@
+export * from './actor-follow-score-cache'
+export * from './videos-preview-cache'
+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 @@
+import { join } from 'path'
+import { FILES_CACHE, CONFIG } from '../../initializers'
+import { VideoModel } from '../../models/video/video'
+import { VideoCaptionModel } from '../../models/video/video-caption'
+import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
+
+type GetPathParam = { videoId: string, language: string }
+
+class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
+
+  private static readonly KEY_DELIMITER = '%'
+  private static instance: VideosCaptionCache
+
+  private constructor () {
+    super()
+  }
+
+  static get Instance () {
+    return this.instance || (this.instance = new this())
+  }
+
+  async getFilePath (params: GetPathParam) {
+    const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(params.videoId, params.language)
+    if (!videoCaption) return undefined
+
+    if (videoCaption.isOwned()) return join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.getCaptionName())
+
+    const key = params.videoId + VideosCaptionCache.KEY_DELIMITER + params.language
+    return this.loadFromLRU(key)
+  }
+
+  protected async loadRemoteFile (key: string) {
+    const [ videoId, language ] = key.split(VideosCaptionCache.KEY_DELIMITER)
+
+    const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(videoId, language)
+    if (!videoCaption) return undefined
+
+    if (videoCaption.isOwned()) throw new Error('Cannot load remote caption of owned video.')
+
+    // Used to fetch the path
+    const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
+    if (!video) return undefined
+
+    const remoteStaticPath = videoCaption.getCaptionStaticPath()
+    const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName())
+
+    return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath)
+  }
+}
+
+export {
+  VideosCaptionCache
+}
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 @@
+import { join } from 'path'
+import { FILES_CACHE, CONFIG, STATIC_PATHS } from '../../initializers'
+import { VideoModel } from '../../models/video/video'
+import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
+
+class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
+
+  private static instance: VideosPreviewCache
+
+  private constructor () {
+    super()
+  }
+
+  static get Instance () {
+    return this.instance || (this.instance = new this())
+  }
+
+  async getFilePath (videoUUID: string) {
+    const video = await VideoModel.loadByUUIDWithFile(videoUUID)
+    if (!video) return undefined
+
+    if (video.isOwned()) return join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName())
+
+    return this.loadFromLRU(videoUUID)
+  }
+
+  protected async loadRemoteFile (key: string) {
+    const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(key)
+    if (!video) return undefined
+
+    if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.')
+
+    const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreviewName())
+    const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, video.getPreviewName())
+
+    return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath)
+  }
+}
+
+export {
+  VideosPreviewCache
+}
-- 
cgit v1.2.3