]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/download.ts
Merge branch 'release/4.3.0' into develop
[github/Chocobozzz/PeerTube.git] / server / controllers / download.ts
1 import cors from 'cors'
2 import express from 'express'
3 import { logger } from '@server/helpers/logger'
4 import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
5 import { Hooks } from '@server/lib/plugins/hooks'
6 import { VideoPathManager } from '@server/lib/video-path-manager'
7 import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
8 import { addQueryParams } from '@shared/core-utils'
9 import { HttpStatusCode, VideoStorage, VideoStreamingPlaylistType } from '@shared/models'
10 import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants'
11 import { asyncMiddleware, optionalAuthenticate, videosDownloadValidator } from '../middlewares'
12
13 const downloadRouter = express.Router()
14
15 downloadRouter.use(cors())
16
17 downloadRouter.use(
18 STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename',
19 asyncMiddleware(downloadTorrent)
20 )
21
22 downloadRouter.use(
23 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
24 optionalAuthenticate,
25 asyncMiddleware(videosDownloadValidator),
26 asyncMiddleware(downloadVideoFile)
27 )
28
29 downloadRouter.use(
30 STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
31 optionalAuthenticate,
32 asyncMiddleware(videosDownloadValidator),
33 asyncMiddleware(downloadHLSVideoFile)
34 )
35
36 // ---------------------------------------------------------------------------
37
38 export {
39 downloadRouter
40 }
41
42 // ---------------------------------------------------------------------------
43
44 async function downloadTorrent (req: express.Request, res: express.Response) {
45 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
46 if (!result) {
47 return res.fail({
48 status: HttpStatusCode.NOT_FOUND_404,
49 message: 'Torrent file not found'
50 })
51 }
52
53 const allowParameters = { torrentPath: result.path, downloadName: result.downloadName }
54
55 const allowedResult = await Hooks.wrapFun(
56 isTorrentDownloadAllowed,
57 allowParameters,
58 'filter:api.download.torrent.allowed.result'
59 )
60
61 if (!checkAllowResult(res, allowParameters, allowedResult)) return
62
63 return res.download(result.path, result.downloadName)
64 }
65
66 async function downloadVideoFile (req: express.Request, res: express.Response) {
67 const video = res.locals.videoAll
68
69 const videoFile = getVideoFile(req, video.VideoFiles)
70 if (!videoFile) {
71 return res.fail({
72 status: HttpStatusCode.NOT_FOUND_404,
73 message: 'Video file not found'
74 })
75 }
76
77 const allowParameters = { video, videoFile }
78
79 const allowedResult = await Hooks.wrapFun(
80 isVideoDownloadAllowed,
81 allowParameters,
82 'filter:api.download.video.allowed.result'
83 )
84
85 if (!checkAllowResult(res, allowParameters, allowedResult)) return
86
87 if (videoFile.storage === VideoStorage.OBJECT_STORAGE) {
88 return redirectToObjectStorage({ req, res, video, file: videoFile })
89 }
90
91 await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), path => {
92 // Express uses basename on filename parameter
93 const videoName = video.name.replace(/[/\\]/g, '_')
94 const filename = `${videoName}-${videoFile.resolution}p${videoFile.extname}`
95
96 return res.download(path, filename)
97 })
98 }
99
100 async function downloadHLSVideoFile (req: express.Request, res: express.Response) {
101 const video = res.locals.videoAll
102 const streamingPlaylist = getHLSPlaylist(video)
103 if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end
104
105 const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles)
106 if (!videoFile) {
107 return res.fail({
108 status: HttpStatusCode.NOT_FOUND_404,
109 message: 'Video file not found'
110 })
111 }
112
113 const allowParameters = { video, streamingPlaylist, videoFile }
114
115 const allowedResult = await Hooks.wrapFun(
116 isVideoDownloadAllowed,
117 allowParameters,
118 'filter:api.download.video.allowed.result'
119 )
120
121 if (!checkAllowResult(res, allowParameters, allowedResult)) return
122
123 if (videoFile.storage === VideoStorage.OBJECT_STORAGE) {
124 return redirectToObjectStorage({ req, res, video, file: videoFile })
125 }
126
127 await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(streamingPlaylist), path => {
128 const filename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}`
129
130 return res.download(path, filename)
131 })
132 }
133
134 function getVideoFile (req: express.Request, files: MVideoFile[]) {
135 const resolution = parseInt(req.params.resolution, 10)
136 return files.find(f => f.resolution === resolution)
137 }
138
139 function getHLSPlaylist (video: MVideoFullLight) {
140 const playlist = video.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
141 if (!playlist) return undefined
142
143 return Object.assign(playlist, { Video: video })
144 }
145
146 type AllowedResult = {
147 allowed: boolean
148 errorMessage?: string
149 }
150
151 function isTorrentDownloadAllowed (_object: {
152 torrentPath: string
153 }): AllowedResult {
154 return { allowed: true }
155 }
156
157 function isVideoDownloadAllowed (_object: {
158 video: MVideo
159 videoFile: MVideoFile
160 streamingPlaylist?: MStreamingPlaylist
161 }): AllowedResult {
162 return { allowed: true }
163 }
164
165 function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) {
166 if (!result || result.allowed !== true) {
167 logger.info('Download is not allowed.', { result, allowParameters })
168
169 res.fail({
170 status: HttpStatusCode.FORBIDDEN_403,
171 message: result?.errorMessage || 'Refused download'
172 })
173 return false
174 }
175
176 return true
177 }
178
179 function redirectToObjectStorage (options: {
180 req: express.Request
181 res: express.Response
182 video: MVideo
183 file: MVideoFile
184 }) {
185 const { req, res, video, file } = options
186
187 const baseUrl = file.getObjectStorageUrl(video)
188
189 const url = video.hasPrivateStaticPath() && req.query.videoFileToken
190 ? addQueryParams(baseUrl, { videoFileToken: req.query.videoFileToken })
191 : baseUrl
192
193 return res.redirect(url)
194 }