]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/api/videos.js
ebfdb32f9c1fcdf311a7ed5a19d7a1f0577aa430
[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 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, callback) {
124 const videoInfos = req.body
125
126 waterfall([
127
128 databaseUtils.startSerializableTransaction,
129
130 function findOrCreateAuthor (t, callbackWaterfall) {
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 callbackWaterfall(err, t, authorInstance)
140 })
141 },
142
143 function findOrCreateTags (t, author, callbackWaterfall) {
144 const tags = videoInfos.tags
145
146 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
147 return callbackWaterfall(err, t, author, tagInstances)
148 })
149 },
150
151 function createVideoObject (t, author, tagInstances, callbackWaterfall) {
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 callbackWaterfall(null, t, author, tagInstances, video)
164 },
165
166 // Set the videoname the same as the id
167 function renameVideoFile (t, author, tagInstances, video, callbackWaterfall) {
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 callbackWaterfall(err)
174
175 // This is important in case if there is another attempt
176 videoFile.filename = video.getVideoFilename()
177 return callbackWaterfall(null, t, author, tagInstances, video)
178 })
179 },
180
181 function insertVideoIntoDB (t, author, tagInstances, video, callbackWaterfall) {
182 const options = { transaction: t }
183
184 // Add tags association
185 video.save(options).asCallback(function (err, videoCreated) {
186 if (err) return callbackWaterfall(err)
187
188 // Do not forget to add Author informations to the created video
189 videoCreated.Author = author
190
191 return callbackWaterfall(err, t, tagInstances, videoCreated)
192 })
193 },
194
195 function associateTagsToVideo (t, tagInstances, video, callbackWaterfall) {
196 const options = { transaction: t }
197
198 video.setTags(tagInstances, options).asCallback(function (err) {
199 video.Tags = tagInstances
200
201 return callbackWaterfall(err, t, video)
202 })
203 },
204
205 function sendToFriends (t, video, callbackWaterfall) {
206 video.toAddRemoteJSON(function (err, remoteVideo) {
207 if (err) return callbackWaterfall(err)
208
209 // Now we'll add the video's meta data to our friends
210 friends.addVideoToFriends(remoteVideo, t, function (err) {
211 return callbackWaterfall(err, t)
212 })
213 })
214 }
215
216 ], function andFinally (err, t) {
217 if (err) {
218 // This is just a debug because we will retry the insert
219 logger.debug('Cannot insert the video.', { error: err })
220
221 // Abort transaction?
222 if (t) t.rollback()
223
224 return callback(err)
225 }
226
227 // Commit transaction
228 t.commit().asCallback(function (err) {
229 if (err) return callback(err)
230
231 logger.info('Video with name %s created.', videoInfos.name)
232 return callback(null)
233 })
234 })
235 }
236
237 function updateVideoRetryWrapper (req, res, next) {
238 const options = {
239 arguments: [ req, res ],
240 errorMessage: 'Cannot update the video with many retries.'
241 }
242
243 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
244 if (err) return next(err)
245
246 // TODO : include Location of the new video -> 201
247 return res.type('json').status(204).end()
248 })
249 }
250
251 function updateVideo (req, res, finalCallback) {
252 const videoInstance = res.locals.video
253 const videoFieldsSave = videoInstance.toJSON()
254 const videoInfosToUpdate = req.body
255
256 waterfall([
257
258 databaseUtils.startSerializableTransaction,
259
260 function findOrCreateTags (t, callback) {
261 if (videoInfosToUpdate.tags) {
262 db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) {
263 return callback(err, t, tagInstances)
264 })
265 } else {
266 return callback(null, t, null)
267 }
268 },
269
270 function updateVideoIntoDB (t, tagInstances, callback) {
271 const options = {
272 transaction: t
273 }
274
275 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
276 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
277
278 videoInstance.save(options).asCallback(function (err) {
279 return callback(err, t, tagInstances)
280 })
281 },
282
283 function associateTagsToVideo (t, tagInstances, callback) {
284 if (tagInstances) {
285 const options = { transaction: t }
286
287 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
288 videoInstance.Tags = tagInstances
289
290 return callback(err, t)
291 })
292 } else {
293 return callback(null, t)
294 }
295 },
296
297 function sendToFriends (t, callback) {
298 const json = videoInstance.toUpdateRemoteJSON()
299
300 // Now we'll update the video's meta data to our friends
301 friends.updateVideoToFriends(json, t, function (err) {
302 return callback(err, t)
303 })
304 }
305
306 ], function andFinally (err, t) {
307 if (err) {
308 logger.debug('Cannot update the video.', { error: err })
309
310 // Abort transaction?
311 if (t) t.rollback()
312
313 // Force fields we want to update
314 // If the transaction is retried, sequelize will think the object has not changed
315 // So it will skip the SQL request, even if the last one was ROLLBACKed!
316 Object.keys(videoFieldsSave).forEach(function (key) {
317 const value = videoFieldsSave[key]
318 videoInstance.set(key, value)
319 })
320
321 return finalCallback(err)
322 }
323
324 // Commit transaction
325 t.commit().asCallback(function (err) {
326 if (err) return finalCallback(err)
327
328 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
329 return finalCallback(null)
330 })
331 })
332 }
333
334 function getVideo (req, res, next) {
335 const videoInstance = res.locals.video
336 res.json(videoInstance.toFormatedJSON())
337 }
338
339 function listVideos (req, res, next) {
340 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
341 if (err) return next(err)
342
343 res.json(utils.getFormatedObjects(videosList, videosTotal))
344 })
345 }
346
347 function removeVideo (req, res, next) {
348 const videoInstance = res.locals.video
349
350 videoInstance.destroy().asCallback(function (err) {
351 if (err) {
352 logger.error('Errors when removed the video.', { error: err })
353 return next(err)
354 }
355
356 return res.type('json').status(204).end()
357 })
358 }
359
360 function searchVideos (req, res, next) {
361 db.Video.searchAndPopulateAuthorAndPodAndTags(
362 req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort,
363 function (err, videosList, videosTotal) {
364 if (err) return next(err)
365
366 res.json(utils.getFormatedObjects(videosList, videosTotal))
367 }
368 )
369 }
370
371 function listVideoAbuses (req, res, next) {
372 db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) {
373 if (err) return next(err)
374
375 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
376 })
377 }
378
379 function reportVideoAbuseRetryWrapper (req, res, next) {
380 const options = {
381 arguments: [ req, res ],
382 errorMessage: 'Cannot report abuse to the video with many retries.'
383 }
384
385 databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) {
386 if (err) return next(err)
387
388 return res.type('json').status(204).end()
389 })
390 }
391
392 function reportVideoAbuse (req, res, finalCallback) {
393 const videoInstance = res.locals.video
394 const reporterUsername = res.locals.oauth.token.User.username
395
396 const abuse = {
397 reporterUsername,
398 reason: req.body.reason,
399 videoId: videoInstance.id,
400 reporterPodId: null // This is our pod that reported this abuse
401 }
402
403 waterfall([
404
405 function startTransaction (callback) {
406 db.sequelize.transaction().asCallback(function (err, t) {
407 return callback(err, t)
408 })
409 },
410
411 function createAbuse (t, callback) {
412 db.VideoAbuse.create(abuse).asCallback(function (err, abuse) {
413 return callback(err, t, abuse)
414 })
415 },
416
417 function sendToFriendsIfNeeded (t, abuse, callback) {
418 // We send the information to the destination pod
419 if (videoInstance.isOwned() === false) {
420 const reportData = {
421 reporterUsername,
422 reportReason: abuse.reason,
423 videoRemoteId: videoInstance.remoteId
424 }
425
426 friends.reportAbuseVideoToFriend(reportData, videoInstance)
427 }
428
429 return callback(null, t)
430 }
431
432 ], function andFinally (err, t) {
433 if (err) {
434 logger.debug('Cannot update the video.', { error: err })
435
436 // Abort transaction?
437 if (t) t.rollback()
438
439 return finalCallback(err)
440 }
441
442 // Commit transaction
443 t.commit().asCallback(function (err) {
444 if (err) return finalCallback(err)
445
446 logger.info('Abuse report for video %s created.', videoInstance.name)
447 return finalCallback(null)
448 })
449 })
450 }
451