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