Split types and typings
[oweals/peertube.git] / server / controllers / static.ts
1 import * as cors from 'cors'
2 import * as express from 'express'
3 import {
4   CONSTRAINTS_FIELDS,
5   DEFAULT_THEME_NAME,
6   HLS_STREAMING_PLAYLIST_DIRECTORY,
7   PEERTUBE_VERSION,
8   ROUTE_CACHE_LIFETIME,
9   STATIC_DOWNLOAD_PATHS,
10   STATIC_MAX_AGE,
11   STATIC_PATHS,
12   WEBSERVER
13 } from '../initializers/constants'
14 import { cacheRoute } from '../middlewares/cache'
15 import { asyncMiddleware, videosDownloadValidator } from '../middlewares'
16 import { VideoModel } from '../models/video/video'
17 import { UserModel } from '../models/account/user'
18 import { VideoCommentModel } from '../models/video/video-comment'
19 import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo'
20 import { join } from 'path'
21 import { root } from '../helpers/core-utils'
22 import { CONFIG, isEmailEnabled } from '../initializers/config'
23 import { getPreview, getVideoCaption } from './lazy-static'
24 import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type'
25 import { MVideoFile, MVideoFullLight } from '@server/types/models'
26 import { getTorrentFilePath, getVideoFilePath } from '@server/lib/video-paths'
27 import { getThemeOrDefault } from '../lib/plugins/theme-utils'
28 import { getEnabledResolutions, getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config'
29
30 const staticRouter = express.Router()
31
32 staticRouter.use(cors())
33
34 /*
35   Cors is very important to let other servers access torrent and video files
36 */
37
38 const torrentsPhysicalPath = CONFIG.STORAGE.TORRENTS_DIR
39 staticRouter.use(
40   STATIC_PATHS.TORRENTS,
41   cors(),
42   express.static(torrentsPhysicalPath, { maxAge: 0 }) // Don't cache because we could regenerate the torrent file
43 )
44 staticRouter.use(
45   STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+).torrent',
46   asyncMiddleware(videosDownloadValidator),
47   downloadTorrent
48 )
49 staticRouter.use(
50   STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+)-hls.torrent',
51   asyncMiddleware(videosDownloadValidator),
52   downloadHLSVideoFileTorrent
53 )
54
55 // Videos path for webseeding
56 staticRouter.use(
57   STATIC_PATHS.WEBSEED,
58   cors(),
59   express.static(CONFIG.STORAGE.VIDEOS_DIR, { fallthrough: false }) // 404 because we don't have this video
60 )
61 staticRouter.use(
62   STATIC_PATHS.REDUNDANCY,
63   cors(),
64   express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }) // 404 because we don't have this video
65 )
66
67 staticRouter.use(
68   STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
69   asyncMiddleware(videosDownloadValidator),
70   downloadVideoFile
71 )
72
73 staticRouter.use(
74   STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
75   asyncMiddleware(videosDownloadValidator),
76   downloadHLSVideoFile
77 )
78
79 // HLS
80 staticRouter.use(
81   STATIC_PATHS.STREAMING_PLAYLISTS.HLS,
82   cors(),
83   express.static(HLS_STREAMING_PLAYLIST_DIRECTORY, { fallthrough: false }) // 404 if the file does not exist
84 )
85
86 // Thumbnails path for express
87 const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR
88 staticRouter.use(
89   STATIC_PATHS.THUMBNAILS,
90   express.static(thumbnailsPhysicalPath, { maxAge: STATIC_MAX_AGE.SERVER, fallthrough: false }) // 404 if the file does not exist
91 )
92
93 // DEPRECATED: use lazy-static route instead
94 const avatarsPhysicalPath = CONFIG.STORAGE.AVATARS_DIR
95 staticRouter.use(
96   STATIC_PATHS.AVATARS,
97   express.static(avatarsPhysicalPath, { maxAge: STATIC_MAX_AGE.SERVER, fallthrough: false }) // 404 if the file does not exist
98 )
99
100 // DEPRECATED: use lazy-static route instead
101 staticRouter.use(
102   STATIC_PATHS.PREVIEWS + ':uuid.jpg',
103   asyncMiddleware(getPreview)
104 )
105
106 // DEPRECATED: use lazy-static route instead
107 staticRouter.use(
108   STATIC_PATHS.VIDEO_CAPTIONS + ':videoId-:captionLanguage([a-z]+).vtt',
109   asyncMiddleware(getVideoCaption)
110 )
111
112 // robots.txt service
113 staticRouter.get('/robots.txt',
114   asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.ROBOTS)),
115   (_, res: express.Response) => {
116     res.type('text/plain')
117     return res.send(CONFIG.INSTANCE.ROBOTS)
118   }
119 )
120
121 // security.txt service
122 staticRouter.get('/security.txt',
123   (_, res: express.Response) => {
124     return res.redirect(301, '/.well-known/security.txt')
125   }
126 )
127
128 staticRouter.get('/.well-known/security.txt',
129   asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.SECURITYTXT)),
130   (_, res: express.Response) => {
131     res.type('text/plain')
132     return res.send(CONFIG.INSTANCE.SECURITYTXT + CONFIG.INSTANCE.SECURITYTXT_CONTACT)
133   }
134 )
135
136 // nodeinfo service
137 staticRouter.use('/.well-known/nodeinfo',
138   asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.NODEINFO)),
139   (_, res: express.Response) => {
140     return res.json({
141       links: [
142         {
143           rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
144           href: WEBSERVER.URL + '/nodeinfo/2.0.json'
145         }
146       ]
147     })
148   }
149 )
150 staticRouter.use('/nodeinfo/:version.json',
151   asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.NODEINFO)),
152   asyncMiddleware(generateNodeinfo)
153 )
154
155 // dnt-policy.txt service (see https://www.eff.org/dnt-policy)
156 staticRouter.use('/.well-known/dnt-policy.txt',
157   asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.DNT_POLICY)),
158   (_, res: express.Response) => {
159     res.type('text/plain')
160
161     return res.sendFile(join(root(), 'dist/server/static/dnt-policy/dnt-policy-1.0.txt'))
162   }
163 )
164
165 // dnt service (see https://www.w3.org/TR/tracking-dnt/#status-resource)
166 staticRouter.use('/.well-known/dnt/',
167   (_, res: express.Response) => {
168     res.json({ tracking: 'N' })
169   }
170 )
171
172 staticRouter.use('/.well-known/change-password',
173   (_, res: express.Response) => {
174     res.redirect('/my-account/settings')
175   }
176 )
177
178 staticRouter.use('/.well-known/host-meta',
179   (_, res: express.Response) => {
180     res.type('application/xml')
181
182     const xml = '<?xml version="1.0" encoding="UTF-8"?>\n' +
183       '<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">\n' +
184       `  <Link rel="lrdd" type="application/xrd+xml" template="${WEBSERVER.URL}/.well-known/webfinger?resource={uri}"/>\n` +
185       '</XRD>'
186
187     res.send(xml).end()
188   }
189 )
190
191 // ---------------------------------------------------------------------------
192
193 export {
194   staticRouter
195 }
196
197 // ---------------------------------------------------------------------------
198
199 async function generateNodeinfo (req: express.Request, res: express.Response) {
200   const { totalVideos } = await VideoModel.getStats()
201   const { totalLocalVideoComments } = await VideoCommentModel.getStats()
202   const { totalUsers } = await UserModel.getStats()
203   let json = {}
204
205   if (req.params.version && (req.params.version === '2.0')) {
206     json = {
207       version: '2.0',
208       software: {
209         name: 'peertube',
210         version: PEERTUBE_VERSION
211       },
212       protocols: [
213         'activitypub'
214       ],
215       services: {
216         inbound: [],
217         outbound: [
218           'atom1.0',
219           'rss2.0'
220         ]
221       },
222       openRegistrations: CONFIG.SIGNUP.ENABLED,
223       usage: {
224         users: {
225           total: totalUsers
226         },
227         localPosts: totalVideos,
228         localComments: totalLocalVideoComments
229       },
230       metadata: {
231         taxonomy: {
232           postsName: 'Videos'
233         },
234         nodeName: CONFIG.INSTANCE.NAME,
235         nodeDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
236         nodeConfig: {
237           search: {
238             remoteUri: {
239               users: CONFIG.SEARCH.REMOTE_URI.USERS,
240               anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
241             }
242           },
243           plugin: {
244             registered: getRegisteredPlugins()
245           },
246           theme: {
247             registered: getRegisteredThemes(),
248             default: getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
249           },
250           email: {
251             enabled: isEmailEnabled()
252           },
253           contactForm: {
254             enabled: CONFIG.CONTACT_FORM.ENABLED
255           },
256           transcoding: {
257             hls: {
258               enabled: CONFIG.TRANSCODING.HLS.ENABLED
259             },
260             webtorrent: {
261               enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
262             },
263             enabledResolutions: getEnabledResolutions()
264           },
265           import: {
266             videos: {
267               http: {
268                 enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
269               },
270               torrent: {
271                 enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
272               }
273             }
274           },
275           autoBlacklist: {
276             videos: {
277               ofUsers: {
278                 enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
279               }
280             }
281           },
282           avatar: {
283             file: {
284               size: {
285                 max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max
286               },
287               extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME
288             }
289           },
290           video: {
291             image: {
292               extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
293               size: {
294                 max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max
295               }
296             },
297             file: {
298               extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME
299             }
300           },
301           videoCaption: {
302             file: {
303               size: {
304                 max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max
305               },
306               extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME
307             }
308           },
309           user: {
310             videoQuota: CONFIG.USER.VIDEO_QUOTA,
311             videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
312           },
313           trending: {
314             videos: {
315               intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
316             }
317           },
318           tracker: {
319             enabled: CONFIG.TRACKER.ENABLED
320           }
321         }
322       }
323     } as HttpNodeinfoDiasporaSoftwareNsSchema20
324     res.contentType('application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"')
325   } else {
326     json = { error: 'Nodeinfo schema version not handled' }
327     res.status(404)
328   }
329
330   return res.send(json).end()
331 }
332
333 function downloadTorrent (req: express.Request, res: express.Response) {
334   const video = res.locals.videoAll
335
336   const videoFile = getVideoFile(req, video.VideoFiles)
337   if (!videoFile) return res.status(404).end()
338
339   return res.download(getTorrentFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p.torrent`)
340 }
341
342 function downloadHLSVideoFileTorrent (req: express.Request, res: express.Response) {
343   const video = res.locals.videoAll
344
345   const playlist = getHLSPlaylist(video)
346   if (!playlist) return res.status(404).end
347
348   const videoFile = getVideoFile(req, playlist.VideoFiles)
349   if (!videoFile) return res.status(404).end()
350
351   return res.download(getTorrentFilePath(playlist, videoFile), `${video.name}-${videoFile.resolution}p-hls.torrent`)
352 }
353
354 function downloadVideoFile (req: express.Request, res: express.Response) {
355   const video = res.locals.videoAll
356
357   const videoFile = getVideoFile(req, video.VideoFiles)
358   if (!videoFile) return res.status(404).end()
359
360   return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
361 }
362
363 function downloadHLSVideoFile (req: express.Request, res: express.Response) {
364   const video = res.locals.videoAll
365   const playlist = getHLSPlaylist(video)
366   if (!playlist) return res.status(404).end
367
368   const videoFile = getVideoFile(req, playlist.VideoFiles)
369   if (!videoFile) return res.status(404).end()
370
371   const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}`
372   return res.download(getVideoFilePath(playlist, videoFile), filename)
373 }
374
375 function getVideoFile (req: express.Request, files: MVideoFile[]) {
376   const resolution = parseInt(req.params.resolution, 10)
377   return files.find(f => f.resolution === resolution)
378 }
379
380 function getHLSPlaylist (video: MVideoFullLight) {
381   const playlist = video.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
382   if (!playlist) return undefined
383
384   return Object.assign(playlist, { Video: video })
385 }