]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/videos/upload.ts
Add tags to logs in AP videos
[github/Chocobozzz/PeerTube.git] / server / controllers / api / videos / upload.ts
CommitLineData
c158a5fa
C
1import * as express from 'express'
2import { move } from 'fs-extra'
3import { extname } from 'path'
4import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload'
5import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
6import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
7import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
8import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
9import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
10import { uploadx } from '@uploadx/core'
11import { VideoCreate, VideoState } from '../../../../shared'
12import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
13import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
14import { retryTransactionWrapper } from '../../../helpers/database-utils'
15import { createReqFiles } from '../../../helpers/express-utils'
16import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
17import { logger, loggerTagsFactory } from '../../../helpers/logger'
18import { CONFIG } from '../../../initializers/config'
19import { DEFAULT_AUDIO_RESOLUTION, MIMETYPES } from '../../../initializers/constants'
20import { sequelizeTypescript } from '../../../initializers/database'
21import { federateVideoIfNeeded } from '../../../lib/activitypub/videos'
22import { Notifier } from '../../../lib/notifier'
23import { Hooks } from '../../../lib/plugins/hooks'
24import { generateVideoMiniature } from '../../../lib/thumbnail'
25import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
26import {
27 asyncMiddleware,
28 asyncRetryTransactionMiddleware,
29 authenticate,
30 videosAddLegacyValidator,
31 videosAddResumableInitValidator,
32 videosAddResumableValidator
33} from '../../../middlewares'
34import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
35import { VideoModel } from '../../../models/video/video'
36import { VideoFileModel } from '../../../models/video/video-file'
37
38const lTags = loggerTagsFactory('api', 'video')
39const auditLogger = auditLoggerFactory('videos')
40const uploadRouter = express.Router()
41const uploadxMiddleware = uploadx.upload({ directory: getResumableUploadPath() })
42
43const reqVideoFileAdd = createReqFiles(
44 [ 'videofile', 'thumbnailfile', 'previewfile' ],
45 Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
46 {
47 videofile: CONFIG.STORAGE.TMP_DIR,
48 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
49 previewfile: CONFIG.STORAGE.TMP_DIR
50 }
51)
52
53const reqVideoFileAddResumable = createReqFiles(
54 [ 'thumbnailfile', 'previewfile' ],
55 MIMETYPES.IMAGE.MIMETYPE_EXT,
56 {
57 thumbnailfile: getResumableUploadPath(),
58 previewfile: getResumableUploadPath()
59 }
60)
61
62uploadRouter.post('/upload',
63 authenticate,
64 reqVideoFileAdd,
65 asyncMiddleware(videosAddLegacyValidator),
66 asyncRetryTransactionMiddleware(addVideoLegacy)
67)
68
69uploadRouter.post('/upload-resumable',
70 authenticate,
71 reqVideoFileAddResumable,
72 asyncMiddleware(videosAddResumableInitValidator),
73 uploadxMiddleware
74)
75
76uploadRouter.delete('/upload-resumable',
77 authenticate,
78 uploadxMiddleware
79)
80
81uploadRouter.put('/upload-resumable',
82 authenticate,
83 uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes
84 asyncMiddleware(videosAddResumableValidator),
85 asyncMiddleware(addVideoResumable)
86)
87
88// ---------------------------------------------------------------------------
89
90export {
91 uploadRouter
92}
93
94// ---------------------------------------------------------------------------
95
96export async function addVideoLegacy (req: express.Request, res: express.Response) {
97 // Uploading the video could be long
98 // Set timeout to 10 minutes, as Express's default is 2 minutes
99 req.setTimeout(1000 * 60 * 10, () => {
76148b27
RK
100 logger.error('Video upload has timed out.')
101 return res.fail({
102 status: HttpStatusCode.REQUEST_TIMEOUT_408,
103 message: 'Video upload has timed out.'
104 })
c158a5fa
C
105 })
106
107 const videoPhysicalFile = req.files['videofile'][0]
108 const videoInfo: VideoCreate = req.body
109 const files = req.files
110
111 return addVideo({ res, videoPhysicalFile, videoInfo, files })
112}
113
114export async function addVideoResumable (_req: express.Request, res: express.Response) {
115 const videoPhysicalFile = res.locals.videoFileResumable
116 const videoInfo = videoPhysicalFile.metadata
117 const files = { previewfile: videoInfo.previewfile }
118
119 // Don't need the meta file anymore
120 await deleteResumableUploadMetaFile(videoPhysicalFile.path)
121
122 return addVideo({ res, videoPhysicalFile, videoInfo, files })
123}
124
125async function addVideo (options: {
126 res: express.Response
127 videoPhysicalFile: express.VideoUploadFile
128 videoInfo: VideoCreate
129 files: express.UploadFiles
130}) {
131 const { res, videoPhysicalFile, videoInfo, files } = options
132 const videoChannel = res.locals.videoChannel
133 const user = res.locals.oauth.token.User
134
135 const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id)
136
137 videoData.state = CONFIG.TRANSCODING.ENABLED
138 ? VideoState.TO_TRANSCODE
139 : VideoState.PUBLISHED
140
141 videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware
142
143 const video = new VideoModel(videoData) as MVideoFullLight
144 video.VideoChannel = videoChannel
145 video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
146
147 const videoFile = await buildNewFile(video, videoPhysicalFile)
148
149 // Move physical file
150 const destination = getVideoFilePath(video, videoFile)
151 await move(videoPhysicalFile.path, destination)
152 // This is important in case if there is another attempt in the retry process
153 videoPhysicalFile.filename = getVideoFilePath(video, videoFile)
154 videoPhysicalFile.path = destination
155
156 const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
157 video,
158 files,
159 fallback: type => generateVideoMiniature({ video, videoFile, type })
160 })
161
162 const { videoCreated } = await sequelizeTypescript.transaction(async t => {
163 const sequelizeOptions = { transaction: t }
164
165 const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
166
167 await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
168 await videoCreated.addAndSaveThumbnail(previewModel, t)
169
170 // Do not forget to add video channel information to the created video
171 videoCreated.VideoChannel = res.locals.videoChannel
172
173 videoFile.videoId = video.id
174 await videoFile.save(sequelizeOptions)
175
176 video.VideoFiles = [ videoFile ]
177
178 await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
179
180 // Schedule an update in the future?
181 if (videoInfo.scheduleUpdate) {
182 await ScheduleVideoUpdateModel.create({
183 videoId: video.id,
184 updateAt: new Date(videoInfo.scheduleUpdate.updateAt),
185 privacy: videoInfo.scheduleUpdate.privacy || null
186 }, sequelizeOptions)
187 }
188
189 // Channel has a new content, set as updated
190 await videoCreated.VideoChannel.setAsUpdated(t)
191
192 await autoBlacklistVideoIfNeeded({
193 video,
194 user,
195 isRemote: false,
196 isNew: true,
197 transaction: t
198 })
199
200 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
201 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid))
202
203 return { videoCreated }
204 })
205
206 createTorrentFederate(video, videoFile)
207
208 if (video.state === VideoState.TO_TRANSCODE) {
209 await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user)
210 }
211
212 Hooks.runAction('action:api.video.uploaded', { video: videoCreated })
213
214 return res.json({
215 video: {
216 id: videoCreated.id,
217 uuid: videoCreated.uuid
218 }
219 })
220}
221
222async function buildNewFile (video: MVideo, videoPhysicalFile: express.VideoUploadFile) {
223 const videoFile = new VideoFileModel({
224 extname: extname(videoPhysicalFile.filename),
225 size: videoPhysicalFile.size,
226 videoStreamingPlaylistId: null,
227 metadata: await getMetadataFromFile(videoPhysicalFile.path)
228 })
229
230 if (videoFile.isAudio()) {
231 videoFile.resolution = DEFAULT_AUDIO_RESOLUTION
232 } else {
233 videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path)
234 videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution
235 }
236
237 videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname)
238
239 return videoFile
240}
241
242async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoFile) {
243 await createTorrentAndSetInfoHash(video, fileArg)
244
245 // Refresh videoFile because the createTorrentAndSetInfoHash could be long
246 const refreshedFile = await VideoFileModel.loadWithVideo(fileArg.id)
247 // File does not exist anymore, remove the generated torrent
248 if (!refreshedFile) return fileArg.removeTorrent()
249
250 refreshedFile.infoHash = fileArg.infoHash
251 refreshedFile.torrentFilename = fileArg.torrentFilename
252
253 return refreshedFile.save()
254}
255
256function createTorrentFederate (video: MVideoFullLight, videoFile: MVideoFile): void {
257 // Create the torrent file in async way because it could be long
258 createTorrentAndSetInfoHashAsync(video, videoFile)
259 .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) }))
260 .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id))
261 .then(refreshedVideo => {
262 if (!refreshedVideo) return
263
264 // Only federate and notify after the torrent creation
265 Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo)
266
267 return retryTransactionWrapper(() => {
268 return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t))
269 })
270 })
271 .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) }))
272}