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