From 65fcc3119c334b75dd13bcfdebf186afdc580a8f Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 15 May 2017 22:22:03 +0200 Subject: First typescript iteration --- server/controllers/api/videos/abuse.js | 112 -------- server/controllers/api/videos/abuse.ts | 117 ++++++++ server/controllers/api/videos/blacklist.js | 43 --- server/controllers/api/videos/blacklist.ts | 43 +++ server/controllers/api/videos/index.js | 404 --------------------------- server/controllers/api/videos/index.ts | 426 +++++++++++++++++++++++++++++ server/controllers/api/videos/rate.js | 169 ------------ server/controllers/api/videos/rate.ts | 181 ++++++++++++ 8 files changed, 767 insertions(+), 728 deletions(-) delete mode 100644 server/controllers/api/videos/abuse.js create mode 100644 server/controllers/api/videos/abuse.ts delete mode 100644 server/controllers/api/videos/blacklist.js create mode 100644 server/controllers/api/videos/blacklist.ts delete mode 100644 server/controllers/api/videos/index.js create mode 100644 server/controllers/api/videos/index.ts delete mode 100644 server/controllers/api/videos/rate.js create mode 100644 server/controllers/api/videos/rate.ts (limited to 'server/controllers/api/videos') diff --git a/server/controllers/api/videos/abuse.js b/server/controllers/api/videos/abuse.js deleted file mode 100644 index 0fb44bb14..000000000 --- a/server/controllers/api/videos/abuse.js +++ /dev/null @@ -1,112 +0,0 @@ -'use strict' - -const express = require('express') -const waterfall = require('async/waterfall') - -const db = require('../../../initializers/database') -const logger = require('../../../helpers/logger') -const friends = require('../../../lib/friends') -const middlewares = require('../../../middlewares') -const admin = middlewares.admin -const oAuth = middlewares.oauth -const pagination = middlewares.pagination -const validators = middlewares.validators -const validatorsPagination = validators.pagination -const validatorsSort = validators.sort -const validatorsVideos = validators.videos -const sort = middlewares.sort -const databaseUtils = require('../../../helpers/database-utils') -const utils = require('../../../helpers/utils') - -const router = express.Router() - -router.get('/abuse', - oAuth.authenticate, - admin.ensureIsAdmin, - validatorsPagination.pagination, - validatorsSort.videoAbusesSort, - sort.setVideoAbusesSort, - pagination.setPagination, - listVideoAbuses -) -router.post('/:id/abuse', - oAuth.authenticate, - validatorsVideos.videoAbuseReport, - reportVideoAbuseRetryWrapper -) - -// --------------------------------------------------------------------------- - -module.exports = router - -// --------------------------------------------------------------------------- - -function listVideoAbuses (req, res, next) { - db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) { - if (err) return next(err) - - res.json(utils.getFormatedObjects(abusesList, abusesTotal)) - }) -} - -function reportVideoAbuseRetryWrapper (req, res, next) { - const options = { - arguments: [ req, res ], - errorMessage: 'Cannot report abuse to the video with many retries.' - } - - databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) { - if (err) return next(err) - - return res.type('json').status(204).end() - }) -} - -function reportVideoAbuse (req, res, finalCallback) { - const videoInstance = res.locals.video - const reporterUsername = res.locals.oauth.token.User.username - - const abuse = { - reporterUsername, - reason: req.body.reason, - videoId: videoInstance.id, - reporterPodId: null // This is our pod that reported this abuse - } - - waterfall([ - - databaseUtils.startSerializableTransaction, - - function createAbuse (t, callback) { - db.VideoAbuse.create(abuse).asCallback(function (err, abuse) { - return callback(err, t, abuse) - }) - }, - - function sendToFriendsIfNeeded (t, abuse, callback) { - // We send the information to the destination pod - if (videoInstance.isOwned() === false) { - const reportData = { - reporterUsername, - reportReason: abuse.reason, - videoRemoteId: videoInstance.remoteId - } - - friends.reportAbuseVideoToFriend(reportData, videoInstance) - } - - return callback(null, t) - }, - - databaseUtils.commitTransaction - - ], function andFinally (err, t) { - if (err) { - logger.debug('Cannot update the video.', { error: err }) - return databaseUtils.rollbackTransaction(err, t, finalCallback) - } - - logger.info('Abuse report for video %s created.', videoInstance.name) - return finalCallback(null) - }) -} diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts new file mode 100644 index 000000000..88204120f --- /dev/null +++ b/server/controllers/api/videos/abuse.ts @@ -0,0 +1,117 @@ +import express = require('express') +import { waterfall } from 'async' + +const db = require('../../../initializers/database') +import friends = require('../../../lib/friends') +import { + logger, + getFormatedObjects, + retryTransactionWrapper, + startSerializableTransaction, + commitTransaction, + rollbackTransaction +} from '../../../helpers' +import { + authenticate, + ensureIsAdmin, + paginationValidator, + videoAbuseReportValidator, + videoAbusesSortValidator, + setVideoAbusesSort, + setPagination +} from '../../../middlewares' + +const abuseVideoRouter = express.Router() + +abuseVideoRouter.get('/abuse', + authenticate, + ensureIsAdmin, + paginationValidator, + videoAbusesSortValidator, + setVideoAbusesSort, + setPagination, + listVideoAbuses +) +abuseVideoRouter.post('/:id/abuse', + authenticate, + videoAbuseReportValidator, + reportVideoAbuseRetryWrapper +) + +// --------------------------------------------------------------------------- + +export { + abuseVideoRouter +} + +// --------------------------------------------------------------------------- + +function listVideoAbuses (req, res, next) { + db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) { + if (err) return next(err) + + res.json(getFormatedObjects(abusesList, abusesTotal)) + }) +} + +function reportVideoAbuseRetryWrapper (req, res, next) { + const options = { + arguments: [ req, res ], + errorMessage: 'Cannot report abuse to the video with many retries.' + } + + retryTransactionWrapper(reportVideoAbuse, options, function (err) { + if (err) return next(err) + + return res.type('json').status(204).end() + }) +} + +function reportVideoAbuse (req, res, finalCallback) { + const videoInstance = res.locals.video + const reporterUsername = res.locals.oauth.token.User.username + + const abuse = { + reporterUsername, + reason: req.body.reason, + videoId: videoInstance.id, + reporterPodId: null // This is our pod that reported this abuse + } + + waterfall([ + + startSerializableTransaction, + + function createAbuse (t, callback) { + db.VideoAbuse.create(abuse).asCallback(function (err, abuse) { + return callback(err, t, abuse) + }) + }, + + function sendToFriendsIfNeeded (t, abuse, callback) { + // We send the information to the destination pod + if (videoInstance.isOwned() === false) { + const reportData = { + reporterUsername, + reportReason: abuse.reason, + videoRemoteId: videoInstance.remoteId + } + + friends.reportAbuseVideoToFriend(reportData, videoInstance) + } + + return callback(null, t) + }, + + commitTransaction + + ], function andFinally (err, t) { + if (err) { + logger.debug('Cannot update the video.', { error: err }) + return rollbackTransaction(err, t, finalCallback) + } + + logger.info('Abuse report for video %s created.', videoInstance.name) + return finalCallback(null) + }) +} diff --git a/server/controllers/api/videos/blacklist.js b/server/controllers/api/videos/blacklist.js deleted file mode 100644 index 8c3e2a69d..000000000 --- a/server/controllers/api/videos/blacklist.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict' - -const express = require('express') - -const db = require('../../../initializers/database') -const logger = require('../../../helpers/logger') -const middlewares = require('../../../middlewares') -const admin = middlewares.admin -const oAuth = middlewares.oauth -const validators = middlewares.validators -const validatorsVideos = validators.videos - -const router = express.Router() - -router.post('/:id/blacklist', - oAuth.authenticate, - admin.ensureIsAdmin, - validatorsVideos.videosBlacklist, - addVideoToBlacklist -) - -// --------------------------------------------------------------------------- - -module.exports = router - -// --------------------------------------------------------------------------- - -function addVideoToBlacklist (req, res, next) { - const videoInstance = res.locals.video - - const toCreate = { - videoId: videoInstance.id - } - - db.BlacklistedVideo.create(toCreate).asCallback(function (err) { - if (err) { - logger.error('Errors when blacklisting video ', { error: err }) - return next(err) - } - - return res.type('json').status(204).end() - }) -} diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts new file mode 100644 index 000000000..db6d95e73 --- /dev/null +++ b/server/controllers/api/videos/blacklist.ts @@ -0,0 +1,43 @@ +import express = require('express') + +const db = require('../../../initializers/database') +import { logger } from '../../../helpers' +import { + authenticate, + ensureIsAdmin, + videosBlacklistValidator +} from '../../../middlewares' + +const blacklistRouter = express.Router() + +blacklistRouter.post('/:id/blacklist', + authenticate, + ensureIsAdmin, + videosBlacklistValidator, + addVideoToBlacklist +) + +// --------------------------------------------------------------------------- + +export { + blacklistRouter +} + +// --------------------------------------------------------------------------- + +function addVideoToBlacklist (req, res, next) { + const videoInstance = res.locals.video + + const toCreate = { + videoId: videoInstance.id + } + + db.BlacklistedVideo.create(toCreate).asCallback(function (err) { + if (err) { + logger.error('Errors when blacklisting video ', { error: err }) + return next(err) + } + + return res.type('json').status(204).end() + }) +} diff --git a/server/controllers/api/videos/index.js b/server/controllers/api/videos/index.js deleted file mode 100644 index 8de44d5ac..000000000 --- a/server/controllers/api/videos/index.js +++ /dev/null @@ -1,404 +0,0 @@ -'use strict' - -const express = require('express') -const fs = require('fs') -const multer = require('multer') -const path = require('path') -const waterfall = require('async/waterfall') - -const constants = require('../../../initializers/constants') -const db = require('../../../initializers/database') -const logger = require('../../../helpers/logger') -const friends = require('../../../lib/friends') -const middlewares = require('../../../middlewares') -const oAuth = middlewares.oauth -const pagination = middlewares.pagination -const validators = middlewares.validators -const validatorsPagination = validators.pagination -const validatorsSort = validators.sort -const validatorsVideos = validators.videos -const search = middlewares.search -const sort = middlewares.sort -const databaseUtils = require('../../../helpers/database-utils') -const utils = require('../../../helpers/utils') - -const abuseController = require('./abuse') -const blacklistController = require('./blacklist') -const rateController = require('./rate') - -const router = express.Router() - -// multer configuration -const storage = multer.diskStorage({ - destination: function (req, file, cb) { - cb(null, constants.CONFIG.STORAGE.VIDEOS_DIR) - }, - - filename: function (req, file, cb) { - let extension = '' - if (file.mimetype === 'video/webm') extension = 'webm' - else if (file.mimetype === 'video/mp4') extension = 'mp4' - else if (file.mimetype === 'video/ogg') extension = 'ogv' - utils.generateRandomString(16, function (err, randomString) { - const fieldname = err ? undefined : randomString - cb(null, fieldname + '.' + extension) - }) - } -}) - -const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }]) - -router.use('/', abuseController) -router.use('/', blacklistController) -router.use('/', rateController) - -router.get('/categories', listVideoCategories) -router.get('/licences', listVideoLicences) -router.get('/languages', listVideoLanguages) - -router.get('/', - validatorsPagination.pagination, - validatorsSort.videosSort, - sort.setVideosSort, - pagination.setPagination, - listVideos -) -router.put('/:id', - oAuth.authenticate, - reqFiles, - validatorsVideos.videosUpdate, - updateVideoRetryWrapper -) -router.post('/', - oAuth.authenticate, - reqFiles, - validatorsVideos.videosAdd, - addVideoRetryWrapper -) -router.get('/:id', - validatorsVideos.videosGet, - getVideo -) - -router.delete('/:id', - oAuth.authenticate, - validatorsVideos.videosRemove, - removeVideo -) - -router.get('/search/:value', - validatorsVideos.videosSearch, - validatorsPagination.pagination, - validatorsSort.videosSort, - sort.setVideosSort, - pagination.setPagination, - search.setVideosSearch, - searchVideos -) - -// --------------------------------------------------------------------------- - -module.exports = router - -// --------------------------------------------------------------------------- - -function listVideoCategories (req, res, next) { - res.json(constants.VIDEO_CATEGORIES) -} - -function listVideoLicences (req, res, next) { - res.json(constants.VIDEO_LICENCES) -} - -function listVideoLanguages (req, res, next) { - res.json(constants.VIDEO_LANGUAGES) -} - -// Wrapper to video add that retry the function if there is a database error -// We need this because we run the transaction in SERIALIZABLE isolation that can fail -function addVideoRetryWrapper (req, res, next) { - const options = { - arguments: [ req, res, req.files.videofile[0] ], - errorMessage: 'Cannot insert the video with many retries.' - } - - databaseUtils.retryTransactionWrapper(addVideo, options, function (err) { - if (err) return next(err) - - // TODO : include Location of the new video -> 201 - return res.type('json').status(204).end() - }) -} - -function addVideo (req, res, videoFile, finalCallback) { - const videoInfos = req.body - - waterfall([ - - databaseUtils.startSerializableTransaction, - - function findOrCreateAuthor (t, callback) { - const user = res.locals.oauth.token.User - - const name = user.username - // null because it is OUR pod - const podId = null - const userId = user.id - - db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) { - return callback(err, t, authorInstance) - }) - }, - - function findOrCreateTags (t, author, callback) { - const tags = videoInfos.tags - - db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { - return callback(err, t, author, tagInstances) - }) - }, - - function createVideoObject (t, author, tagInstances, callback) { - const videoData = { - name: videoInfos.name, - remoteId: null, - extname: path.extname(videoFile.filename), - category: videoInfos.category, - licence: videoInfos.licence, - language: videoInfos.language, - nsfw: videoInfos.nsfw, - description: videoInfos.description, - duration: videoFile.duration, - authorId: author.id - } - - const video = db.Video.build(videoData) - - return callback(null, t, author, tagInstances, video) - }, - - // Set the videoname the same as the id - function renameVideoFile (t, author, tagInstances, video, callback) { - const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR - const source = path.join(videoDir, videoFile.filename) - const destination = path.join(videoDir, video.getVideoFilename()) - - fs.rename(source, destination, function (err) { - if (err) return callback(err) - - // This is important in case if there is another attempt - videoFile.filename = video.getVideoFilename() - return callback(null, t, author, tagInstances, video) - }) - }, - - function insertVideoIntoDB (t, author, tagInstances, video, callback) { - const options = { transaction: t } - - // Add tags association - video.save(options).asCallback(function (err, videoCreated) { - if (err) return callback(err) - - // Do not forget to add Author informations to the created video - videoCreated.Author = author - - return callback(err, t, tagInstances, videoCreated) - }) - }, - - function associateTagsToVideo (t, tagInstances, video, callback) { - const options = { transaction: t } - - video.setTags(tagInstances, options).asCallback(function (err) { - video.Tags = tagInstances - - return callback(err, t, video) - }) - }, - - function sendToFriends (t, video, callback) { - // Let transcoding job send the video to friends because the videofile extension might change - if (constants.CONFIG.TRANSCODING.ENABLED === true) return callback(null, t) - - video.toAddRemoteJSON(function (err, remoteVideo) { - if (err) return callback(err) - - // Now we'll add the video's meta data to our friends - friends.addVideoToFriends(remoteVideo, t, function (err) { - return callback(err, t) - }) - }) - }, - - databaseUtils.commitTransaction - - ], function andFinally (err, t) { - if (err) { - // This is just a debug because we will retry the insert - logger.debug('Cannot insert the video.', { error: err }) - return databaseUtils.rollbackTransaction(err, t, finalCallback) - } - - logger.info('Video with name %s created.', videoInfos.name) - return finalCallback(null) - }) -} - -function updateVideoRetryWrapper (req, res, next) { - const options = { - arguments: [ req, res ], - errorMessage: 'Cannot update the video with many retries.' - } - - databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) { - if (err) return next(err) - - // TODO : include Location of the new video -> 201 - return res.type('json').status(204).end() - }) -} - -function updateVideo (req, res, finalCallback) { - const videoInstance = res.locals.video - const videoFieldsSave = videoInstance.toJSON() - const videoInfosToUpdate = req.body - - waterfall([ - - databaseUtils.startSerializableTransaction, - - function findOrCreateTags (t, callback) { - if (videoInfosToUpdate.tags) { - db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) { - return callback(err, t, tagInstances) - }) - } else { - return callback(null, t, null) - } - }, - - function updateVideoIntoDB (t, tagInstances, callback) { - const options = { - transaction: t - } - - if (videoInfosToUpdate.name !== undefined) videoInstance.set('name', videoInfosToUpdate.name) - if (videoInfosToUpdate.category !== undefined) videoInstance.set('category', videoInfosToUpdate.category) - if (videoInfosToUpdate.licence !== undefined) videoInstance.set('licence', videoInfosToUpdate.licence) - if (videoInfosToUpdate.language !== undefined) videoInstance.set('language', videoInfosToUpdate.language) - if (videoInfosToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfosToUpdate.nsfw) - if (videoInfosToUpdate.description !== undefined) videoInstance.set('description', videoInfosToUpdate.description) - - videoInstance.save(options).asCallback(function (err) { - return callback(err, t, tagInstances) - }) - }, - - function associateTagsToVideo (t, tagInstances, callback) { - if (tagInstances) { - const options = { transaction: t } - - videoInstance.setTags(tagInstances, options).asCallback(function (err) { - videoInstance.Tags = tagInstances - - return callback(err, t) - }) - } else { - return callback(null, t) - } - }, - - function sendToFriends (t, callback) { - const json = videoInstance.toUpdateRemoteJSON() - - // Now we'll update the video's meta data to our friends - friends.updateVideoToFriends(json, t, function (err) { - return callback(err, t) - }) - }, - - databaseUtils.commitTransaction - - ], function andFinally (err, t) { - if (err) { - logger.debug('Cannot update the video.', { error: err }) - - // Force fields we want to update - // If the transaction is retried, sequelize will think the object has not changed - // So it will skip the SQL request, even if the last one was ROLLBACKed! - Object.keys(videoFieldsSave).forEach(function (key) { - const value = videoFieldsSave[key] - videoInstance.set(key, value) - }) - - return databaseUtils.rollbackTransaction(err, t, finalCallback) - } - - logger.info('Video with name %s updated.', videoInfosToUpdate.name) - return finalCallback(null) - }) -} - -function getVideo (req, res, next) { - const videoInstance = res.locals.video - - if (videoInstance.isOwned()) { - // The increment is done directly in the database, not using the instance value - videoInstance.increment('views').asCallback(function (err) { - if (err) { - logger.error('Cannot add view to video %d.', videoInstance.id) - return - } - - // FIXME: make a real view system - // For example, only add a view when a user watch a video during 30s etc - const qaduParams = { - videoId: videoInstance.id, - type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS - } - friends.quickAndDirtyUpdateVideoToFriends(qaduParams) - }) - } else { - // Just send the event to our friends - const eventParams = { - videoId: videoInstance.id, - type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS - } - friends.addEventToRemoteVideo(eventParams) - } - - // Do not wait the view system - res.json(videoInstance.toFormatedJSON()) -} - -function listVideos (req, res, next) { - db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) { - if (err) return next(err) - - res.json(utils.getFormatedObjects(videosList, videosTotal)) - }) -} - -function removeVideo (req, res, next) { - const videoInstance = res.locals.video - - videoInstance.destroy().asCallback(function (err) { - if (err) { - logger.error('Errors when removed the video.', { error: err }) - return next(err) - } - - return res.type('json').status(204).end() - }) -} - -function searchVideos (req, res, next) { - db.Video.searchAndPopulateAuthorAndPodAndTags( - req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort, - function (err, videosList, videosTotal) { - if (err) return next(err) - - res.json(utils.getFormatedObjects(videosList, videosTotal)) - } - ) -} diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts new file mode 100644 index 000000000..5fbf03676 --- /dev/null +++ b/server/controllers/api/videos/index.ts @@ -0,0 +1,426 @@ +import express = require('express') +import fs = require('fs') +import multer = require('multer') +import path = require('path') +import { waterfall } from 'async' + +const db = require('../../../initializers/database') +import { + CONFIG, + REQUEST_VIDEO_QADU_TYPES, + REQUEST_VIDEO_EVENT_TYPES, + VIDEO_CATEGORIES, + VIDEO_LICENCES, + VIDEO_LANGUAGES +} from '../../../initializers' +import { + addEventToRemoteVideo, + quickAndDirtyUpdateVideoToFriends, + addVideoToFriends, + updateVideoToFriends +} from '../../../lib' +import { + authenticate, + paginationValidator, + videosSortValidator, + setVideosSort, + setPagination, + setVideosSearch, + videosUpdateValidator, + videosSearchValidator, + videosAddValidator, + videosGetValidator, + videosRemoveValidator +} from '../../../middlewares' +import { + logger, + commitTransaction, + retryTransactionWrapper, + rollbackTransaction, + startSerializableTransaction, + generateRandomString, + getFormatedObjects +} from '../../../helpers' + +import { abuseVideoRouter } from './abuse' +import { blacklistRouter } from './blacklist' +import { rateVideoRouter } from './rate' + +const videosRouter = express.Router() + +// multer configuration +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, CONFIG.STORAGE.VIDEOS_DIR) + }, + + filename: function (req, file, cb) { + let extension = '' + if (file.mimetype === 'video/webm') extension = 'webm' + else if (file.mimetype === 'video/mp4') extension = 'mp4' + else if (file.mimetype === 'video/ogg') extension = 'ogv' + generateRandomString(16, function (err, randomString) { + const fieldname = err ? undefined : randomString + cb(null, fieldname + '.' + extension) + }) + } +}) + +const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }]) + +videosRouter.use('/', abuseVideoRouter) +videosRouter.use('/', blacklistRouter) +videosRouter.use('/', rateVideoRouter) + +videosRouter.get('/categories', listVideoCategories) +videosRouter.get('/licences', listVideoLicences) +videosRouter.get('/languages', listVideoLanguages) + +videosRouter.get('/', + paginationValidator, + videosSortValidator, + setVideosSort, + setPagination, + listVideos +) +videosRouter.put('/:id', + authenticate, + reqFiles, + videosUpdateValidator, + updateVideoRetryWrapper +) +videosRouter.post('/', + authenticate, + reqFiles, + videosAddValidator, + addVideoRetryWrapper +) +videosRouter.get('/:id', + videosGetValidator, + getVideo +) + +videosRouter.delete('/:id', + authenticate, + videosRemoveValidator, + removeVideo +) + +videosRouter.get('/search/:value', + videosSearchValidator, + paginationValidator, + videosSortValidator, + setVideosSort, + setPagination, + setVideosSearch, + searchVideos +) + +// --------------------------------------------------------------------------- + +export { + videosRouter +} + +// --------------------------------------------------------------------------- + +function listVideoCategories (req, res, next) { + res.json(VIDEO_CATEGORIES) +} + +function listVideoLicences (req, res, next) { + res.json(VIDEO_LICENCES) +} + +function listVideoLanguages (req, res, next) { + res.json(VIDEO_LANGUAGES) +} + +// Wrapper to video add that retry the function if there is a database error +// We need this because we run the transaction in SERIALIZABLE isolation that can fail +function addVideoRetryWrapper (req, res, next) { + const options = { + arguments: [ req, res, req.files.videofile[0] ], + errorMessage: 'Cannot insert the video with many retries.' + } + + retryTransactionWrapper(addVideo, options, function (err) { + if (err) return next(err) + + // TODO : include Location of the new video -> 201 + return res.type('json').status(204).end() + }) +} + +function addVideo (req, res, videoFile, finalCallback) { + const videoInfos = req.body + + waterfall([ + + startSerializableTransaction, + + function findOrCreateAuthor (t, callback) { + const user = res.locals.oauth.token.User + + const name = user.username + // null because it is OUR pod + const podId = null + const userId = user.id + + db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) { + return callback(err, t, authorInstance) + }) + }, + + function findOrCreateTags (t, author, callback) { + const tags = videoInfos.tags + + db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { + return callback(err, t, author, tagInstances) + }) + }, + + function createVideoObject (t, author, tagInstances, callback) { + const videoData = { + name: videoInfos.name, + remoteId: null, + extname: path.extname(videoFile.filename), + category: videoInfos.category, + licence: videoInfos.licence, + language: videoInfos.language, + nsfw: videoInfos.nsfw, + description: videoInfos.description, + duration: videoFile.duration, + authorId: author.id + } + + const video = db.Video.build(videoData) + + return callback(null, t, author, tagInstances, video) + }, + + // Set the videoname the same as the id + function renameVideoFile (t, author, tagInstances, video, callback) { + const videoDir = CONFIG.STORAGE.VIDEOS_DIR + const source = path.join(videoDir, videoFile.filename) + const destination = path.join(videoDir, video.getVideoFilename()) + + fs.rename(source, destination, function (err) { + if (err) return callback(err) + + // This is important in case if there is another attempt + videoFile.filename = video.getVideoFilename() + return callback(null, t, author, tagInstances, video) + }) + }, + + function insertVideoIntoDB (t, author, tagInstances, video, callback) { + const options = { transaction: t } + + // Add tags association + video.save(options).asCallback(function (err, videoCreated) { + if (err) return callback(err) + + // Do not forget to add Author informations to the created video + videoCreated.Author = author + + return callback(err, t, tagInstances, videoCreated) + }) + }, + + function associateTagsToVideo (t, tagInstances, video, callback) { + const options = { transaction: t } + + video.setTags(tagInstances, options).asCallback(function (err) { + video.Tags = tagInstances + + return callback(err, t, video) + }) + }, + + function sendToFriends (t, video, callback) { + // Let transcoding job send the video to friends because the videofile extension might change + if (CONFIG.TRANSCODING.ENABLED === true) return callback(null, t) + + video.toAddRemoteJSON(function (err, remoteVideo) { + if (err) return callback(err) + + // Now we'll add the video's meta data to our friends + addVideoToFriends(remoteVideo, t, function (err) { + return callback(err, t) + }) + }) + }, + + commitTransaction + + ], function andFinally (err, t) { + if (err) { + // This is just a debug because we will retry the insert + logger.debug('Cannot insert the video.', { error: err }) + return rollbackTransaction(err, t, finalCallback) + } + + logger.info('Video with name %s created.', videoInfos.name) + return finalCallback(null) + }) +} + +function updateVideoRetryWrapper (req, res, next) { + const options = { + arguments: [ req, res ], + errorMessage: 'Cannot update the video with many retries.' + } + + retryTransactionWrapper(updateVideo, options, function (err) { + if (err) return next(err) + + // TODO : include Location of the new video -> 201 + return res.type('json').status(204).end() + }) +} + +function updateVideo (req, res, finalCallback) { + const videoInstance = res.locals.video + const videoFieldsSave = videoInstance.toJSON() + const videoInfosToUpdate = req.body + + waterfall([ + + startSerializableTransaction, + + function findOrCreateTags (t, callback) { + if (videoInfosToUpdate.tags) { + db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) { + return callback(err, t, tagInstances) + }) + } else { + return callback(null, t, null) + } + }, + + function updateVideoIntoDB (t, tagInstances, callback) { + const options = { + transaction: t + } + + if (videoInfosToUpdate.name !== undefined) videoInstance.set('name', videoInfosToUpdate.name) + if (videoInfosToUpdate.category !== undefined) videoInstance.set('category', videoInfosToUpdate.category) + if (videoInfosToUpdate.licence !== undefined) videoInstance.set('licence', videoInfosToUpdate.licence) + if (videoInfosToUpdate.language !== undefined) videoInstance.set('language', videoInfosToUpdate.language) + if (videoInfosToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfosToUpdate.nsfw) + if (videoInfosToUpdate.description !== undefined) videoInstance.set('description', videoInfosToUpdate.description) + + videoInstance.save(options).asCallback(function (err) { + return callback(err, t, tagInstances) + }) + }, + + function associateTagsToVideo (t, tagInstances, callback) { + if (tagInstances) { + const options = { transaction: t } + + videoInstance.setTags(tagInstances, options).asCallback(function (err) { + videoInstance.Tags = tagInstances + + return callback(err, t) + }) + } else { + return callback(null, t) + } + }, + + function sendToFriends (t, callback) { + const json = videoInstance.toUpdateRemoteJSON() + + // Now we'll update the video's meta data to our friends + updateVideoToFriends(json, t, function (err) { + return callback(err, t) + }) + }, + + commitTransaction + + ], function andFinally (err, t) { + if (err) { + logger.debug('Cannot update the video.', { error: err }) + + // Force fields we want to update + // If the transaction is retried, sequelize will think the object has not changed + // So it will skip the SQL request, even if the last one was ROLLBACKed! + Object.keys(videoFieldsSave).forEach(function (key) { + const value = videoFieldsSave[key] + videoInstance.set(key, value) + }) + + return rollbackTransaction(err, t, finalCallback) + } + + logger.info('Video with name %s updated.', videoInfosToUpdate.name) + return finalCallback(null) + }) +} + +function getVideo (req, res, next) { + const videoInstance = res.locals.video + + if (videoInstance.isOwned()) { + // The increment is done directly in the database, not using the instance value + videoInstance.increment('views').asCallback(function (err) { + if (err) { + logger.error('Cannot add view to video %d.', videoInstance.id) + return + } + + // FIXME: make a real view system + // For example, only add a view when a user watch a video during 30s etc + const qaduParams = { + videoId: videoInstance.id, + type: REQUEST_VIDEO_QADU_TYPES.VIEWS + } + quickAndDirtyUpdateVideoToFriends(qaduParams) + }) + } else { + // Just send the event to our friends + const eventParams = { + videoId: videoInstance.id, + type: REQUEST_VIDEO_EVENT_TYPES.VIEWS + } + addEventToRemoteVideo(eventParams) + } + + // Do not wait the view system + res.json(videoInstance.toFormatedJSON()) +} + +function listVideos (req, res, next) { + db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) { + if (err) return next(err) + + res.json(getFormatedObjects(videosList, videosTotal)) + }) +} + +function removeVideo (req, res, next) { + const videoInstance = res.locals.video + + videoInstance.destroy().asCallback(function (err) { + if (err) { + logger.error('Errors when removed the video.', { error: err }) + return next(err) + } + + return res.type('json').status(204).end() + }) +} + +function searchVideos (req, res, next) { + db.Video.searchAndPopulateAuthorAndPodAndTags( + req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort, + function (err, videosList, videosTotal) { + if (err) return next(err) + + res.json(getFormatedObjects(videosList, videosTotal)) + } + ) +} diff --git a/server/controllers/api/videos/rate.js b/server/controllers/api/videos/rate.js deleted file mode 100644 index df8a69a1d..000000000 --- a/server/controllers/api/videos/rate.js +++ /dev/null @@ -1,169 +0,0 @@ -'use strict' - -const express = require('express') -const waterfall = require('async/waterfall') - -const constants = require('../../../initializers/constants') -const db = require('../../../initializers/database') -const logger = require('../../../helpers/logger') -const friends = require('../../../lib/friends') -const middlewares = require('../../../middlewares') -const oAuth = middlewares.oauth -const validators = middlewares.validators -const validatorsVideos = validators.videos -const databaseUtils = require('../../../helpers/database-utils') - -const router = express.Router() - -router.put('/:id/rate', - oAuth.authenticate, - validatorsVideos.videoRate, - rateVideoRetryWrapper -) - -// --------------------------------------------------------------------------- - -module.exports = router - -// --------------------------------------------------------------------------- - -function rateVideoRetryWrapper (req, res, next) { - const options = { - arguments: [ req, res ], - errorMessage: 'Cannot update the user video rate.' - } - - databaseUtils.retryTransactionWrapper(rateVideo, options, function (err) { - if (err) return next(err) - - return res.type('json').status(204).end() - }) -} - -function rateVideo (req, res, finalCallback) { - const rateType = req.body.rating - const videoInstance = res.locals.video - const userInstance = res.locals.oauth.token.User - - waterfall([ - databaseUtils.startSerializableTransaction, - - function findPreviousRate (t, callback) { - db.UserVideoRate.load(userInstance.id, videoInstance.id, t, function (err, previousRate) { - return callback(err, t, previousRate) - }) - }, - - function insertUserRateIntoDB (t, previousRate, callback) { - const options = { transaction: t } - - let likesToIncrement = 0 - let dislikesToIncrement = 0 - - if (rateType === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement++ - else if (rateType === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++ - - // There was a previous rate, update it - if (previousRate) { - // We will remove the previous rate, so we will need to remove it from the video attribute - if (previousRate.type === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement-- - else if (previousRate.type === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement-- - - previousRate.type = rateType - - previousRate.save(options).asCallback(function (err) { - return callback(err, t, likesToIncrement, dislikesToIncrement) - }) - } else { // There was not a previous rate, insert a new one - const query = { - userId: userInstance.id, - videoId: videoInstance.id, - type: rateType - } - - db.UserVideoRate.create(query, options).asCallback(function (err) { - return callback(err, t, likesToIncrement, dislikesToIncrement) - }) - } - }, - - function updateVideoAttributeDB (t, likesToIncrement, dislikesToIncrement, callback) { - const options = { transaction: t } - const incrementQuery = { - likes: likesToIncrement, - dislikes: dislikesToIncrement - } - - // Even if we do not own the video we increment the attributes - // It is usefull for the user to have a feedback - videoInstance.increment(incrementQuery, options).asCallback(function (err) { - return callback(err, t, likesToIncrement, dislikesToIncrement) - }) - }, - - function sendEventsToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) { - // No need for an event type, we own the video - if (videoInstance.isOwned()) return callback(null, t, likesToIncrement, dislikesToIncrement) - - const eventsParams = [] - - if (likesToIncrement !== 0) { - eventsParams.push({ - videoId: videoInstance.id, - type: constants.REQUEST_VIDEO_EVENT_TYPES.LIKES, - count: likesToIncrement - }) - } - - if (dislikesToIncrement !== 0) { - eventsParams.push({ - videoId: videoInstance.id, - type: constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES, - count: dislikesToIncrement - }) - } - - friends.addEventsToRemoteVideo(eventsParams, t, function (err) { - return callback(err, t, likesToIncrement, dislikesToIncrement) - }) - }, - - function sendQaduToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) { - // We do not own the video, there is no need to send a quick and dirty update to friends - // Our rate was already sent by the addEvent function - if (videoInstance.isOwned() === false) return callback(null, t) - - const qadusParams = [] - - if (likesToIncrement !== 0) { - qadusParams.push({ - videoId: videoInstance.id, - type: constants.REQUEST_VIDEO_QADU_TYPES.LIKES - }) - } - - if (dislikesToIncrement !== 0) { - qadusParams.push({ - videoId: videoInstance.id, - type: constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES - }) - } - - friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) { - return callback(err, t) - }) - }, - - databaseUtils.commitTransaction - - ], function (err, t) { - if (err) { - // This is just a debug because we will retry the insert - logger.debug('Cannot add the user video rate.', { error: err }) - return databaseUtils.rollbackTransaction(err, t, finalCallback) - } - - logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username) - return finalCallback(null) - }) -} diff --git a/server/controllers/api/videos/rate.ts b/server/controllers/api/videos/rate.ts new file mode 100644 index 000000000..21053792a --- /dev/null +++ b/server/controllers/api/videos/rate.ts @@ -0,0 +1,181 @@ +import express = require('express') +import { waterfall } from 'async' + +const db = require('../../../initializers/database') +import { + logger, + retryTransactionWrapper, + startSerializableTransaction, + commitTransaction, + rollbackTransaction +} from '../../../helpers' +import { + VIDEO_RATE_TYPES, + REQUEST_VIDEO_EVENT_TYPES, + REQUEST_VIDEO_QADU_TYPES +} from '../../../initializers' +import { + addEventsToRemoteVideo, + quickAndDirtyUpdatesVideoToFriends +} from '../../../lib' +import { + authenticate, + videoRateValidator +} from '../../../middlewares' + +const rateVideoRouter = express.Router() + +rateVideoRouter.put('/:id/rate', + authenticate, + videoRateValidator, + rateVideoRetryWrapper +) + +// --------------------------------------------------------------------------- + +export { + rateVideoRouter +} + +// --------------------------------------------------------------------------- + +function rateVideoRetryWrapper (req, res, next) { + const options = { + arguments: [ req, res ], + errorMessage: 'Cannot update the user video rate.' + } + + retryTransactionWrapper(rateVideo, options, function (err) { + if (err) return next(err) + + return res.type('json').status(204).end() + }) +} + +function rateVideo (req, res, finalCallback) { + const rateType = req.body.rating + const videoInstance = res.locals.video + const userInstance = res.locals.oauth.token.User + + waterfall([ + startSerializableTransaction, + + function findPreviousRate (t, callback) { + db.UserVideoRate.load(userInstance.id, videoInstance.id, t, function (err, previousRate) { + return callback(err, t, previousRate) + }) + }, + + function insertUserRateIntoDB (t, previousRate, callback) { + const options = { transaction: t } + + let likesToIncrement = 0 + let dislikesToIncrement = 0 + + if (rateType === VIDEO_RATE_TYPES.LIKE) likesToIncrement++ + else if (rateType === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++ + + // There was a previous rate, update it + if (previousRate) { + // We will remove the previous rate, so we will need to remove it from the video attribute + if (previousRate.type === VIDEO_RATE_TYPES.LIKE) likesToIncrement-- + else if (previousRate.type === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement-- + + previousRate.type = rateType + + previousRate.save(options).asCallback(function (err) { + return callback(err, t, likesToIncrement, dislikesToIncrement) + }) + } else { // There was not a previous rate, insert a new one + const query = { + userId: userInstance.id, + videoId: videoInstance.id, + type: rateType + } + + db.UserVideoRate.create(query, options).asCallback(function (err) { + return callback(err, t, likesToIncrement, dislikesToIncrement) + }) + } + }, + + function updateVideoAttributeDB (t, likesToIncrement, dislikesToIncrement, callback) { + const options = { transaction: t } + const incrementQuery = { + likes: likesToIncrement, + dislikes: dislikesToIncrement + } + + // Even if we do not own the video we increment the attributes + // It is usefull for the user to have a feedback + videoInstance.increment(incrementQuery, options).asCallback(function (err) { + return callback(err, t, likesToIncrement, dislikesToIncrement) + }) + }, + + function sendEventsToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) { + // No need for an event type, we own the video + if (videoInstance.isOwned()) return callback(null, t, likesToIncrement, dislikesToIncrement) + + const eventsParams = [] + + if (likesToIncrement !== 0) { + eventsParams.push({ + videoId: videoInstance.id, + type: REQUEST_VIDEO_EVENT_TYPES.LIKES, + count: likesToIncrement + }) + } + + if (dislikesToIncrement !== 0) { + eventsParams.push({ + videoId: videoInstance.id, + type: REQUEST_VIDEO_EVENT_TYPES.DISLIKES, + count: dislikesToIncrement + }) + } + + addEventsToRemoteVideo(eventsParams, t, function (err) { + return callback(err, t, likesToIncrement, dislikesToIncrement) + }) + }, + + function sendQaduToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) { + // We do not own the video, there is no need to send a quick and dirty update to friends + // Our rate was already sent by the addEvent function + if (videoInstance.isOwned() === false) return callback(null, t) + + const qadusParams = [] + + if (likesToIncrement !== 0) { + qadusParams.push({ + videoId: videoInstance.id, + type: REQUEST_VIDEO_QADU_TYPES.LIKES + }) + } + + if (dislikesToIncrement !== 0) { + qadusParams.push({ + videoId: videoInstance.id, + type: REQUEST_VIDEO_QADU_TYPES.DISLIKES + }) + } + + quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) { + return callback(err, t) + }) + }, + + commitTransaction + + ], function (err, t) { + if (err) { + // This is just a debug because we will retry the insert + logger.debug('Cannot add the user video rate.', { error: err }) + return rollbackTransaction(err, t, finalCallback) + } + + logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username) + return finalCallback(null) + }) +} -- cgit v1.2.3