]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/videos.js
Server: fix update remote video
[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
4df023f2 23const databaseUtils = require('../../helpers/database-utils')
f253b1c1 24const utils = require('../../helpers/utils')
f0f5567b
C
25
26const router = express.Router()
9f10b292
C
27
28// multer configuration
f0f5567b 29const storage = multer.diskStorage({
9f10b292 30 destination: function (req, file, cb) {
b3d92510 31 cb(null, constants.CONFIG.STORAGE.VIDEOS_DIR)
9f10b292
C
32 },
33
34 filename: function (req, file, cb) {
f0f5567b 35 let extension = ''
9f10b292
C
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'
bc503c2a
C
39 utils.generateRandomString(16, function (err, randomString) {
40 const fieldname = err ? undefined : randomString
9f10b292
C
41 cb(null, fieldname + '.' + extension)
42 })
43 }
44})
45
8c9c1942 46const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
8c308c2b 47
55fa55a9
C
48router.get('/abuse',
49 oAuth.authenticate,
50 admin.ensureIsAdmin,
51 validatorsPagination.pagination,
52 validatorsSort.videoAbusesSort,
53 sort.setVideoAbusesSort,
54 pagination.setPagination,
55 listVideoAbuses
56)
57router.post('/:id/abuse',
58 oAuth.authenticate,
59 validatorsVideos.videoAbuseReport,
bf4ff8fe 60 reportVideoAbuseRetryWrapper
55fa55a9
C
61)
62
fbf1134e 63router.get('/',
fc51fde0
C
64 validatorsPagination.pagination,
65 validatorsSort.videosSort,
a877d5ac 66 sort.setVideosSort,
fbf1134e
C
67 pagination.setPagination,
68 listVideos
69)
7b1f49de
C
70router.put('/:id',
71 oAuth.authenticate,
72 reqFiles,
73 validatorsVideos.videosUpdate,
ed04d94f 74 updateVideoRetryWrapper
7b1f49de 75)
fbf1134e 76router.post('/',
69b0a27c 77 oAuth.authenticate,
fbf1134e 78 reqFiles,
fc51fde0 79 validatorsVideos.videosAdd,
ed04d94f 80 addVideoRetryWrapper
fbf1134e
C
81)
82router.get('/:id',
fc51fde0 83 validatorsVideos.videosGet,
68ce3ae0 84 getVideo
fbf1134e
C
85)
86router.delete('/:id',
69b0a27c 87 oAuth.authenticate,
fc51fde0 88 validatorsVideos.videosRemove,
fbf1134e
C
89 removeVideo
90)
46246b5f 91router.get('/search/:value',
fc51fde0
C
92 validatorsVideos.videosSearch,
93 validatorsPagination.pagination,
94 validatorsSort.videosSort,
a877d5ac 95 sort.setVideosSort,
fbf1134e 96 pagination.setPagination,
46246b5f 97 search.setVideosSearch,
fbf1134e
C
98 searchVideos
99)
8c308c2b 100
9f10b292 101// ---------------------------------------------------------------------------
c45f7f84 102
9f10b292 103module.exports = router
c45f7f84 104
9f10b292 105// ---------------------------------------------------------------------------
c45f7f84 106
ed04d94f
C
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
109function addVideoRetryWrapper (req, res, next) {
d6a5b018
C
110 const options = {
111 arguments: [ req, res, req.files.videofile[0] ],
112 errorMessage: 'Cannot insert the video with many retries.'
113 }
ed04d94f 114
4df023f2 115 databaseUtils.retryTransactionWrapper(addVideo, options, function (err) {
d6a5b018
C
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 })
ed04d94f
C
121}
122
123function addVideo (req, res, videoFile, callback) {
bc503c2a 124 const videoInfos = req.body
9f10b292 125
1a42c9e2 126 waterfall([
807df9e6 127
4df023f2 128 databaseUtils.startSerializableTransaction,
7920c273 129
ed04d94f 130 function findOrCreateAuthor (t, callbackWaterfall) {
4712081f 131 const user = res.locals.oauth.token.User
feb4bdfd 132
4ff0d862
C
133 const name = user.username
134 // null because it is OUR pod
135 const podId = null
136 const userId = user.id
4712081f 137
4ff0d862 138 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
ed04d94f 139 return callbackWaterfall(err, t, authorInstance)
7920c273
C
140 })
141 },
142
ed04d94f 143 function findOrCreateTags (t, author, callbackWaterfall) {
7920c273 144 const tags = videoInfos.tags
4ff0d862
C
145
146 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
ed04d94f 147 return callbackWaterfall(err, t, author, tagInstances)
feb4bdfd
C
148 })
149 },
150
ed04d94f 151 function createVideoObject (t, author, tagInstances, callbackWaterfall) {
807df9e6
C
152 const videoData = {
153 name: videoInfos.name,
558d7c23
C
154 remoteId: null,
155 extname: path.extname(videoFile.filename),
807df9e6 156 description: videoInfos.description,
67100f1f 157 duration: videoFile.duration,
feb4bdfd 158 authorId: author.id
807df9e6
C
159 }
160
feb4bdfd 161 const video = db.Video.build(videoData)
558d7c23 162
ed04d94f 163 return callbackWaterfall(null, t, author, tagInstances, video)
558d7c23
C
164 },
165
feb4bdfd 166 // Set the videoname the same as the id
ed04d94f 167 function renameVideoFile (t, author, tagInstances, video, callbackWaterfall) {
558d7c23
C
168 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
169 const source = path.join(videoDir, videoFile.filename)
f285faa0 170 const destination = path.join(videoDir, video.getVideoFilename())
558d7c23
C
171
172 fs.rename(source, destination, function (err) {
ed04d94f
C
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)
558d7c23
C
178 })
179 },
180
ed04d94f 181 function insertVideoIntoDB (t, author, tagInstances, video, callbackWaterfall) {
7920c273
C
182 const options = { transaction: t }
183
184 // Add tags association
185 video.save(options).asCallback(function (err, videoCreated) {
ed04d94f 186 if (err) return callbackWaterfall(err)
7920c273 187
feb4bdfd
C
188 // Do not forget to add Author informations to the created video
189 videoCreated.Author = author
190
ed04d94f 191 return callbackWaterfall(err, t, tagInstances, videoCreated)
3a8a8b51 192 })
807df9e6
C
193 },
194
ed04d94f 195 function associateTagsToVideo (t, tagInstances, video, callbackWaterfall) {
7920c273
C
196 const options = { transaction: t }
197
198 video.setTags(tagInstances, options).asCallback(function (err) {
199 video.Tags = tagInstances
200
ed04d94f 201 return callbackWaterfall(err, t, video)
7920c273
C
202 })
203 },
204
ed04d94f 205 function sendToFriends (t, video, callbackWaterfall) {
7b1f49de 206 video.toAddRemoteJSON(function (err, remoteVideo) {
ed04d94f 207 if (err) return callbackWaterfall(err)
807df9e6 208
528a9efa 209 // Now we'll add the video's meta data to our friends
ed04d94f
C
210 friends.addVideoToFriends(remoteVideo, t, function (err) {
211 return callbackWaterfall(err, t)
212 })
528a9efa 213 })
807df9e6
C
214 }
215
7b1f49de
C
216 ], function andFinally (err, t) {
217 if (err) {
ed04d94f
C
218 // This is just a debug because we will retry the insert
219 logger.debug('Cannot insert the video.', { error: err })
7b1f49de
C
220
221 // Abort transaction?
222 if (t) t.rollback()
223
ed04d94f 224 return callback(err)
7b1f49de
C
225 }
226
227 // Commit transaction
dea32aac
C
228 t.commit().asCallback(function (err) {
229 if (err) return callback(err)
7b1f49de 230
dea32aac
C
231 logger.info('Video with name %s created.', videoInfos.name)
232 return callback(null)
233 })
7b1f49de
C
234 })
235}
236
ed04d94f 237function updateVideoRetryWrapper (req, res, next) {
d6a5b018
C
238 const options = {
239 arguments: [ req, res ],
240 errorMessage: 'Cannot update the video with many retries.'
241 }
ed04d94f 242
4df023f2 243 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
d6a5b018
C
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 })
ed04d94f
C
249}
250
251function updateVideo (req, res, finalCallback) {
818f7987 252 const videoInstance = res.locals.video
7f4e7c36 253 const videoFieldsSave = videoInstance.toJSON()
7b1f49de
C
254 const videoInfosToUpdate = req.body
255
256 waterfall([
257
4df023f2 258 databaseUtils.startSerializableTransaction,
7b1f49de
C
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) {
7f4e7c36
C
271 const options = {
272 transaction: t
273 }
7b1f49de
C
274
275 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
276 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
277
7b1f49de 278 videoInstance.save(options).asCallback(function (err) {
7b1f49de
C
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
ed04d94f
C
301 friends.updateVideoToFriends(json, t, function (err) {
302 return callback(err, t)
303 })
7b1f49de
C
304 }
305
7920c273 306 ], function andFinally (err, t) {
807df9e6 307 if (err) {
ed04d94f 308 logger.debug('Cannot update the video.', { error: err })
7920c273
C
309
310 // Abort transaction?
311 if (t) t.rollback()
312
7f4e7c36
C
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
ed04d94f 321 return finalCallback(err)
807df9e6
C
322 }
323
7920c273 324 // Commit transaction
dea32aac
C
325 t.commit().asCallback(function (err) {
326 if (err) return finalCallback(err)
7920c273 327
dea32aac
C
328 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
329 return finalCallback(null)
330 })
9f10b292
C
331 })
332}
8c308c2b 333
68ce3ae0 334function getVideo (req, res, next) {
818f7987
C
335 const videoInstance = res.locals.video
336 res.json(videoInstance.toFormatedJSON())
9f10b292 337}
8c308c2b 338
9f10b292 339function listVideos (req, res, next) {
feb4bdfd 340 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
9f10b292 341 if (err) return next(err)
c45f7f84 342
55fa55a9 343 res.json(utils.getFormatedObjects(videosList, videosTotal))
9f10b292
C
344 })
345}
c45f7f84 346
9f10b292 347function removeVideo (req, res, next) {
818f7987 348 const videoInstance = res.locals.video
8c308c2b 349
818f7987 350 videoInstance.destroy().asCallback(function (err) {
807df9e6
C
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()
9f10b292
C
357 })
358}
8c308c2b 359
9f10b292 360function searchVideos (req, res, next) {
7920c273
C
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)
8c308c2b 365
55fa55a9 366 res.json(utils.getFormatedObjects(videosList, videosTotal))
7920c273
C
367 }
368 )
9f10b292 369}
c173e565 370
55fa55a9
C
371function 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)
2df82d42 374
55fa55a9 375 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
2df82d42 376 })
55fa55a9 377}
2df82d42 378
bf4ff8fe 379function reportVideoAbuseRetryWrapper (req, res, next) {
4df023f2
C
380 const options = {
381 arguments: [ req, res ],
382 errorMessage: 'Cannot report abuse to the video with many retries.'
383 }
bf4ff8fe 384
4df023f2
C
385 databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) {
386 if (err) return next(err)
387
388 return res.type('json').status(204).end()
389 })
bf4ff8fe
C
390}
391
392function reportVideoAbuse (req, res, finalCallback) {
55fa55a9
C
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
68ce3ae0 401 }
55fa55a9 402
bf4ff8fe
C
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 }
55fa55a9 425
bf4ff8fe 426 friends.reportAbuseVideoToFriend(reportData, videoInstance)
55fa55a9
C
427 }
428
bf4ff8fe 429 return callback(null, t)
55fa55a9
C
430 }
431
bf4ff8fe
C
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
dea32aac
C
443 t.commit().asCallback(function (err) {
444 if (err) return finalCallback(err)
bf4ff8fe 445
dea32aac
C
446 logger.info('Abuse report for video %s created.', videoInstance.name)
447 return finalCallback(null)
448 })
55fa55a9 449 })
2df82d42 450}
55fa55a9 451