]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/remote/videos.js
Server: transaction serializable for videos
[github/Chocobozzz/PeerTube.git] / server / controllers / api / remote / videos.js
CommitLineData
528a9efa
C
1'use strict'
2
1a42c9e2 3const eachSeries = require('async/eachSeries')
528a9efa 4const express = require('express')
feb4bdfd 5const waterfall = require('async/waterfall')
528a9efa 6
a6fd2b30
C
7const db = require('../../../initializers/database')
8const middlewares = require('../../../middlewares')
528a9efa 9const secureMiddleware = middlewares.secure
55fa55a9
C
10const videosValidators = middlewares.validators.remote.videos
11const signatureValidators = middlewares.validators.remote.signature
a6fd2b30 12const logger = require('../../../helpers/logger')
ed04d94f 13const utils = require('../../../helpers/utils')
528a9efa
C
14
15const router = express.Router()
16
a6fd2b30 17router.post('/',
55fa55a9 18 signatureValidators.signature,
0eb78d53 19 secureMiddleware.checkSignature,
55fa55a9 20 videosValidators.remoteVideos,
528a9efa
C
21 remoteVideos
22)
23
24// ---------------------------------------------------------------------------
25
26module.exports = router
27
28// ---------------------------------------------------------------------------
29
30function remoteVideos (req, res, next) {
31 const requests = req.body.data
4ff0d862 32 const fromPod = res.locals.secure.pod
528a9efa
C
33
34 // We need to process in the same order to keep consistency
35 // TODO: optimization
1a42c9e2 36 eachSeries(requests, function (request, callbackEach) {
55fa55a9 37 const data = request.data
528a9efa 38
3d118fb5
C
39 switch (request.type) {
40 case 'add':
ed04d94f 41 addRemoteVideoRetryWrapper(data, fromPod, callbackEach)
3d118fb5
C
42 break
43
44 case 'update':
ed04d94f 45 updateRemoteVideoRetryWrapper(data, fromPod, callbackEach)
3d118fb5
C
46 break
47
48 case 'remove':
55fa55a9
C
49 removeRemoteVideo(data, fromPod, callbackEach)
50 break
51
52 case 'report-abuse':
53 reportAbuseRemoteVideo(data, fromPod, callbackEach)
3d118fb5
C
54 break
55
56 default:
57 logger.error('Unkown remote request type %s.', request.type)
528a9efa 58 }
aaf61f38
C
59 }, function (err) {
60 if (err) logger.error('Error managing remote videos.', { error: err })
528a9efa
C
61 })
62
63 // We don't need to keep the other pod waiting
64 return res.type('json').status(204).end()
65}
66
ed04d94f
C
67// Handle retries on fail
68function addRemoteVideoRetryWrapper (videoToCreateData, fromPod, finalCallback) {
69 utils.transactionRetryer(
70 function (callback) {
71 return addRemoteVideo(videoToCreateData, fromPod, callback)
72 },
73 function (err) {
74 if (err) {
75 logger.error('Cannot insert the remote video with many retries.', { error: err })
ed04d94f
C
76 }
77
d8cc063e
C
78 // Do not return the error, continue the process
79 return finalCallback(null)
ed04d94f
C
80 }
81 )
82}
83
4ff0d862 84function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
ed04d94f 85 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
6666aad4 86
feb4bdfd
C
87 waterfall([
88
7920c273 89 function startTransaction (callback) {
ed04d94f 90 db.sequelize.transaction({ isolationLevel: 'SERIALIZABLE' }).asCallback(function (err, t) {
7920c273
C
91 return callback(err, t)
92 })
93 },
94
4ff0d862
C
95 function findOrCreateAuthor (t, callback) {
96 const name = videoToCreateData.author
97 const podId = fromPod.id
98 // This author is from another pod so we do not associate a user
99 const userId = null
feb4bdfd 100
4ff0d862
C
101 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
102 return callback(err, t, authorInstance)
feb4bdfd
C
103 })
104 },
105
7920c273
C
106 function findOrCreateTags (t, author, callback) {
107 const tags = videoToCreateData.tags
7920c273 108
4ff0d862 109 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
7920c273
C
110 return callback(err, t, author, tagInstances)
111 })
112 },
113
114 function createVideoObject (t, author, tagInstances, callback) {
feb4bdfd
C
115 const videoData = {
116 name: videoToCreateData.name,
117 remoteId: videoToCreateData.remoteId,
118 extname: videoToCreateData.extname,
119 infoHash: videoToCreateData.infoHash,
120 description: videoToCreateData.description,
121 authorId: author.id,
124648d7 122 duration: videoToCreateData.duration,
79066fdf 123 createdAt: videoToCreateData.createdAt,
ed04d94f 124 // FIXME: updatedAt does not seems to be considered by Sequelize
79066fdf 125 updatedAt: videoToCreateData.updatedAt
feb4bdfd
C
126 }
127
128 const video = db.Video.build(videoData)
129
7920c273 130 return callback(null, t, tagInstances, video)
feb4bdfd
C
131 },
132
7920c273 133 function generateThumbnail (t, tagInstances, video, callback) {
4d324488 134 db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) {
feb4bdfd 135 if (err) {
4d324488 136 logger.error('Cannot generate thumbnail from data.', { error: err })
feb4bdfd
C
137 return callback(err)
138 }
139
7920c273 140 return callback(err, t, tagInstances, video)
feb4bdfd
C
141 })
142 },
143
7920c273
C
144 function insertVideoIntoDB (t, tagInstances, video, callback) {
145 const options = {
146 transaction: t
147 }
148
149 video.save(options).asCallback(function (err, videoCreated) {
150 return callback(err, t, tagInstances, videoCreated)
151 })
152 },
153
154 function associateTagsToVideo (t, tagInstances, video, callback) {
155 const options = { transaction: t }
156
157 video.setTags(tagInstances, options).asCallback(function (err) {
158 return callback(err, t)
159 })
c77fa067
C
160 }
161
7920c273
C
162 ], function (err, t) {
163 if (err) {
ed04d94f
C
164 // This is just a debug because we will retry the insert
165 logger.debug('Cannot insert the remote video.', { error: err })
7920c273
C
166
167 // Abort transaction?
168 if (t) t.rollback()
169
170 return finalCallback(err)
171 }
172
173 // Commit transaction
dea32aac
C
174 t.commit().asCallback(function (err) {
175 if (err) return finalCallback(err)
7920c273 176
d8cc063e 177 logger.info('Remote video %s inserted.', videoToCreateData.name)
dea32aac
C
178 return finalCallback(null)
179 })
7920c273 180 })
528a9efa
C
181}
182
ed04d94f
C
183// Handle retries on fail
184function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) {
185 utils.transactionRetryer(
186 function (callback) {
187 return updateRemoteVideo(videoAttributesToUpdate, fromPod, callback)
188 },
189 function (err) {
190 if (err) {
191 logger.error('Cannot update the remote video with many retries.', { error: err })
ed04d94f
C
192 }
193
d8cc063e
C
194 // Do not return the error, continue the process
195 return finalCallback(null)
ed04d94f
C
196 }
197 )
198}
199
3d118fb5 200function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
ed04d94f 201 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
feb4bdfd 202
3d118fb5
C
203 waterfall([
204
205 function startTransaction (callback) {
edc5e860 206 db.sequelize.transaction({ isolationLevel: 'SERIALIZABLE' }).asCallback(function (err, t) {
3d118fb5
C
207 return callback(err, t)
208 })
209 },
210
211 function findVideo (t, callback) {
55fa55a9
C
212 fetchVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
213 return callback(err, t, videoInstance)
3d118fb5
C
214 })
215 },
216
217 function findOrCreateTags (t, videoInstance, callback) {
218 const tags = videoAttributesToUpdate.tags
219
220 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
221 return callback(err, t, videoInstance, tagInstances)
222 })
223 },
224
225 function updateVideoIntoDB (t, videoInstance, tagInstances, callback) {
226 const options = { transaction: t }
227
228 videoInstance.set('name', videoAttributesToUpdate.name)
229 videoInstance.set('description', videoAttributesToUpdate.description)
230 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
231 videoInstance.set('duration', videoAttributesToUpdate.duration)
232 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
79066fdf 233 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
3d118fb5
C
234 videoInstance.set('extname', videoAttributesToUpdate.extname)
235
236 videoInstance.save(options).asCallback(function (err) {
237 return callback(err, t, videoInstance, tagInstances)
238 })
239 },
240
241 function associateTagsToVideo (t, videoInstance, tagInstances, callback) {
242 const options = { transaction: t }
243
244 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
245 return callback(err, t)
246 })
528a9efa
C
247 }
248
3d118fb5
C
249 ], function (err, t) {
250 if (err) {
ed04d94f
C
251 // This is just a debug because we will retry the insert
252 logger.debug('Cannot update the remote video.', { error: err })
3d118fb5
C
253
254 // Abort transaction?
255 if (t) t.rollback()
256
257 return finalCallback(err)
6666aad4
C
258 }
259
dea32aac
C
260 // Commit transaction
261 t.commit().asCallback(function (err) {
262 if (err) return finalCallback(err)
3d118fb5 263
dea32aac
C
264 logger.info('Remote video %s updated', videoAttributesToUpdate.name)
265 return finalCallback(null)
266 })
3d118fb5
C
267 })
268}
269
270function removeRemoteVideo (videoToRemoveData, fromPod, callback) {
271 // We need the instance because we have to remove some other stuffs (thumbnail etc)
55fa55a9 272 fetchVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) {
d8cc063e
C
273 // Do not return the error, continue the process
274 if (err) return callback(null)
55fa55a9
C
275
276 logger.debug('Removing remote video %s.', video.remoteId)
d8cc063e
C
277 video.destroy().asCallback(function (err) {
278 // Do not return the error, continue the process
279 if (err) {
280 logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err })
281 }
282
283 return callback(null)
284 })
55fa55a9
C
285 })
286}
287
288function reportAbuseRemoteVideo (reportData, fromPod, callback) {
289 db.Video.load(reportData.videoRemoteId, function (err, video) {
3d118fb5 290 if (err || !video) {
55fa55a9
C
291 if (!err) err = new Error('video not found')
292
ed04d94f 293 logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId })
d8cc063e
C
294 // Do not return the error, continue the process
295 return callback(null)
3d118fb5 296 }
6666aad4 297
55fa55a9
C
298 logger.debug('Reporting remote abuse for video %s.', video.id)
299
300 const videoAbuseData = {
301 reporterUsername: reportData.reporterUsername,
302 reason: reportData.reportReason,
303 reporterPodId: fromPod.id,
304 videoId: video.id
305 }
306
d8cc063e
C
307 db.VideoAbuse.create(videoAbuseData).asCallback(function (err) {
308 if (err) {
309 logger.error('Cannot create remote abuse video.', { error: err })
310 }
311
312 return callback(null)
313 })
55fa55a9
C
314 })
315}
316
317function fetchVideo (podHost, remoteId, callback) {
318 db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) {
319 if (err || !video) {
320 if (!err) err = new Error('video not found')
321
ed04d94f 322 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
55fa55a9
C
323 return callback(err)
324 }
325
326 return callback(null, video)
528a9efa
C
327 })
328}