X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fcontrollers%2Fapi%2Fremote%2Fvideos.js;h=39c9579c1a21fd3c5346b9b6f3c4b5a7184b8656;hb=e4c87ec26962e359d1c70b03ed188a3f19d6a25b;hp=87c49bff918bdaa0c773224bd06530d5223dfd57;hpb=a6fd2b30bf717eec14972a2175354781f5f43e77;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/controllers/api/remote/videos.js b/server/controllers/api/remote/videos.js index 87c49bff9..39c9579c1 100644 --- a/server/controllers/api/remote/videos.js +++ b/server/controllers/api/remote/videos.js @@ -5,20 +5,46 @@ const express = require('express') const waterfall = require('async/waterfall') const db = require('../../../initializers/database') +const constants = require('../../../initializers/constants') const middlewares = require('../../../middlewares') const secureMiddleware = middlewares.secure -const validators = middlewares.validators.remote +const videosValidators = middlewares.validators.remote.videos +const signatureValidators = middlewares.validators.remote.signature const logger = require('../../../helpers/logger') +const databaseUtils = require('../../../helpers/database-utils') + +const ENDPOINT_ACTIONS = constants.REQUEST_ENDPOINT_ACTIONS[constants.REQUEST_ENDPOINTS.VIDEOS] + +// Functions to call when processing a remote request +const functionsHash = {} +functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper +functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper +functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo +functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo const router = express.Router() router.post('/', - validators.signature, + signatureValidators.signature, secureMiddleware.checkSignature, - validators.remoteVideos, + videosValidators.remoteVideos, remoteVideos ) +router.post('/qadu', + signatureValidators.signature, + secureMiddleware.checkSignature, + videosValidators.remoteQaduVideos, + remoteVideosQadu +) + +router.post('/events', + signatureValidators.signature, + secureMiddleware.checkSignature, + videosValidators.remoteEventsVideos, + remoteVideosEvents +) + // --------------------------------------------------------------------------- module.exports = router @@ -31,44 +57,197 @@ function remoteVideos (req, res, next) { // We need to process in the same order to keep consistency // TODO: optimization + eachSeries(requests, function (request, callbackEach) { + const data = request.data + + // Get the function we need to call in order to process the request + const fun = functionsHash[request.type] + if (fun === undefined) { + logger.error('Unkown remote request type %s.', request.type) + return callbackEach(null) + } + + fun.call(this, data, fromPod, callbackEach) + }, function (err) { + if (err) logger.error('Error managing remote videos.', { error: err }) + }) + + // We don't need to keep the other pod waiting + return res.type('json').status(204).end() +} + +function remoteVideosQadu (req, res, next) { + const requests = req.body.data + const fromPod = res.locals.secure.pod + eachSeries(requests, function (request, callbackEach) { const videoData = request.data - switch (request.type) { - case 'add': - addRemoteVideo(videoData, fromPod, callbackEach) - break + quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod, callbackEach) + }, function (err) { + if (err) logger.error('Error managing remote videos.', { error: err }) + }) - case 'update': - updateRemoteVideo(videoData, fromPod, callbackEach) - break + return res.type('json').status(204).end() +} - case 'remove': - removeRemoteVideo(videoData, fromPod, callbackEach) - break +function remoteVideosEvents (req, res, next) { + const requests = req.body.data + const fromPod = res.locals.secure.pod - default: - logger.error('Unkown remote request type %s.', request.type) - } + eachSeries(requests, function (request, callbackEach) { + const eventData = request.data + + processVideosEventsRetryWrapper(eventData, fromPod, callbackEach) }, function (err) { if (err) logger.error('Error managing remote videos.', { error: err }) }) - // We don't need to keep the other pod waiting return res.type('json').status(204).end() } -function addRemoteVideo (videoToCreateData, fromPod, finalCallback) { - logger.debug('Adding remote video "%s".', videoToCreateData.name) +function processVideosEventsRetryWrapper (eventData, fromPod, finalCallback) { + const options = { + arguments: [ eventData, fromPod ], + errorMessage: 'Cannot process videos events with many retries.' + } + databaseUtils.retryTransactionWrapper(processVideosEvents, options, finalCallback) +} + +function processVideosEvents (eventData, fromPod, finalCallback) { waterfall([ + databaseUtils.startSerializableTransaction, - function startTransaction (callback) { - db.sequelize.transaction().asCallback(function (err, t) { + function findVideo (t, callback) { + fetchOwnedVideo(eventData.remoteId, function (err, videoInstance) { + return callback(err, t, videoInstance) + }) + }, + + function updateVideoIntoDB (t, videoInstance, callback) { + const options = { transaction: t } + + let columnToUpdate + + switch (eventData.eventType) { + case constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS: + columnToUpdate = 'views' + break + + case constants.REQUEST_VIDEO_EVENT_TYPES.LIKES: + columnToUpdate = 'likes' + break + + case constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES: + columnToUpdate = 'dislikes' + break + + default: + return callback(new Error('Unknown video event type.')) + } + + const query = {} + query[columnToUpdate] = eventData.count + + videoInstance.increment(query, options).asCallback(function (err) { return callback(err, t) }) }, + databaseUtils.commitTransaction + + ], function (err, t) { + if (err) { + console.log(err) + logger.debug('Cannot process a video event.', { error: err }) + return databaseUtils.rollbackTransaction(err, t, finalCallback) + } + + logger.info('Remote video event processed for video %s.', eventData.remoteId) + return finalCallback(null) + }) +} + +function quickAndDirtyUpdateVideoRetryWrapper (videoData, fromPod, finalCallback) { + const options = { + arguments: [ videoData, fromPod ], + errorMessage: 'Cannot update quick and dirty the remote video with many retries.' + } + + databaseUtils.retryTransactionWrapper(quickAndDirtyUpdateVideo, options, finalCallback) +} + +function quickAndDirtyUpdateVideo (videoData, fromPod, finalCallback) { + waterfall([ + databaseUtils.startSerializableTransaction, + + function findVideo (t, callback) { + fetchRemoteVideo(fromPod.host, videoData.remoteId, function (err, videoInstance) { + return callback(err, t, videoInstance) + }) + }, + + function updateVideoIntoDB (t, videoInstance, callback) { + const options = { transaction: t } + + if (videoData.views) { + videoInstance.set('views', videoData.views) + } + + if (videoData.likes) { + videoInstance.set('likes', videoData.likes) + } + + if (videoData.dislikes) { + videoInstance.set('dislikes', videoData.dislikes) + } + + videoInstance.save(options).asCallback(function (err) { + return callback(err, t) + }) + }, + + databaseUtils.commitTransaction + + ], function (err, t) { + if (err) { + logger.debug('Cannot quick and dirty update the remote video.', { error: err }) + return databaseUtils.rollbackTransaction(err, t, finalCallback) + } + + logger.info('Remote video %s quick and dirty updated', videoData.name) + return finalCallback(null) + }) +} + +// Handle retries on fail +function addRemoteVideoRetryWrapper (videoToCreateData, fromPod, finalCallback) { + const options = { + arguments: [ videoToCreateData, fromPod ], + errorMessage: 'Cannot insert the remote video with many retries.' + } + + databaseUtils.retryTransactionWrapper(addRemoteVideo, options, finalCallback) +} + +function addRemoteVideo (videoToCreateData, fromPod, finalCallback) { + logger.debug('Adding remote video "%s".', videoToCreateData.remoteId) + + waterfall([ + + databaseUtils.startSerializableTransaction, + + function assertRemoteIdAndHostUnique (t, callback) { + db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId, function (err, video) { + if (err) return callback(err) + + if (video) return callback(new Error('RemoteId and host pair is not unique.')) + + return callback(null, t) + }) + }, + function findOrCreateAuthor (t, callback) { const name = videoToCreateData.author const podId = fromPod.id @@ -98,6 +277,7 @@ function addRemoteVideo (videoToCreateData, fromPod, finalCallback) { authorId: author.id, duration: videoToCreateData.duration, createdAt: videoToCreateData.createdAt, + // FIXME: updatedAt does not seems to be considered by Sequelize updatedAt: videoToCreateData.updatedAt } @@ -128,49 +308,49 @@ function addRemoteVideo (videoToCreateData, fromPod, finalCallback) { }, function associateTagsToVideo (t, tagInstances, video, callback) { - const options = { transaction: t } + const options = { + transaction: t + } video.setTags(tagInstances, options).asCallback(function (err) { return callback(err, t) }) - } + }, + + databaseUtils.commitTransaction ], function (err, t) { if (err) { - logger.error('Cannot insert the remote video.') - - // Abort transaction? - if (t) t.rollback() - - return finalCallback(err) + // This is just a debug because we will retry the insert + logger.debug('Cannot insert the remote video.', { error: err }) + return databaseUtils.rollbackTransaction(err, t, finalCallback) } - // Commit transaction - t.commit() - - return finalCallback() + logger.info('Remote video %s inserted.', videoToCreateData.name) + return finalCallback(null) }) } +// Handle retries on fail +function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) { + const options = { + arguments: [ videoAttributesToUpdate, fromPod ], + errorMessage: 'Cannot update the remote video with many retries' + } + + databaseUtils.retryTransactionWrapper(updateRemoteVideo, options, finalCallback) +} + function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) { - logger.debug('Updating remote video "%s".', videoAttributesToUpdate.name) + logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId) waterfall([ - function startTransaction (callback) { - db.sequelize.transaction().asCallback(function (err, t) { - return callback(err, t) - }) - }, + databaseUtils.startSerializableTransaction, function findVideo (t, callback) { - db.Video.loadByHostAndRemoteId(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) { - if (err || !videoInstance) { - logger.error('Cannot load video from host and remote id.', { error: err.message }) - return callback(err) - } - - return callback(null, t, videoInstance) + fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) { + return callback(err, t, videoInstance) }) }, @@ -204,34 +384,91 @@ function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) { videoInstance.setTags(tagInstances, options).asCallback(function (err) { return callback(err, t) }) - } + }, + + databaseUtils.commitTransaction ], function (err, t) { if (err) { - logger.error('Cannot update the remote video.') + // This is just a debug because we will retry the insert + logger.debug('Cannot update the remote video.', { error: err }) + return databaseUtils.rollbackTransaction(err, t, finalCallback) + } + + logger.info('Remote video %s updated', videoAttributesToUpdate.name) + return finalCallback(null) + }) +} + +function removeRemoteVideo (videoToRemoveData, fromPod, callback) { + // We need the instance because we have to remove some other stuffs (thumbnail etc) + fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) { + // Do not return the error, continue the process + if (err) return callback(null) + + logger.debug('Removing remote video %s.', video.remoteId) + video.destroy().asCallback(function (err) { + // Do not return the error, continue the process + if (err) { + logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err }) + } + + return callback(null) + }) + }) +} + +function reportAbuseRemoteVideo (reportData, fromPod, callback) { + fetchOwnedVideo(reportData.videoRemoteId, function (err, video) { + if (err || !video) { + if (!err) err = new Error('video not found') + + logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId }) + // Do not return the error, continue the process + return callback(null) + } - // Abort transaction? - if (t) t.rollback() + logger.debug('Reporting remote abuse for video %s.', video.id) - return finalCallback(err) + const videoAbuseData = { + reporterUsername: reportData.reporterUsername, + reason: reportData.reportReason, + reporterPodId: fromPod.id, + videoId: video.id } - // Commit transaction - t.commit() + db.VideoAbuse.create(videoAbuseData).asCallback(function (err) { + if (err) { + logger.error('Cannot create remote abuse video.', { error: err }) + } - return finalCallback() + return callback(null) + }) }) } -function removeRemoteVideo (videoToRemoveData, fromPod, callback) { - // We need the instance because we have to remove some other stuffs (thumbnail etc) - db.Video.loadByHostAndRemoteId(fromPod.host, videoToRemoveData.remoteId, function (err, video) { +function fetchOwnedVideo (id, callback) { + db.Video.load(id, function (err, video) { if (err || !video) { - logger.error('Cannot load video from host and remote id.', { error: err.message }) + if (!err) err = new Error('video not found') + + logger.error('Cannot load owned video from id.', { error: err, id }) return callback(err) } - logger.debug('Removing remote video %s.', video.remoteId) - video.destroy().asCallback(callback) + return callback(null, video) + }) +} + +function fetchRemoteVideo (podHost, remoteId, callback) { + db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) { + if (err || !video) { + if (!err) err = new Error('video not found') + + logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId }) + return callback(err) + } + + return callback(null, video) }) }