From f162d32da098aa55f6de2367142faa166edb7c08 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 6 Jun 2023 15:59:51 +0200 Subject: [PATCH] Support lazy download thumbnails --- server.ts | 10 +- server/controllers/api/video-playlist.ts | 8 +- server/controllers/api/videos/import.ts | 6 +- server/controllers/api/videos/live.ts | 4 +- server/controllers/api/videos/upload.ts | 4 +- server/controllers/download.ts | 4 +- server/controllers/lazy-static.ts | 100 +++++---------- server/initializers/constants.ts | 4 +- .../videos/shared/abstract-builder.ts | 8 +- .../avatar-permanent-file-cache.ts | 27 ++++ server/lib/files-cache/index.ts | 9 +- .../shared/abstract-permanent-file-cache.ts | 119 ++++++++++++++++++ .../abstract-simple-file-cache.ts} | 4 +- server/lib/files-cache/shared/index.ts | 2 + ...ts => video-captions-simple-file-cache.ts} | 12 +- ...ts => video-previews-simple-file-cache.ts} | 8 +- ...=> video-storyboards-simple-file-cache.ts} | 8 +- ...ts => video-torrents-simple-file-cache.ts} | 8 +- server/lib/job-queue/handlers/video-import.ts | 4 +- .../job-queue/handlers/video-live-ending.ts | 8 +- server/lib/local-actor.ts | 44 +------ server/lib/thumbnail.ts | 48 ++++--- server/lib/video-pre-import.ts | 4 +- server/lib/video.ts | 4 +- .../video/shared/video-table-attributes.ts | 1 + server/models/video/thumbnail.ts | 4 + support/nginx/peertube | 22 ---- 27 files changed, 272 insertions(+), 212 deletions(-) create mode 100644 server/lib/files-cache/avatar-permanent-file-cache.ts create mode 100644 server/lib/files-cache/shared/abstract-permanent-file-cache.ts rename server/lib/files-cache/{abstract-video-static-file-cache.ts => shared/abstract-simple-file-cache.ts} (90%) create mode 100644 server/lib/files-cache/shared/index.ts rename server/lib/files-cache/{videos-caption-cache.ts => video-captions-simple-file-cache.ts} (80%) rename server/lib/files-cache/{videos-preview-cache.ts => video-previews-simple-file-cache.ts} (86%) rename server/lib/files-cache/{videos-storyboard-cache.ts => video-storyboards-simple-file-cache.ts} (84%) rename server/lib/files-cache/{videos-torrent-cache.ts => video-torrents-simple-file-cache.ts} (89%) diff --git a/server.ts b/server.ts index 5d3acb2cd..e25322b66 100644 --- a/server.ts +++ b/server.ts @@ -21,7 +21,7 @@ import { checkMissedConfig, checkFFmpeg, checkNodeVersion } from './server/initi // Do not use barrels because we don't want to load all modules here (we need to initialize database first) import { CONFIG } from './server/initializers/config' -import { API_VERSION, FILES_CACHE, WEBSERVER, loadLanguages } from './server/initializers/constants' +import { API_VERSION, WEBSERVER, loadLanguages } from './server/initializers/constants' import { logger } from './server/helpers/logger' const missed = checkMissedConfig() @@ -101,7 +101,6 @@ loadLanguages() import { installApplication } from './server/initializers/installer' import { Emailer } from './server/lib/emailer' import { JobQueue } from './server/lib/job-queue' -import { VideosPreviewCache, VideosCaptionCache, VideosStoryboardCache } from './server/lib/files-cache' import { activityPubRouter, apiRouter, @@ -143,7 +142,6 @@ import { Hooks } from './server/lib/plugins/hooks' import { PluginManager } from './server/lib/plugins/plugin-manager' import { LiveManager } from './server/lib/live' import { HttpStatusCode } from './shared/models/http/http-error-codes' -import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' import { ServerConfigManager } from '@server/lib/server-config-manager' import { VideoViewsManager } from '@server/lib/views/video-views-manager' import { isTestOrDevInstance } from './server/helpers/core-utils' @@ -312,12 +310,6 @@ async function startApplication () { ServerConfigManager.Instance.init() ]) - // Caches initializations - VideosPreviewCache.Instance.init(CONFIG.CACHE.PREVIEWS.SIZE, FILES_CACHE.PREVIEWS.MAX_AGE) - VideosCaptionCache.Instance.init(CONFIG.CACHE.VIDEO_CAPTIONS.SIZE, FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE) - VideosTorrentCache.Instance.init(CONFIG.CACHE.TORRENTS.SIZE, FILES_CACHE.TORRENTS.MAX_AGE) - VideosStoryboardCache.Instance.init(CONFIG.CACHE.STORYBOARDS.SIZE, FILES_CACHE.STORYBOARDS.MAX_AGE) - // Enable Schedulers ActorFollowScheduler.Instance.enable() RemoveOldJobsScheduler.Instance.enable() diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts index fe00034ed..1568ee597 100644 --- a/server/controllers/api/video-playlist.ts +++ b/server/controllers/api/video-playlist.ts @@ -23,7 +23,7 @@ import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constant import { sequelizeTypescript } from '../../initializers/database' import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' -import { updatePlaylistMiniatureFromExisting } from '../../lib/thumbnail' +import { updateLocalPlaylistMiniatureFromExisting } from '../../lib/thumbnail' import { apiRateLimiter, asyncMiddleware, @@ -178,7 +178,7 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) { const thumbnailField = req.files['thumbnailfile'] const thumbnailModel = thumbnailField - ? await updatePlaylistMiniatureFromExisting({ + ? await updateLocalPlaylistMiniatureFromExisting({ inputPath: thumbnailField[0].path, playlist: videoPlaylist, automaticallyGenerated: false @@ -220,7 +220,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response) const thumbnailField = req.files['thumbnailfile'] const thumbnailModel = thumbnailField - ? await updatePlaylistMiniatureFromExisting({ + ? await updateLocalPlaylistMiniatureFromExisting({ inputPath: thumbnailField[0].path, playlist: videoPlaylistInstance, automaticallyGenerated: false @@ -497,7 +497,7 @@ async function generateThumbnailForPlaylist (videoPlaylist: MVideoPlaylistThumbn } const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoMiniature.filename) - const thumbnailModel = await updatePlaylistMiniatureFromExisting({ + const thumbnailModel = await updateLocalPlaylistMiniatureFromExisting({ inputPath, playlist: videoPlaylist, automaticallyGenerated: true, diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index b8016140e..defe9efd4 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts @@ -14,7 +14,7 @@ import { getSecureTorrentName } from '../../../helpers/utils' import { CONFIG } from '../../../initializers/config' import { MIMETYPES } from '../../../initializers/constants' import { JobQueue } from '../../../lib/job-queue/job-queue' -import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' +import { updateLocalVideoMiniatureFromExisting } from '../../../lib/thumbnail' import { asyncMiddleware, asyncRetryTransactionMiddleware, @@ -193,7 +193,7 @@ async function processThumbnail (req: express.Request, video: MVideoThumbnail) { if (thumbnailField) { const thumbnailPhysicalFile = thumbnailField[0] - return updateVideoMiniatureFromExisting({ + return updateLocalVideoMiniatureFromExisting({ inputPath: thumbnailPhysicalFile.path, video, type: ThumbnailType.MINIATURE, @@ -209,7 +209,7 @@ async function processPreview (req: express.Request, video: MVideoThumbnail): Pr if (previewField) { const previewPhysicalFile = previewField[0] - return updateVideoMiniatureFromExisting({ + return updateLocalVideoMiniatureFromExisting({ inputPath: previewPhysicalFile.path, video, type: ThumbnailType.PREVIEW, diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts index cf82c9791..e19e8c652 100644 --- a/server/controllers/api/videos/live.ts +++ b/server/controllers/api/videos/live.ts @@ -21,7 +21,7 @@ import { buildUUID, uuidToShort } from '@shared/extra-utils' import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoPrivacy, VideoState } from '@shared/models' import { logger } from '../../../helpers/logger' import { sequelizeTypescript } from '../../../initializers/database' -import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' +import { updateLocalVideoMiniatureFromExisting } from '../../../lib/thumbnail' import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares' import { VideoModel } from '../../../models/video/video' import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' @@ -166,7 +166,7 @@ async function addLiveVideo (req: express.Request, res: express.Response) { video, files: req.files, fallback: type => { - return updateVideoMiniatureFromExisting({ + return updateLocalVideoMiniatureFromExisting({ inputPath: ASSETS_PATH.DEFAULT_LIVE_BACKGROUND, video, type, diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts index 6c471ff90..0e07302d2 100644 --- a/server/controllers/api/videos/upload.ts +++ b/server/controllers/api/videos/upload.ts @@ -21,7 +21,7 @@ import { logger, loggerTagsFactory } from '../../../helpers/logger' import { MIMETYPES } from '../../../initializers/constants' import { sequelizeTypescript } from '../../../initializers/database' import { Hooks } from '../../../lib/plugins/hooks' -import { generateVideoMiniature } from '../../../lib/thumbnail' +import { generateLocalVideoMiniature } from '../../../lib/thumbnail' import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' import { asyncMiddleware, @@ -153,7 +153,7 @@ async function addVideo (options: { const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ video, files, - fallback: type => generateVideoMiniature({ video, videoFile, type }) + fallback: type => generateLocalVideoMiniature({ video, videoFile, type }) }) const { videoCreated } = await sequelizeTypescript.transaction(async t => { diff --git a/server/controllers/download.ts b/server/controllers/download.ts index 4c3ab0163..4b94e34bd 100644 --- a/server/controllers/download.ts +++ b/server/controllers/download.ts @@ -1,7 +1,7 @@ import cors from 'cors' import express from 'express' import { logger } from '@server/helpers/logger' -import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' +import { VideoTorrentsSimpleFileCache } from '@server/lib/files-cache' import { generateHLSFilePresignedUrl, generateWebVideoPresignedUrl } from '@server/lib/object-storage' import { Hooks } from '@server/lib/plugins/hooks' import { VideoPathManager } from '@server/lib/video-path-manager' @@ -43,7 +43,7 @@ export { // --------------------------------------------------------------------------- async function downloadTorrent (req: express.Request, res: express.Response) { - const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) + const result = await VideoTorrentsSimpleFileCache.Instance.getFilePath(req.params.filename) if (!result) { return res.fail({ status: HttpStatusCode.NOT_FOUND_404, diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts index 6ffd39730..8e18b0642 100644 --- a/server/controllers/lazy-static.ts +++ b/server/controllers/lazy-static.ts @@ -1,14 +1,27 @@ import cors from 'cors' import express from 'express' -import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' -import { MActorImage } from '@server/types/models' +import { CONFIG } from '@server/initializers/config' import { HttpStatusCode } from '../../shared/models/http/http-error-codes' -import { logger } from '../helpers/logger' -import { ACTOR_IMAGES_SIZE, LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' -import { VideosCaptionCache, VideosPreviewCache, VideosStoryboardCache } from '../lib/files-cache' -import { actorImagePathUnsafeCache, downloadActorImageFromWorker } from '../lib/local-actor' +import { FILES_CACHE, LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' +import { + AvatarPermanentFileCache, + VideoCaptionsSimpleFileCache, + VideoPreviewsSimpleFileCache, + VideoStoryboardsSimpleFileCache, + VideoTorrentsSimpleFileCache +} from '../lib/files-cache' import { asyncMiddleware, handleStaticError } from '../middlewares' -import { ActorImageModel } from '../models/actor/actor-image' + +// --------------------------------------------------------------------------- +// Cache initializations +// --------------------------------------------------------------------------- + +VideoPreviewsSimpleFileCache.Instance.init(CONFIG.CACHE.PREVIEWS.SIZE, FILES_CACHE.PREVIEWS.MAX_AGE) +VideoCaptionsSimpleFileCache.Instance.init(CONFIG.CACHE.VIDEO_CAPTIONS.SIZE, FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE) +VideoTorrentsSimpleFileCache.Instance.init(CONFIG.CACHE.TORRENTS.SIZE, FILES_CACHE.TORRENTS.MAX_AGE) +VideoStoryboardsSimpleFileCache.Instance.init(CONFIG.CACHE.STORYBOARDS.SIZE, FILES_CACHE.STORYBOARDS.MAX_AGE) + +// --------------------------------------------------------------------------- const lazyStaticRouter = express.Router() @@ -60,94 +73,37 @@ export { // --------------------------------------------------------------------------- -async function getActorImage (req: express.Request, res: express.Response, next: express.NextFunction) { +const avatarPermanentFileCache = new AvatarPermanentFileCache() + +function getActorImage (req: express.Request, res: express.Response, next: express.NextFunction) { const filename = req.params.filename - if (actorImagePathUnsafeCache.has(filename)) { - return res.sendFile(actorImagePathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER }) - } - - const image = await ActorImageModel.loadByName(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() - - logger.info('Lazy serve remote actor image %s.', image.fileUrl) - - try { - await downloadActorImageFromWorker({ - filename: image.filename, - fileUrl: image.fileUrl, - size: getActorImageSize(image), - type: image.type - }) - } catch (err) { - logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err }) - return res.status(HttpStatusCode.NOT_FOUND_404).end() - } - - image.onDisk = true - image.save() - .catch(err => logger.error('Cannot save new actor image disk state.', { err })) - } - - const path = image.getPath() - - actorImagePathUnsafeCache.set(filename, path) - - return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }, (err: any) => { - if (!err) return - - // 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 actor image %s.', filename, { err }) - - actorImagePathUnsafeCache.delete(filename) - - image.onDisk = false - image.save() - .catch(err => logger.error('Cannot save new actor image disk state.', { err })) - } - - return next(err) - }) -} - -function getActorImageSize (image: MActorImage): { width: number, height: number } { - if (image.width && image.height) { - return { - height: image.height, - width: image.width - } - } - - return ACTOR_IMAGES_SIZE[image.type][0] + return avatarPermanentFileCache.lazyServe({ filename, res, next }) } async function getPreview (req: express.Request, res: express.Response) { - const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename) + const result = await VideoPreviewsSimpleFileCache.Instance.getFilePath(req.params.filename) if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) } async function getStoryboard (req: express.Request, res: express.Response) { - const result = await VideosStoryboardCache.Instance.getFilePath(req.params.filename) + const result = await VideoStoryboardsSimpleFileCache.Instance.getFilePath(req.params.filename) if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) } async function getVideoCaption (req: express.Request, res: express.Response) { - const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename) + const result = await VideoCaptionsSimpleFileCache.Instance.getFilePath(req.params.filename) if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) } async function getTorrent (req: express.Request, res: express.Response) { - const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) + const result = await VideoTorrentsSimpleFileCache.Instance.getFilePath(req.params.filename) if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() // Torrents still use the old naming convention (video uuid + .torrent) diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 3a643a60b..511aa91cc 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -854,8 +854,8 @@ const LRU_CACHE = { USER_TOKENS: { MAX_SIZE: 1000 }, - ACTOR_IMAGE_STATIC: { - MAX_SIZE: 500 + FILENAME_TO_PATH_PERMANENT_FILE_CACHE: { + MAX_SIZE: 1000 }, STATIC_VIDEO_FILES_RIGHTS_CHECK: { MAX_SIZE: 5000, diff --git a/server/lib/activitypub/videos/shared/abstract-builder.ts b/server/lib/activitypub/videos/shared/abstract-builder.ts index 8af67ecac..e50bf29dc 100644 --- a/server/lib/activitypub/videos/shared/abstract-builder.ts +++ b/server/lib/activitypub/videos/shared/abstract-builder.ts @@ -1,7 +1,7 @@ import { CreationAttributes, Transaction } from 'sequelize/types' import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils' import { logger, LoggerTagsFn } from '@server/helpers/logger' -import { updatePlaceholderThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail' +import { updateRemoteThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail' import { setVideoTags } from '@server/lib/video' import { StoryboardModel } from '@server/models/video/storyboard' import { VideoCaptionModel } from '@server/models/video/video-caption' @@ -55,15 +55,15 @@ export abstract class APVideoAbstractBuilder { } protected async setPreview (video: MVideoFullLight, t?: Transaction) { - // Don't fetch the preview that could be big, create a placeholder instead const previewIcon = getPreviewFromIcons(this.videoObject) if (!previewIcon) return - const previewModel = updatePlaceholderThumbnail({ + const previewModel = updateRemoteThumbnail({ fileUrl: previewIcon.url, video, type: ThumbnailType.PREVIEW, - size: previewIcon + size: previewIcon, + onDisk: false // Don't fetch the preview that could be big, create a placeholder instead }) await video.addAndSaveThumbnail(previewModel, t) diff --git a/server/lib/files-cache/avatar-permanent-file-cache.ts b/server/lib/files-cache/avatar-permanent-file-cache.ts new file mode 100644 index 000000000..89228c5a5 --- /dev/null +++ b/server/lib/files-cache/avatar-permanent-file-cache.ts @@ -0,0 +1,27 @@ +import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' +import { ActorImageModel } from '@server/models/actor/actor-image' +import { MActorImage } from '@server/types/models' +import { AbstractPermanentFileCache } from './shared' +import { CONFIG } from '@server/initializers/config' + +export class AvatarPermanentFileCache extends AbstractPermanentFileCache { + + constructor () { + super(CONFIG.STORAGE.ACTOR_IMAGES) + } + + protected loadModel (filename: string) { + return ActorImageModel.loadByName(filename) + } + + protected getImageSize (image: MActorImage): { width: number, height: number } { + if (image.width && image.height) { + return { + height: image.height, + width: image.width + } + } + + return ACTOR_IMAGES_SIZE[image.type][0] + } +} diff --git a/server/lib/files-cache/index.ts b/server/lib/files-cache/index.ts index 59cec7215..cc11d5385 100644 --- a/server/lib/files-cache/index.ts +++ b/server/lib/files-cache/index.ts @@ -1,4 +1,5 @@ -export * from './videos-caption-cache' -export * from './videos-preview-cache' -export * from './videos-storyboard-cache' -export * from './videos-torrent-cache' +export * from './avatar-permanent-file-cache' +export * from './video-captions-simple-file-cache' +export * from './video-previews-simple-file-cache' +export * from './video-storyboards-simple-file-cache' +export * from './video-torrents-simple-file-cache' 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 @@ +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 +} + +export abstract class AbstractPermanentFileCache { + // Unsafe because it can return paths that do not exist anymore + private readonly filenameToPathUnsafeCache = new LRUCache({ + 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 + + 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) + } +} diff --git a/server/lib/files-cache/abstract-video-static-file-cache.ts b/server/lib/files-cache/shared/abstract-simple-file-cache.ts similarity index 90% rename from server/lib/files-cache/abstract-video-static-file-cache.ts rename to server/lib/files-cache/shared/abstract-simple-file-cache.ts index a7ac88525..6fab322cd 100644 --- a/server/lib/files-cache/abstract-video-static-file-cache.ts +++ b/server/lib/files-cache/shared/abstract-simple-file-cache.ts @@ -1,10 +1,10 @@ import { remove } from 'fs-extra' -import { logger } from '../../helpers/logger' +import { logger } from '../../../helpers/logger' import memoizee from 'memoizee' type GetFilePathResult = { isOwned: boolean, path: string, downloadName?: string } | undefined -export abstract class AbstractVideoStaticFileCache { +export abstract class AbstractSimpleFileCache { getFilePath: (params: T) => Promise 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 @@ +export * from './abstract-permanent-file-cache' +export * from './abstract-simple-file-cache' diff --git a/server/lib/files-cache/videos-caption-cache.ts b/server/lib/files-cache/video-captions-simple-file-cache.ts similarity index 80% rename from server/lib/files-cache/videos-caption-cache.ts rename to server/lib/files-cache/video-captions-simple-file-cache.ts index d21acf4ef..cbeeff732 100644 --- a/server/lib/files-cache/videos-caption-cache.ts +++ b/server/lib/files-cache/video-captions-simple-file-cache.ts @@ -5,11 +5,11 @@ import { CONFIG } from '../../initializers/config' import { FILES_CACHE } from '../../initializers/constants' import { VideoModel } from '../../models/video/video' import { VideoCaptionModel } from '../../models/video/video-caption' -import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' +import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache' -class VideosCaptionCache extends AbstractVideoStaticFileCache { +class VideoCaptionsSimpleFileCache extends AbstractSimpleFileCache { - private static instance: VideosCaptionCache + private static instance: VideoCaptionsSimpleFileCache private constructor () { super() @@ -23,7 +23,9 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache { const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(filename) if (!videoCaption) return undefined - if (videoCaption.isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.filename) } + if (videoCaption.isOwned()) { + return { isOwned: true, path: join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.filename) } + } return this.loadRemoteFile(filename) } @@ -55,5 +57,5 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache { } export { - VideosCaptionCache + VideoCaptionsSimpleFileCache } diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/video-previews-simple-file-cache.ts similarity index 86% rename from server/lib/files-cache/videos-preview-cache.ts rename to server/lib/files-cache/video-previews-simple-file-cache.ts index d19c3f4f4..a05e80e16 100644 --- a/server/lib/files-cache/videos-preview-cache.ts +++ b/server/lib/files-cache/video-previews-simple-file-cache.ts @@ -1,15 +1,15 @@ import { join } from 'path' import { FILES_CACHE } from '../../initializers/constants' import { VideoModel } from '../../models/video/video' -import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' +import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache' import { doRequestAndSaveToFile } from '@server/helpers/requests' import { ThumbnailModel } from '@server/models/video/thumbnail' import { ThumbnailType } from '@shared/models' import { logger } from '@server/helpers/logger' -class VideosPreviewCache extends AbstractVideoStaticFileCache { +class VideoPreviewsSimpleFileCache extends AbstractSimpleFileCache { - private static instance: VideosPreviewCache + private static instance: VideoPreviewsSimpleFileCache private constructor () { super() @@ -54,5 +54,5 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache { } export { - VideosPreviewCache + VideoPreviewsSimpleFileCache } diff --git a/server/lib/files-cache/videos-storyboard-cache.ts b/server/lib/files-cache/video-storyboards-simple-file-cache.ts similarity index 84% rename from server/lib/files-cache/videos-storyboard-cache.ts rename to server/lib/files-cache/video-storyboards-simple-file-cache.ts index b0a55104f..4cd96e70c 100644 --- a/server/lib/files-cache/videos-storyboard-cache.ts +++ b/server/lib/files-cache/video-storyboards-simple-file-cache.ts @@ -3,11 +3,11 @@ import { logger } from '@server/helpers/logger' import { doRequestAndSaveToFile } from '@server/helpers/requests' import { StoryboardModel } from '@server/models/video/storyboard' import { FILES_CACHE } from '../../initializers/constants' -import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' +import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache' -class VideosStoryboardCache extends AbstractVideoStaticFileCache { +class VideoStoryboardsSimpleFileCache extends AbstractSimpleFileCache { - private static instance: VideosStoryboardCache + private static instance: VideoStoryboardsSimpleFileCache private constructor () { super() @@ -49,5 +49,5 @@ class VideosStoryboardCache extends AbstractVideoStaticFileCache { } export { - VideosStoryboardCache + VideoStoryboardsSimpleFileCache } diff --git a/server/lib/files-cache/videos-torrent-cache.ts b/server/lib/files-cache/video-torrents-simple-file-cache.ts similarity index 89% rename from server/lib/files-cache/videos-torrent-cache.ts rename to server/lib/files-cache/video-torrents-simple-file-cache.ts index a6bf98dd4..8bcd0b9bf 100644 --- a/server/lib/files-cache/videos-torrent-cache.ts +++ b/server/lib/files-cache/video-torrents-simple-file-cache.ts @@ -6,11 +6,11 @@ import { MVideo, MVideoFile } from '@server/types/models' import { CONFIG } from '../../initializers/config' import { FILES_CACHE } from '../../initializers/constants' import { VideoModel } from '../../models/video/video' -import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' +import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache' -class VideosTorrentCache extends AbstractVideoStaticFileCache { +class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache { - private static instance: VideosTorrentCache + private static instance: VideoTorrentsSimpleFileCache private constructor () { super() @@ -66,5 +66,5 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache { } export { - VideosTorrentCache + VideoTorrentsSimpleFileCache } diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index c1355dcef..436bf3175 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts @@ -39,7 +39,7 @@ import { VideoFileModel } from '../../../models/video/video-file' import { VideoImportModel } from '../../../models/video/video-import' import { federateVideoIfNeeded } from '../../activitypub/videos' import { Notifier } from '../../notifier' -import { generateVideoMiniature } from '../../thumbnail' +import { generateLocalVideoMiniature } from '../../thumbnail' import { JobQueue } from '../job-queue' async function processVideoImport (job: Job): Promise { @@ -274,7 +274,7 @@ async function generateMiniature (videoImportWithFiles: MVideoImportDefaultFiles } } - const miniatureModel = await generateVideoMiniature({ + const miniatureModel = await generateLocalVideoMiniature({ video: videoImportWithFiles.Video, videoFile, type: thumbnailType diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 95d4f5e64..ae886de35 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts @@ -7,7 +7,7 @@ import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' import { cleanupAndDestroyPermanentLive, cleanupTMPLiveFiles, cleanupUnsavedNormalLive } from '@server/lib/live' import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '@server/lib/paths' -import { generateVideoMiniature } from '@server/lib/thumbnail' +import { generateLocalVideoMiniature } from '@server/lib/thumbnail' import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/hls-transcoding' import { VideoPathManager } from '@server/lib/video-path-manager' import { moveToNextState } from '@server/lib/video-state' @@ -143,7 +143,7 @@ async function saveReplayToExternalVideo (options: { await remove(replayDirectory) for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) { - const image = await generateVideoMiniature({ video: replayVideo, videoFile: replayVideo.getMaxQualityFile(), type }) + const image = await generateLocalVideoMiniature({ video: replayVideo, videoFile: replayVideo.getMaxQualityFile(), type }) await replayVideo.addAndSaveThumbnail(image) } @@ -198,7 +198,7 @@ async function replaceLiveByReplay (options: { // Regenerate the thumbnail & preview? if (videoWithFiles.getMiniature().automaticallyGenerated === true) { - const miniature = await generateVideoMiniature({ + const miniature = await generateLocalVideoMiniature({ video: videoWithFiles, videoFile: videoWithFiles.getMaxQualityFile(), type: ThumbnailType.MINIATURE @@ -207,7 +207,7 @@ async function replaceLiveByReplay (options: { } if (videoWithFiles.getPreview().automaticallyGenerated === true) { - const preview = await generateVideoMiniature({ + const preview = await generateLocalVideoMiniature({ video: videoWithFiles, videoFile: videoWithFiles.getMaxQualityFile(), type: ThumbnailType.PREVIEW diff --git a/server/lib/local-actor.ts b/server/lib/local-actor.ts index 16dc265a3..872addc58 100644 --- a/server/lib/local-actor.ts +++ b/server/lib/local-actor.ts @@ -1,5 +1,4 @@ import { remove } from 'fs-extra' -import { LRUCache } from 'lru-cache' import { join } from 'path' import { Transaction } from 'sequelize/types' import { ActorModel } from '@server/models/actor/actor' @@ -8,14 +7,14 @@ import { buildUUID } from '@shared/extra-utils' import { ActivityPubActorType, ActorImageType } from '@shared/models' import { retryTransactionWrapper } from '../helpers/database-utils' import { CONFIG } from '../initializers/config' -import { ACTOR_IMAGES_SIZE, LRU_CACHE, WEBSERVER } from '../initializers/constants' +import { ACTOR_IMAGES_SIZE, WEBSERVER } from '../initializers/constants' import { sequelizeTypescript } from '../initializers/database' import { MAccountDefault, MActor, MChannelDefault } from '../types/models' import { deleteActorImages, updateActorImages } from './activitypub/actors' import { sendUpdateActor } from './activitypub/send' -import { downloadImageFromWorker, processImageFromWorker } from './worker/parent-process' +import { processImageFromWorker } from './worker/parent-process' -function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { +export function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { return new ActorModel({ type, url, @@ -32,7 +31,7 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU }) as MActor } -async function updateLocalActorImageFiles ( +export async function updateLocalActorImageFiles ( accountOrChannel: MAccountDefault | MChannelDefault, imagePhysicalFile: Express.Multer.File, type: ActorImageType @@ -73,7 +72,7 @@ async function updateLocalActorImageFiles ( })) } -async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { +export async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { return retryTransactionWrapper(() => { return sequelizeTypescript.transaction(async t => { const updatedActor = await deleteActorImages(accountOrChannel.Actor, type, t) @@ -88,7 +87,7 @@ async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MC // --------------------------------------------------------------------------- -async function findAvailableLocalActorName (baseActorName: string, transaction?: Transaction) { +export async function findAvailableLocalActorName (baseActorName: string, transaction?: Transaction) { let actor = await ActorModel.loadLocalByName(baseActorName, transaction) if (!actor) return baseActorName @@ -101,34 +100,3 @@ async function findAvailableLocalActorName (baseActorName: string, transaction?: throw new Error('Cannot find available actor local name (too much iterations).') } - -// --------------------------------------------------------------------------- - -function downloadActorImageFromWorker (options: { - fileUrl: string - filename: string - type: ActorImageType - size: typeof ACTOR_IMAGES_SIZE[ActorImageType][0] -}) { - const downloaderOptions = { - url: options.fileUrl, - destDir: CONFIG.STORAGE.ACTOR_IMAGES, - destName: options.filename, - size: options.size - } - - return downloadImageFromWorker(downloaderOptions) -} - -// Unsafe so could returns paths that does not exist anymore -const actorImagePathUnsafeCache = new LRUCache({ max: LRU_CACHE.ACTOR_IMAGE_STATIC.MAX_SIZE }) - -export { - actorImagePathUnsafeCache, - updateLocalActorImageFiles, - findAvailableLocalActorName, - downloadActorImageFromWorker, - deleteLocalActorImageFile, - downloadImageFromWorker, - buildActorInstance -} diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index 02b867a91..e792567ff 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts @@ -7,13 +7,12 @@ import { ThumbnailModel } from '../models/video/thumbnail' import { MVideoFile, MVideoThumbnail, MVideoUUID } from '../types/models' import { MThumbnail } from '../types/models/video/thumbnail' import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' -import { downloadImageFromWorker } from './local-actor' import { VideoPathManager } from './video-path-manager' -import { processImageFromWorker } from './worker/parent-process' +import { downloadImageFromWorker, processImageFromWorker } from './worker/parent-process' type ImageSize = { height?: number, width?: number } -function updatePlaylistMiniatureFromExisting (options: { +function updateLocalPlaylistMiniatureFromExisting (options: { inputPath: string playlist: MVideoPlaylistThumbnail automaticallyGenerated: boolean @@ -35,6 +34,7 @@ function updatePlaylistMiniatureFromExisting (options: { width, type, automaticallyGenerated, + onDisk: true, existingThumbnail }) } @@ -57,7 +57,7 @@ function updatePlaylistMiniatureFromUrl (options: { return downloadImageFromWorker({ url: downloadUrl, destDir: basePath, destName: filename, size: { width, height } }) } - return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) + return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl, onDisk: true }) } function updateVideoMiniatureFromUrl (options: { @@ -89,10 +89,10 @@ function updateVideoMiniatureFromUrl (options: { return Promise.resolve() } - return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) + return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl, onDisk: true }) } -function updateVideoMiniatureFromExisting (options: { +function updateLocalVideoMiniatureFromExisting (options: { inputPath: string video: MVideoThumbnail type: ThumbnailType @@ -115,11 +115,12 @@ function updateVideoMiniatureFromExisting (options: { width, type, automaticallyGenerated, - existingThumbnail + existingThumbnail, + onDisk: true }) } -function generateVideoMiniature (options: { +function generateLocalVideoMiniature (options: { video: MVideoThumbnail videoFile: MVideoFile type: ThumbnailType @@ -150,34 +151,36 @@ function generateVideoMiniature (options: { width, type, automaticallyGenerated: true, + onDisk: true, existingThumbnail }) }) } -function updatePlaceholderThumbnail (options: { +function updateRemoteThumbnail (options: { fileUrl: string video: MVideoThumbnail type: ThumbnailType size: ImageSize + onDisk: boolean }) { - const { fileUrl, video, type, size } = options - const { filename: updatedFilename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) + const { fileUrl, video, type, size, onDisk } = options + const { filename: generatedFilename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) const thumbnailUrlChanged = hasThumbnailUrlChanged(existingThumbnail, fileUrl, video) const thumbnail = existingThumbnail || new ThumbnailModel() // Do not change the thumbnail filename if the file did not change - const filename = thumbnailUrlChanged - ? updatedFilename - : existingThumbnail.filename + if (thumbnailUrlChanged) { + thumbnail.filename = generatedFilename + } - thumbnail.filename = filename thumbnail.height = height thumbnail.width = width thumbnail.type = type thumbnail.fileUrl = fileUrl + thumbnail.onDisk = onDisk return thumbnail } @@ -185,14 +188,18 @@ function updatePlaceholderThumbnail (options: { // --------------------------------------------------------------------------- export { - generateVideoMiniature, + generateLocalVideoMiniature, updateVideoMiniatureFromUrl, - updateVideoMiniatureFromExisting, - updatePlaceholderThumbnail, + updateLocalVideoMiniatureFromExisting, + updateRemoteThumbnail, updatePlaylistMiniatureFromUrl, - updatePlaylistMiniatureFromExisting + updateLocalPlaylistMiniatureFromExisting } +// --------------------------------------------------------------------------- +// Private +// --------------------------------------------------------------------------- + function hasThumbnailUrlChanged (existingThumbnail: MThumbnail, downloadUrl: string, video: MVideoUUID) { const existingUrl = existingThumbnail ? existingThumbnail.fileUrl @@ -258,6 +265,7 @@ async function updateThumbnailFromFunction (parameters: { height: number width: number type: ThumbnailType + onDisk: boolean automaticallyGenerated?: boolean fileUrl?: string existingThumbnail?: MThumbnail @@ -269,6 +277,7 @@ async function updateThumbnailFromFunction (parameters: { height, type, existingThumbnail, + onDisk, automaticallyGenerated = null, fileUrl = null } = parameters @@ -285,6 +294,7 @@ async function updateThumbnailFromFunction (parameters: { thumbnail.type = type thumbnail.fileUrl = fileUrl thumbnail.automaticallyGenerated = automaticallyGenerated + thumbnail.onDisk = onDisk if (oldFilename) thumbnail.previousThumbnailFilename = oldFilename diff --git a/server/lib/video-pre-import.ts b/server/lib/video-pre-import.ts index 0ac667ba3..ef9c38731 100644 --- a/server/lib/video-pre-import.ts +++ b/server/lib/video-pre-import.ts @@ -29,7 +29,7 @@ import { } from '@server/types/models' import { ThumbnailType, VideoImportCreate, VideoImportPayload, VideoImportState, VideoPrivacy, VideoState } from '@shared/models' import { getLocalVideoActivityPubUrl } from './activitypub/url' -import { updateVideoMiniatureFromExisting, updateVideoMiniatureFromUrl } from './thumbnail' +import { updateLocalVideoMiniatureFromExisting, updateVideoMiniatureFromUrl } from './thumbnail' import { VideoPasswordModel } from '@server/models/video/video-password' class YoutubeDlImportError extends Error { @@ -256,7 +256,7 @@ async function forgeThumbnail ({ inputPath, video, downloadUrl, type }: { type: ThumbnailType }): Promise { if (inputPath) { - return updateVideoMiniatureFromExisting({ + return updateLocalVideoMiniatureFromExisting({ inputPath, video, type, diff --git a/server/lib/video.ts b/server/lib/video.ts index 588dc553f..362c861a5 100644 --- a/server/lib/video.ts +++ b/server/lib/video.ts @@ -10,7 +10,7 @@ import { FilteredModelAttributes } from '@server/types' import { MThumbnail, MVideoFullLight, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' import { ManageVideoTorrentPayload, ThumbnailType, VideoCreate, VideoPrivacy, VideoState } from '@shared/models' import { CreateJobArgument, JobQueue } from './job-queue/job-queue' -import { updateVideoMiniatureFromExisting } from './thumbnail' +import { updateLocalVideoMiniatureFromExisting } from './thumbnail' import { moveFilesIfPrivacyChanged } from './video-privacy' function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes { @@ -55,7 +55,7 @@ async function buildVideoThumbnailsFromReq (options: { const fields = files?.[p.fieldName] if (fields) { - return updateVideoMiniatureFromExisting({ + return updateLocalVideoMiniatureFromExisting({ inputPath: fields[0].path, video, type: p.type, diff --git a/server/models/video/sql/video/shared/video-table-attributes.ts b/server/models/video/sql/video/shared/video-table-attributes.ts index 34967cd20..e0fa9d7c1 100644 --- a/server/models/video/sql/video/shared/video-table-attributes.ts +++ b/server/models/video/sql/video/shared/video-table-attributes.ts @@ -60,6 +60,7 @@ export class VideoTableAttributes { 'height', 'width', 'fileUrl', + 'onDisk', 'automaticallyGenerated', 'videoId', 'videoPlaylistId', diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts index a4ac581e5..2a1f6a7b4 100644 --- a/server/models/video/thumbnail.ts +++ b/server/models/video/thumbnail.ts @@ -69,6 +69,10 @@ export class ThumbnailModel extends Model @Column automaticallyGenerated: boolean + @AllowNull(false) + @Column + onDisk: boolean + @ForeignKey(() => VideoModel) @Column videoId: number diff --git a/support/nginx/peertube b/support/nginx/peertube index 05a59c072..f5b9d131a 100644 --- a/support/nginx/peertube +++ b/support/nginx/peertube @@ -199,28 +199,6 @@ server { alias /var/www/peertube/peertube-latest/client/dist/$1; } - # Bypass PeerTube for performance reasons. Optional. - location ~ ^/static/(thumbnails|avatars)/ { - if ($request_method = 'OPTIONS') { - add_header Access-Control-Allow-Origin '*'; - add_header Access-Control-Allow-Methods 'GET, OPTIONS'; - add_header Access-Control-Allow-Headers 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; - add_header Access-Control-Max-Age 1728000; # Preflight request can be cached 20 days - add_header Content-Type 'text/plain charset=UTF-8'; - add_header Content-Length 0; - return 204; - } - - add_header Access-Control-Allow-Origin '*'; - add_header Access-Control-Allow-Methods 'GET, OPTIONS'; - add_header Access-Control-Allow-Headers 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; - add_header Cache-Control "public, max-age=7200"; # Cache response 2 hours - - rewrite ^/static/(.*)$ /$1 break; - - try_files $uri @api; - } - location ~ ^(/static/(webseed|streaming-playlists)/private/)|^/download { # We can't rate limit a try_files directive, so we need to duplicate @api