aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/files-cache/shared/abstract-permanent-file-cache.ts
blob: 22596c3ebb17e36be18a25c5161c3bf17ddba998 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import express from 'express'
import { LRUCache } from 'lru-cache'
import { logger } from '@server/helpers/logger'
import { LRU_CACHE, STATIC_MAX_AGE } from '@server/initializers/constants'
import { downloadImageFromWorker } from '@server/lib/worker/parent-process'
import { HttpStatusCode } from '@shared/models'
import { Model } from 'sequelize'

type ImageModel = {
  fileUrl: string
  filename: string
  onDisk: boolean

  isOwned (): boolean
  getPath (): string

  save (): Promise<Model>
}

export abstract class AbstractPermanentFileCache <M extends ImageModel> {
  // Unsafe because it can return paths that do not exist anymore
  private readonly filenameToPathUnsafeCache = new LRUCache<string, string>({
    max: LRU_CACHE.FILENAME_TO_PATH_PERMANENT_FILE_CACHE.MAX_SIZE
  })

  protected abstract getImageSize (image: M): { width: number, height: number }
  protected abstract loadModel (filename: string): Promise<M>

  constructor (private readonly directory: string) {

  }

  async lazyServe (options: {
    filename: string
    res: express.Response
    next: express.NextFunction
  }) {
    const { filename, res, next } = options

    if (this.filenameToPathUnsafeCache.has(filename)) {
      return res.sendFile(this.filenameToPathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER })
    }

    const image = await this.loadModel(filename)
    if (!image) return res.status(HttpStatusCode.NOT_FOUND_404).end()

    if (image.onDisk === false) {
      if (!image.fileUrl) return res.status(HttpStatusCode.NOT_FOUND_404).end()

      try {
        await this.downloadRemoteFile(image)
      } catch (err) {
        logger.warn('Cannot process remote image %s.', image.fileUrl, { err })

        return res.status(HttpStatusCode.NOT_FOUND_404).end()
      }
    }

    const path = image.getPath()
    this.filenameToPathUnsafeCache.set(filename, path)

    return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }, (err: any) => {
      if (!err) return

      this.onServeError({ err, image, next, filename })
    })
  }

  private async downloadRemoteFile (image: M) {
    logger.info('Download remote image %s lazily.', image.fileUrl)

    await this.downloadImage({
      filename: image.filename,
      fileUrl: image.fileUrl,
      size: this.getImageSize(image)
    })

    image.onDisk = true
    image.save()
      .catch(err => logger.error('Cannot save new image disk state.', { err }))
  }

  private onServeError (options: {
    err: any
    image: M
    filename: string
    next: express.NextFunction
  }) {
    const { err, image, filename, next } = options

    // It seems this actor image is not on the disk anymore
    if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) {
      logger.error('Cannot lazy serve image %s.', filename, { err })

      this.filenameToPathUnsafeCache.delete(filename)

      image.onDisk = false
      image.save()
        .catch(err => logger.error('Cannot save new image disk state.', { err }))
    }

    return next(err)
  }

  private downloadImage (options: {
    fileUrl: string
    filename: string
    size: { width: number, height: number }
  }) {
    const downloaderOptions = {
      url: options.fileUrl,
      destDir: this.directory,
      destName: options.filename,
      size: options.size
    }

    return downloadImageFromWorker(downloaderOptions)
  }
}