]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/api/videos.js
df068f961797d36897b582ed14abdf0338915658
[github/Chocobozzz/PeerTube.git] / server / controllers / api / videos.js
1 'use strict'
2
3 const express = require('express')
4 const fs = require('fs')
5 const multer = require('multer')
6 const path = require('path')
7 const waterfall = require('async/waterfall')
8
9 const constants = require('../../initializers/constants')
10 const db = require('../../initializers/database')
11 const logger = require('../../helpers/logger')
12 const friends = require('../../lib/friends')
13 const middlewares = require('../../middlewares')
14 const admin = middlewares.admin
15 const oAuth = middlewares.oauth
16 const pagination = middlewares.pagination
17 const validators = middlewares.validators
18 const validatorsPagination = validators.pagination
19 const validatorsSort = validators.sort
20 const validatorsVideos = validators.videos
21 const search = middlewares.search
22 const sort = middlewares.sort
23 const utils = require('../../helpers/utils')
24
25 const router = express.Router()
26
27 // multer configuration
28 const storage = multer.diskStorage({
29 destination: function (req, file, cb) {
30 cb(null, constants.CONFIG.STORAGE.VIDEOS_DIR)
31 },
32
33 filename: function (req, file, cb) {
34 let extension = ''
35 if (file.mimetype === 'video/webm') extension = 'webm'
36 else if (file.mimetype === 'video/mp4') extension = 'mp4'
37 else if (file.mimetype === 'video/ogg') extension = 'ogv'
38 utils.generateRandomString(16, function (err, randomString) {
39 const fieldname = err ? undefined : randomString
40 cb(null, fieldname + '.' + extension)
41 })
42 }
43 })
44
45 const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
46
47 router.get('/abuse',
48 oAuth.authenticate,
49 admin.ensureIsAdmin,
50 validatorsPagination.pagination,
51 validatorsSort.videoAbusesSort,
52 sort.setVideoAbusesSort,
53 pagination.setPagination,
54 listVideoAbuses
55 )
56 router.post('/:id/abuse',
57 oAuth.authenticate,
58 validatorsVideos.videoAbuseReport,
59 reportVideoAbuseRetryWrapper
60 )
61
62 router.get('/',
63 validatorsPagination.pagination,
64 validatorsSort.videosSort,
65 sort.setVideosSort,
66 pagination.setPagination,
67 listVideos
68 )
69 router.put('/:id',
70 oAuth.authenticate,
71 reqFiles,
72 validatorsVideos.videosUpdate,
73 updateVideoRetryWrapper
74 )
75 router.post('/',
76 oAuth.authenticate,
77 reqFiles,
78 validatorsVideos.videosAdd,
79 addVideoRetryWrapper
80 )
81 router.get('/:id',
82 validatorsVideos.videosGet,
83 getVideo
84 )
85 router.delete('/:id',
86 oAuth.authenticate,
87 validatorsVideos.videosRemove,
88 removeVideo
89 )
90 router.get('/search/:value',
91 validatorsVideos.videosSearch,
92 validatorsPagination.pagination,
93 validatorsSort.videosSort,
94 sort.setVideosSort,
95 pagination.setPagination,
96 search.setVideosSearch,
97 searchVideos
98 )
99
100 // ---------------------------------------------------------------------------
101
102 module.exports = router
103
104 // ---------------------------------------------------------------------------
105
106 // Wrapper to video add that retry the function if there is a database error
107 // We need this because we run the transaction in SERIALIZABLE isolation that can fail
108 function addVideoRetryWrapper (req, res, next) {
109 utils.transactionRetryer(
110 function (callback) {
111 return addVideo(req, res, req.files.videofile[0], callback)
112 },
113 function (err) {
114 if (err) {
115 logger.error('Cannot insert the video with many retries.', { error: err })
116 return next(err)
117 }
118
119 // TODO : include Location of the new video -> 201
120 return res.type('json').status(204).end()
121 }
122 )
123 }
124
125 function addVideo (req, res, videoFile, callback) {
126 const videoInfos = req.body
127
128 waterfall([
129
130 function startTransaction (callbackWaterfall) {
131 db.sequelize.transaction({ isolationLevel: 'SERIALIZABLE' }).asCallback(function (err, t) {
132 return callbackWaterfall(err, t)
133 })
134 },
135
136 function findOrCreateAuthor (t, callbackWaterfall) {
137 const user = res.locals.oauth.token.User
138
139 const name = user.username
140 // null because it is OUR pod
141 const podId = null
142 const userId = user.id
143
144 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
145 return callbackWaterfall(err, t, authorInstance)
146 })
147 },
148
149 function findOrCreateTags (t, author, callbackWaterfall) {
150 const tags = videoInfos.tags
151
152 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
153 return callbackWaterfall(err, t, author, tagInstances)
154 })
155 },
156
157 function createVideoObject (t, author, tagInstances, callbackWaterfall) {
158 const videoData = {
159 name: videoInfos.name,
160 remoteId: null,
161 extname: path.extname(videoFile.filename),
162 description: videoInfos.description,
163 duration: videoFile.duration,
164 authorId: author.id
165 }
166
167 const video = db.Video.build(videoData)
168
169 return callbackWaterfall(null, t, author, tagInstances, video)
170 },
171
172 // Set the videoname the same as the id
173 function renameVideoFile (t, author, tagInstances, video, callbackWaterfall) {
174 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
175 const source = path.join(videoDir, videoFile.filename)
176 const destination = path.join(videoDir, video.getVideoFilename())
177
178 fs.rename(source, destination, function (err) {
179 if (err) return callbackWaterfall(err)
180
181 // This is important in case if there is another attempt
182 videoFile.filename = video.getVideoFilename()
183 return callbackWaterfall(null, t, author, tagInstances, video)
184 })
185 },
186
187 function insertVideoIntoDB (t, author, tagInstances, video, callbackWaterfall) {
188 const options = { transaction: t }
189
190 // Add tags association
191 video.save(options).asCallback(function (err, videoCreated) {
192 if (err) return callbackWaterfall(err)
193
194 // Do not forget to add Author informations to the created video
195 videoCreated.Author = author
196
197 return callbackWaterfall(err, t, tagInstances, videoCreated)
198 })
199 },
200
201 function associateTagsToVideo (t, tagInstances, video, callbackWaterfall) {
202 const options = { transaction: t }
203
204 video.setTags(tagInstances, options).asCallback(function (err) {
205 video.Tags = tagInstances
206
207 return callbackWaterfall(err, t, video)
208 })
209 },
210
211 function sendToFriends (t, video, callbackWaterfall) {
212 video.toAddRemoteJSON(function (err, remoteVideo) {
213 if (err) return callbackWaterfall(err)
214
215 // Now we'll add the video's meta data to our friends
216 friends.addVideoToFriends(remoteVideo, t, function (err) {
217 return callbackWaterfall(err, t)
218 })
219 })
220 }
221
222 ], function andFinally (err, t) {
223 if (err) {
224 // This is just a debug because we will retry the insert
225 logger.debug('Cannot insert the video.', { error: err })
226
227 // Abort transaction?
228 if (t) t.rollback()
229
230 return callback(err)
231 }
232
233 // Commit transaction
234 t.commit().asCallback(function (err) {
235 if (err) return callback(err)
236
237 logger.info('Video with name %s created.', videoInfos.name)
238 return callback(null)
239 })
240 })
241 }
242
243 function updateVideoRetryWrapper (req, res, next) {
244 utils.transactionRetryer(
245 function (callback) {
246 return updateVideo(req, res, callback)
247 },
248 function (err) {
249 if (err) {
250 logger.error('Cannot update the video with many retries.', { error: err })
251 return next(err)
252 }
253
254 // TODO : include Location of the new video -> 201
255 return res.type('json').status(204).end()
256 }
257 )
258 }
259
260 function updateVideo (req, res, finalCallback) {
261 const videoInstance = res.locals.video
262 const videoInfosToUpdate = req.body
263
264 waterfall([
265
266 function startTransaction (callback) {
267 db.sequelize.transaction().asCallback(function (err, t) {
268 return callback(err, t)
269 })
270 },
271
272 function findOrCreateTags (t, callback) {
273 if (videoInfosToUpdate.tags) {
274 db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) {
275 return callback(err, t, tagInstances)
276 })
277 } else {
278 return callback(null, t, null)
279 }
280 },
281
282 function updateVideoIntoDB (t, tagInstances, callback) {
283 const options = { transaction: t }
284
285 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
286 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
287
288 // Add tags association
289 videoInstance.save(options).asCallback(function (err) {
290 return callback(err, t, tagInstances)
291 })
292 },
293
294 function associateTagsToVideo (t, tagInstances, callback) {
295 if (tagInstances) {
296 const options = { transaction: t }
297
298 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
299 videoInstance.Tags = tagInstances
300
301 return callback(err, t)
302 })
303 } else {
304 return callback(null, t)
305 }
306 },
307
308 function sendToFriends (t, callback) {
309 const json = videoInstance.toUpdateRemoteJSON()
310
311 // Now we'll update the video's meta data to our friends
312 friends.updateVideoToFriends(json, t, function (err) {
313 return callback(err, t)
314 })
315 }
316
317 ], function andFinally (err, t) {
318 if (err) {
319 logger.debug('Cannot update the video.', { error: err })
320
321 // Abort transaction?
322 if (t) t.rollback()
323
324 return finalCallback(err)
325 }
326
327 // Commit transaction
328 t.commit().asCallback(function (err) {
329 if (err) return finalCallback(err)
330
331 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
332 return finalCallback(null)
333 })
334 })
335 }
336
337 function getVideo (req, res, next) {
338 const videoInstance = res.locals.video
339 res.json(videoInstance.toFormatedJSON())
340 }
341
342 function listVideos (req, res, next) {
343 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
344 if (err) return next(err)
345
346 res.json(utils.getFormatedObjects(videosList, videosTotal))
347 })
348 }
349
350 function removeVideo (req, res, next) {
351 const videoInstance = res.locals.video
352
353 videoInstance.destroy().asCallback(function (err) {
354 if (err) {
355 logger.error('Errors when removed the video.', { error: err })
356 return next(err)
357 }
358
359 return res.type('json').status(204).end()
360 })
361 }
362
363 function searchVideos (req, res, next) {
364 db.Video.searchAndPopulateAuthorAndPodAndTags(
365 req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort,
366 function (err, videosList, videosTotal) {
367 if (err) return next(err)
368
369 res.json(utils.getFormatedObjects(videosList, videosTotal))
370 }
371 )
372 }
373
374 function listVideoAbuses (req, res, next) {
375 db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) {
376 if (err) return next(err)
377
378 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
379 })
380 }
381
382 function reportVideoAbuseRetryWrapper (req, res, next) {
383 utils.transactionRetryer(
384 function (callback) {
385 return reportVideoAbuse(req, res, callback)
386 },
387 function (err) {
388 if (err) {
389 logger.error('Cannot report abuse to the video with many retries.', { error: err })
390 return next(err)
391 }
392
393 return res.type('json').status(204).end()
394 }
395 )
396 }
397
398 function reportVideoAbuse (req, res, finalCallback) {
399 const videoInstance = res.locals.video
400 const reporterUsername = res.locals.oauth.token.User.username
401
402 const abuse = {
403 reporterUsername,
404 reason: req.body.reason,
405 videoId: videoInstance.id,
406 reporterPodId: null // This is our pod that reported this abuse
407 }
408
409 waterfall([
410
411 function startTransaction (callback) {
412 db.sequelize.transaction().asCallback(function (err, t) {
413 return callback(err, t)
414 })
415 },
416
417 function createAbuse (t, callback) {
418 db.VideoAbuse.create(abuse).asCallback(function (err, abuse) {
419 return callback(err, t, abuse)
420 })
421 },
422
423 function sendToFriendsIfNeeded (t, abuse, callback) {
424 // We send the information to the destination pod
425 if (videoInstance.isOwned() === false) {
426 const reportData = {
427 reporterUsername,
428 reportReason: abuse.reason,
429 videoRemoteId: videoInstance.remoteId
430 }
431
432 friends.reportAbuseVideoToFriend(reportData, videoInstance)
433 }
434
435 return callback(null, t)
436 }
437
438 ], function andFinally (err, t) {
439 if (err) {
440 logger.debug('Cannot update the video.', { error: err })
441
442 // Abort transaction?
443 if (t) t.rollback()
444
445 return finalCallback(err)
446 }
447
448 // Commit transaction
449 t.commit().asCallback(function (err) {
450 if (err) return finalCallback(err)
451
452 logger.info('Abuse report for video %s created.', videoInstance.name)
453 return finalCallback(null)
454 })
455 })
456 }
457