diff options
Diffstat (limited to 'server/controllers/api/remote')
-rw-r--r-- | server/controllers/api/remote/index.js | 16 | ||||
-rw-r--r-- | server/controllers/api/remote/videos.js | 328 |
2 files changed, 344 insertions, 0 deletions
diff --git a/server/controllers/api/remote/index.js b/server/controllers/api/remote/index.js new file mode 100644 index 000000000..2947632d5 --- /dev/null +++ b/server/controllers/api/remote/index.js | |||
@@ -0,0 +1,16 @@ | |||
1 | 'use strict' | ||
2 | |||
3 | const express = require('express') | ||
4 | |||
5 | const utils = require('../../../helpers/utils') | ||
6 | |||
7 | const router = express.Router() | ||
8 | |||
9 | const videosRemoteController = require('./videos') | ||
10 | |||
11 | router.use('/videos', videosRemoteController) | ||
12 | router.use('/*', utils.badRequest) | ||
13 | |||
14 | // --------------------------------------------------------------------------- | ||
15 | |||
16 | module.exports = router | ||
diff --git a/server/controllers/api/remote/videos.js b/server/controllers/api/remote/videos.js new file mode 100644 index 000000000..c45a86dbb --- /dev/null +++ b/server/controllers/api/remote/videos.js | |||
@@ -0,0 +1,328 @@ | |||
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 middlewares = require('../../../middlewares') | ||
9 | const secureMiddleware = middlewares.secure | ||
10 | const videosValidators = middlewares.validators.remote.videos | ||
11 | const signatureValidators = middlewares.validators.remote.signature | ||
12 | const logger = require('../../../helpers/logger') | ||
13 | const utils = require('../../../helpers/utils') | ||
14 | |||
15 | const router = express.Router() | ||
16 | |||
17 | router.post('/', | ||
18 | signatureValidators.signature, | ||
19 | secureMiddleware.checkSignature, | ||
20 | videosValidators.remoteVideos, | ||
21 | remoteVideos | ||
22 | ) | ||
23 | |||
24 | // --------------------------------------------------------------------------- | ||
25 | |||
26 | module.exports = router | ||
27 | |||
28 | // --------------------------------------------------------------------------- | ||
29 | |||
30 | function remoteVideos (req, res, next) { | ||
31 | const requests = req.body.data | ||
32 | const fromPod = res.locals.secure.pod | ||
33 | |||
34 | // We need to process in the same order to keep consistency | ||
35 | // TODO: optimization | ||
36 | eachSeries(requests, function (request, callbackEach) { | ||
37 | const data = request.data | ||
38 | |||
39 | switch (request.type) { | ||
40 | case 'add': | ||
41 | addRemoteVideoRetryWrapper(data, fromPod, callbackEach) | ||
42 | break | ||
43 | |||
44 | case 'update': | ||
45 | updateRemoteVideoRetryWrapper(data, fromPod, callbackEach) | ||
46 | break | ||
47 | |||
48 | case 'remove': | ||
49 | removeRemoteVideo(data, fromPod, callbackEach) | ||
50 | break | ||
51 | |||
52 | case 'report-abuse': | ||
53 | reportAbuseRemoteVideo(data, fromPod, callbackEach) | ||
54 | break | ||
55 | |||
56 | default: | ||
57 | logger.error('Unkown remote request type %s.', request.type) | ||
58 | } | ||
59 | }, function (err) { | ||
60 | if (err) logger.error('Error managing remote videos.', { error: err }) | ||
61 | }) | ||
62 | |||
63 | // We don't need to keep the other pod waiting | ||
64 | return res.type('json').status(204).end() | ||
65 | } | ||
66 | |||
67 | // Handle retries on fail | ||
68 | function 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 }) | ||
76 | } | ||
77 | |||
78 | // Do not return the error, continue the process | ||
79 | return finalCallback(null) | ||
80 | } | ||
81 | ) | ||
82 | } | ||
83 | |||
84 | function addRemoteVideo (videoToCreateData, fromPod, finalCallback) { | ||
85 | logger.debug('Adding remote video "%s".', videoToCreateData.remoteId) | ||
86 | |||
87 | waterfall([ | ||
88 | |||
89 | function startTransaction (callback) { | ||
90 | db.sequelize.transaction({ isolationLevel: 'SERIALIZABLE' }).asCallback(function (err, t) { | ||
91 | return callback(err, t) | ||
92 | }) | ||
93 | }, | ||
94 | |||
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 | ||
100 | |||
101 | db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) { | ||
102 | return callback(err, t, authorInstance) | ||
103 | }) | ||
104 | }, | ||
105 | |||
106 | function findOrCreateTags (t, author, callback) { | ||
107 | const tags = videoToCreateData.tags | ||
108 | |||
109 | db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { | ||
110 | return callback(err, t, author, tagInstances) | ||
111 | }) | ||
112 | }, | ||
113 | |||
114 | function createVideoObject (t, author, tagInstances, callback) { | ||
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, | ||
122 | duration: videoToCreateData.duration, | ||
123 | createdAt: videoToCreateData.createdAt, | ||
124 | // FIXME: updatedAt does not seems to be considered by Sequelize | ||
125 | updatedAt: videoToCreateData.updatedAt | ||
126 | } | ||
127 | |||
128 | const video = db.Video.build(videoData) | ||
129 | |||
130 | return callback(null, t, tagInstances, video) | ||
131 | }, | ||
132 | |||
133 | function generateThumbnail (t, tagInstances, video, callback) { | ||
134 | db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) { | ||
135 | if (err) { | ||
136 | logger.error('Cannot generate thumbnail from data.', { error: err }) | ||
137 | return callback(err) | ||
138 | } | ||
139 | |||
140 | return callback(err, t, tagInstances, video) | ||
141 | }) | ||
142 | }, | ||
143 | |||
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 | }) | ||
160 | } | ||
161 | |||
162 | ], function (err, t) { | ||
163 | if (err) { | ||
164 | // This is just a debug because we will retry the insert | ||
165 | logger.debug('Cannot insert the remote video.', { error: err }) | ||
166 | |||
167 | // Abort transaction? | ||
168 | if (t) t.rollback() | ||
169 | |||
170 | return finalCallback(err) | ||
171 | } | ||
172 | |||
173 | // Commit transaction | ||
174 | t.commit().asCallback(function (err) { | ||
175 | if (err) return finalCallback(err) | ||
176 | |||
177 | logger.info('Remote video %s inserted.', videoToCreateData.name) | ||
178 | return finalCallback(null) | ||
179 | }) | ||
180 | }) | ||
181 | } | ||
182 | |||
183 | // Handle retries on fail | ||
184 | function 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 }) | ||
192 | } | ||
193 | |||
194 | // Do not return the error, continue the process | ||
195 | return finalCallback(null) | ||
196 | } | ||
197 | ) | ||
198 | } | ||
199 | |||
200 | function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) { | ||
201 | logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId) | ||
202 | |||
203 | waterfall([ | ||
204 | |||
205 | function startTransaction (callback) { | ||
206 | db.sequelize.transaction({ isolationLevel: 'SERIALIZABLE' }).asCallback(function (err, t) { | ||
207 | return callback(err, t) | ||
208 | }) | ||
209 | }, | ||
210 | |||
211 | function findVideo (t, callback) { | ||
212 | fetchVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) { | ||
213 | return callback(err, t, videoInstance) | ||
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) | ||
233 | videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) | ||
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 | }) | ||
247 | } | ||
248 | |||
249 | ], function (err, t) { | ||
250 | if (err) { | ||
251 | // This is just a debug because we will retry the insert | ||
252 | logger.debug('Cannot update the remote video.', { error: err }) | ||
253 | |||
254 | // Abort transaction? | ||
255 | if (t) t.rollback() | ||
256 | |||
257 | return finalCallback(err) | ||
258 | } | ||
259 | |||
260 | // Commit transaction | ||
261 | t.commit().asCallback(function (err) { | ||
262 | if (err) return finalCallback(err) | ||
263 | |||
264 | logger.info('Remote video %s updated', videoAttributesToUpdate.name) | ||
265 | return finalCallback(null) | ||
266 | }) | ||
267 | }) | ||
268 | } | ||
269 | |||
270 | function removeRemoteVideo (videoToRemoveData, fromPod, callback) { | ||
271 | // We need the instance because we have to remove some other stuffs (thumbnail etc) | ||
272 | fetchVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) { | ||
273 | // Do not return the error, continue the process | ||
274 | if (err) return callback(null) | ||
275 | |||
276 | logger.debug('Removing remote video %s.', video.remoteId) | ||
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 | }) | ||
285 | }) | ||
286 | } | ||
287 | |||
288 | function reportAbuseRemoteVideo (reportData, fromPod, callback) { | ||
289 | db.Video.load(reportData.videoRemoteId, function (err, video) { | ||
290 | if (err || !video) { | ||
291 | if (!err) err = new Error('video not found') | ||
292 | |||
293 | logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId }) | ||
294 | // Do not return the error, continue the process | ||
295 | return callback(null) | ||
296 | } | ||
297 | |||
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 | |||
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 | }) | ||
314 | }) | ||
315 | } | ||
316 | |||
317 | function 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 | |||
322 | logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId }) | ||
323 | return callback(err) | ||
324 | } | ||
325 | |||
326 | return callback(null, video) | ||
327 | }) | ||
328 | } | ||