aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/controllers/api/videos.js
diff options
context:
space:
mode:
Diffstat (limited to 'server/controllers/api/videos.js')
-rw-r--r--server/controllers/api/videos.js382
1 files changed, 311 insertions, 71 deletions
diff --git a/server/controllers/api/videos.js b/server/controllers/api/videos.js
index daf452573..2c4af520e 100644
--- a/server/controllers/api/videos.js
+++ b/server/controllers/api/videos.js
@@ -2,15 +2,16 @@
2 2
3const express = require('express') 3const express = require('express')
4const fs = require('fs') 4const fs = require('fs')
5const mongoose = require('mongoose')
6const multer = require('multer') 5const multer = require('multer')
7const path = require('path') 6const path = require('path')
8const waterfall = require('async/waterfall') 7const waterfall = require('async/waterfall')
9 8
10const constants = require('../../initializers/constants') 9const constants = require('../../initializers/constants')
10const db = require('../../initializers/database')
11const logger = require('../../helpers/logger') 11const logger = require('../../helpers/logger')
12const friends = require('../../lib/friends') 12const friends = require('../../lib/friends')
13const middlewares = require('../../middlewares') 13const middlewares = require('../../middlewares')
14const admin = middlewares.admin
14const oAuth = middlewares.oauth 15const oAuth = middlewares.oauth
15const pagination = middlewares.pagination 16const pagination = middlewares.pagination
16const validators = middlewares.validators 17const validators = middlewares.validators
@@ -22,7 +23,6 @@ const sort = middlewares.sort
22const utils = require('../../helpers/utils') 23const utils = require('../../helpers/utils')
23 24
24const router = express.Router() 25const router = express.Router()
25const Video = mongoose.model('Video')
26 26
27// multer configuration 27// multer configuration
28const storage = multer.diskStorage({ 28const storage = multer.diskStorage({
@@ -44,6 +44,21 @@ const storage = multer.diskStorage({
44 44
45const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }]) 45const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
46 46
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,
59 reportVideoAbuseRetryWrapper
60)
61
47router.get('/', 62router.get('/',
48 validatorsPagination.pagination, 63 validatorsPagination.pagination,
49 validatorsSort.videosSort, 64 validatorsSort.videosSort,
@@ -51,11 +66,17 @@ router.get('/',
51 pagination.setPagination, 66 pagination.setPagination,
52 listVideos 67 listVideos
53) 68)
69router.put('/:id',
70 oAuth.authenticate,
71 reqFiles,
72 validatorsVideos.videosUpdate,
73 updateVideoRetryWrapper
74)
54router.post('/', 75router.post('/',
55 oAuth.authenticate, 76 oAuth.authenticate,
56 reqFiles, 77 reqFiles,
57 validatorsVideos.videosAdd, 78 validatorsVideos.videosAdd,
58 addVideo 79 addVideoRetryWrapper
59) 80)
60router.get('/:id', 81router.get('/:id',
61 validatorsVideos.videosGet, 82 validatorsVideos.videosGet,
@@ -82,117 +103,264 @@ module.exports = router
82 103
83// --------------------------------------------------------------------------- 104// ---------------------------------------------------------------------------
84 105
85function addVideo (req, res, next) { 106// Wrapper to video add that retry the function if there is a database error
86 const videoFile = req.files.videofile[0] 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) {
87 const videoInfos = req.body 126 const videoInfos = req.body
88 127
89 waterfall([ 128 waterfall([
90 function createVideoObject (callback) {
91 const id = mongoose.Types.ObjectId()
92 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) {
93 const videoData = { 158 const videoData = {
94 _id: id,
95 name: videoInfos.name, 159 name: videoInfos.name,
96 remoteId: null, 160 remoteId: null,
97 extname: path.extname(videoFile.filename), 161 extname: path.extname(videoFile.filename),
98 description: videoInfos.description, 162 description: videoInfos.description,
99 author: res.locals.oauth.token.user.username,
100 duration: videoFile.duration, 163 duration: videoFile.duration,
101 tags: videoInfos.tags 164 authorId: author.id
102 } 165 }
103 166
104 const video = new Video(videoData) 167 const video = db.Video.build(videoData)
105 168
106 return callback(null, video) 169 return callbackWaterfall(null, t, author, tagInstances, video)
107 }, 170 },
108 171
109 // Set the videoname the same as the MongoDB id 172 // Set the videoname the same as the id
110 function renameVideoFile (video, callback) { 173 function renameVideoFile (t, author, tagInstances, video, callbackWaterfall) {
111 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR 174 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
112 const source = path.join(videoDir, videoFile.filename) 175 const source = path.join(videoDir, videoFile.filename)
113 const destination = path.join(videoDir, video.getVideoFilename()) 176 const destination = path.join(videoDir, video.getVideoFilename())
114 177
115 fs.rename(source, destination, function (err) { 178 fs.rename(source, destination, function (err) {
116 return callback(err, video) 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)
117 }) 198 })
118 }, 199 },
119 200
120 function insertIntoDB (video, callback) { 201 function associateTagsToVideo (t, tagInstances, video, callbackWaterfall) {
121 video.save(function (err, video) { 202 const options = { transaction: t }
122 // Assert there are only one argument sent to the next function (video) 203
123 return callback(err, video) 204 video.setTags(tagInstances, options).asCallback(function (err) {
205 video.Tags = tagInstances
206
207 return callbackWaterfall(err, t, video)
124 }) 208 })
125 }, 209 },
126 210
127 function sendToFriends (video, callback) { 211 function sendToFriends (t, video, callbackWaterfall) {
128 video.toRemoteJSON(function (err, remoteVideo) { 212 video.toAddRemoteJSON(function (err, remoteVideo) {
129 if (err) return callback(err) 213 if (err) return callbackWaterfall(err)
130 214
131 // Now we'll add the video's meta data to our friends 215 // Now we'll add the video's meta data to our friends
132 friends.addVideoToFriends(remoteVideo) 216 friends.addVideoToFriends(remoteVideo, t, function (err) {
133 217 return callbackWaterfall(err, t)
134 return callback(null) 218 })
135 }) 219 })
136 } 220 }
137 221
138 ], function andFinally (err) { 222 ], function andFinally (err, t) {
139 if (err) { 223 if (err) {
140 logger.error('Cannot insert the video.') 224 // This is just a debug because we will retry the insert
141 return next(err) 225 logger.debug('Cannot insert the video.', { error: err })
226
227 // Abort transaction?
228 if (t) t.rollback()
229
230 return callback(err)
142 } 231 }
143 232
144 // TODO : include Location of the new video -> 201 233 // Commit transaction
145 return res.type('json').status(204).end() 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 })
146 }) 240 })
147} 241}
148 242
149function getVideo (req, res, next) { 243function updateVideoRetryWrapper (req, res, next) {
150 Video.load(req.params.id, function (err, video) { 244 utils.transactionRetryer(
151 if (err) return next(err) 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 }
152 253
153 if (!video) { 254 // TODO : include Location of the new video -> 201
154 return res.type('json').status(204).end() 255 return res.type('json').status(204).end()
155 } 256 }
156 257 )
157 res.json(video.toFormatedJSON())
158 })
159} 258}
160 259
161function listVideos (req, res, next) { 260function updateVideo (req, res, finalCallback) {
162 Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) { 261 const videoInstance = res.locals.video
163 if (err) return next(err) 262 const videoFieldsSave = videoInstance.toJSON()
263 const videoInfosToUpdate = req.body
164 264
165 res.json(getFormatedVideos(videosList, videosTotal)) 265 waterfall([
166 })
167}
168 266
169function removeVideo (req, res, next) { 267 function startTransaction (callback) {
170 const videoId = req.params.id 268 db.sequelize.transaction({ isolationLevel: 'SERIALIZABLE' }).asCallback(function (err, t) {
269 return callback(err, t)
270 })
271 },
171 272
172 waterfall([ 273 function findOrCreateTags (t, callback) {
173 function getVideo (callback) { 274 if (videoInfosToUpdate.tags) {
174 Video.load(videoId, callback) 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 }
175 }, 281 },
176 282
177 function removeFromDB (video, callback) { 283 function updateVideoIntoDB (t, tagInstances, callback) {
178 video.remove(function (err) { 284 const options = {
179 if (err) return callback(err) 285 transaction: t
286 }
287
288 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
289 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
180 290
181 return callback(null, video) 291 videoInstance.save(options).asCallback(function (err) {
292 return callback(err, t, tagInstances)
182 }) 293 })
183 }, 294 },
184 295
185 function sendInformationToFriends (video, callback) { 296 function associateTagsToVideo (t, tagInstances, callback) {
186 const params = { 297 if (tagInstances) {
187 name: video.name, 298 const options = { transaction: t }
188 remoteId: video._id 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)
189 } 307 }
308 },
190 309
191 friends.removeVideoToFriends(params) 310 function sendToFriends (t, callback) {
311 const json = videoInstance.toUpdateRemoteJSON()
192 312
193 return callback(null) 313 // Now we'll update the video's meta data to our friends
314 friends.updateVideoToFriends(json, t, function (err) {
315 return callback(err, t)
316 })
317 }
318
319 ], function andFinally (err, t) {
320 if (err) {
321 logger.debug('Cannot update the video.', { error: err })
322
323 // Abort transaction?
324 if (t) t.rollback()
325
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
334 return finalCallback(err)
194 } 335 }
195 ], function andFinally (err) { 336
337 // Commit transaction
338 t.commit().asCallback(function (err) {
339 if (err) return finalCallback(err)
340
341 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
342 return finalCallback(null)
343 })
344 })
345}
346
347function getVideo (req, res, next) {
348 const videoInstance = res.locals.video
349 res.json(videoInstance.toFormatedJSON())
350}
351
352function listVideos (req, res, next) {
353 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
354 if (err) return next(err)
355
356 res.json(utils.getFormatedObjects(videosList, videosTotal))
357 })
358}
359
360function removeVideo (req, res, next) {
361 const videoInstance = res.locals.video
362
363 videoInstance.destroy().asCallback(function (err) {
196 if (err) { 364 if (err) {
197 logger.error('Errors when removed the video.', { error: err }) 365 logger.error('Errors when removed the video.', { error: err })
198 return next(err) 366 return next(err)
@@ -203,25 +371,97 @@ function removeVideo (req, res, next) {
203} 371}
204 372
205function searchVideos (req, res, next) { 373function searchVideos (req, res, next) {
206 Video.search(req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort, 374 db.Video.searchAndPopulateAuthorAndPodAndTags(
207 function (err, videosList, videosTotal) { 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)
378
379 res.json(utils.getFormatedObjects(videosList, videosTotal))
380 }
381 )
382}
383
384function listVideoAbuses (req, res, next) {
385 db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) {
208 if (err) return next(err) 386 if (err) return next(err)
209 387
210 res.json(getFormatedVideos(videosList, videosTotal)) 388 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
211 }) 389 })
212} 390}
213 391
214// --------------------------------------------------------------------------- 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 }
215 402
216function getFormatedVideos (videos, videosTotal) { 403 return res.type('json').status(204).end()
217 const formatedVideos = [] 404 }
405 )
406}
218 407
219 videos.forEach(function (video) { 408function reportVideoAbuse (req, res, finalCallback) {
220 formatedVideos.push(video.toFormatedJSON()) 409 const videoInstance = res.locals.video
221 }) 410 const reporterUsername = res.locals.oauth.token.User.username
222 411
223 return { 412 const abuse = {
224 total: videosTotal, 413 reporterUsername,
225 data: formatedVideos 414 reason: req.body.reason,
415 videoId: videoInstance.id,
416 reporterPodId: null // This is our pod that reported this abuse
226 } 417 }
418
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 }
441
442 friends.reportAbuseVideoToFriend(reportData, videoInstance)
443 }
444
445 return callback(null, t)
446 }
447
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
459 t.commit().asCallback(function (err) {
460 if (err) return finalCallback(err)
461
462 logger.info('Abuse report for video %s created.', videoInstance.name)
463 return finalCallback(null)
464 })
465 })
227} 466}
467