diff options
Diffstat (limited to 'server/controllers/api/videos.js')
-rw-r--r-- | server/controllers/api/videos.js | 382 |
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 | ||
3 | const express = require('express') | 3 | const express = require('express') |
4 | const fs = require('fs') | 4 | const fs = require('fs') |
5 | const mongoose = require('mongoose') | ||
6 | const multer = require('multer') | 5 | const multer = require('multer') |
7 | const path = require('path') | 6 | const path = require('path') |
8 | const waterfall = require('async/waterfall') | 7 | const waterfall = require('async/waterfall') |
9 | 8 | ||
10 | const constants = require('../../initializers/constants') | 9 | const constants = require('../../initializers/constants') |
10 | const db = require('../../initializers/database') | ||
11 | const logger = require('../../helpers/logger') | 11 | const logger = require('../../helpers/logger') |
12 | const friends = require('../../lib/friends') | 12 | const friends = require('../../lib/friends') |
13 | const middlewares = require('../../middlewares') | 13 | const middlewares = require('../../middlewares') |
14 | const admin = middlewares.admin | ||
14 | const oAuth = middlewares.oauth | 15 | const oAuth = middlewares.oauth |
15 | const pagination = middlewares.pagination | 16 | const pagination = middlewares.pagination |
16 | const validators = middlewares.validators | 17 | const validators = middlewares.validators |
@@ -22,7 +23,6 @@ const sort = middlewares.sort | |||
22 | const utils = require('../../helpers/utils') | 23 | const utils = require('../../helpers/utils') |
23 | 24 | ||
24 | const router = express.Router() | 25 | const router = express.Router() |
25 | const Video = mongoose.model('Video') | ||
26 | 26 | ||
27 | // multer configuration | 27 | // multer configuration |
28 | const storage = multer.diskStorage({ | 28 | const storage = multer.diskStorage({ |
@@ -44,6 +44,21 @@ const storage = multer.diskStorage({ | |||
44 | 44 | ||
45 | const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }]) | 45 | const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }]) |
46 | 46 | ||
47 | router.get('/abuse', | ||
48 | oAuth.authenticate, | ||
49 | admin.ensureIsAdmin, | ||
50 | validatorsPagination.pagination, | ||
51 | validatorsSort.videoAbusesSort, | ||
52 | sort.setVideoAbusesSort, | ||
53 | pagination.setPagination, | ||
54 | listVideoAbuses | ||
55 | ) | ||
56 | router.post('/:id/abuse', | ||
57 | oAuth.authenticate, | ||
58 | validatorsVideos.videoAbuseReport, | ||
59 | reportVideoAbuseRetryWrapper | ||
60 | ) | ||
61 | |||
47 | router.get('/', | 62 | router.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 | ) |
69 | router.put('/:id', | ||
70 | oAuth.authenticate, | ||
71 | reqFiles, | ||
72 | validatorsVideos.videosUpdate, | ||
73 | updateVideoRetryWrapper | ||
74 | ) | ||
54 | router.post('/', | 75 | router.post('/', |
55 | oAuth.authenticate, | 76 | oAuth.authenticate, |
56 | reqFiles, | 77 | reqFiles, |
57 | validatorsVideos.videosAdd, | 78 | validatorsVideos.videosAdd, |
58 | addVideo | 79 | addVideoRetryWrapper |
59 | ) | 80 | ) |
60 | router.get('/:id', | 81 | router.get('/:id', |
61 | validatorsVideos.videosGet, | 82 | validatorsVideos.videosGet, |
@@ -82,117 +103,264 @@ module.exports = router | |||
82 | 103 | ||
83 | // --------------------------------------------------------------------------- | 104 | // --------------------------------------------------------------------------- |
84 | 105 | ||
85 | function 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 |
108 | function 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 | |||
125 | function 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 | ||
149 | function getVideo (req, res, next) { | 243 | function 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 | ||
161 | function listVideos (req, res, next) { | 260 | function 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 | ||
169 | function 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 | |||
347 | function getVideo (req, res, next) { | ||
348 | const videoInstance = res.locals.video | ||
349 | res.json(videoInstance.toFormatedJSON()) | ||
350 | } | ||
351 | |||
352 | function 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 | |||
360 | function 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 | ||
205 | function searchVideos (req, res, next) { | 373 | function 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 | |||
384 | function 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 | // --------------------------------------------------------------------------- | 392 | function 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 | ||
216 | function getFormatedVideos (videos, videosTotal) { | 403 | return res.type('json').status(204).end() |
217 | const formatedVideos = [] | 404 | } |
405 | ) | ||
406 | } | ||
218 | 407 | ||
219 | videos.forEach(function (video) { | 408 | function 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 | |||