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