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