From 6fcd19ba737f1f5614a56c6925adb882dea43b8d Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 5 Jul 2017 13:26:25 +0200 Subject: Move to promises Closes https://github.com/Chocobozzz/PeerTube/issues/74 --- server/controllers/api/oauth-clients.ts | 21 +- server/controllers/api/pods.ts | 71 +-- server/controllers/api/remote/pods.ts | 20 +- server/controllers/api/remote/videos.ts | 528 ++++++++------------- server/controllers/api/request-schedulers.ts | 38 +- server/controllers/api/users.ts | 86 ++-- server/controllers/api/videos/abuse.ts | 78 ++- server/controllers/api/videos/blacklist.ts | 10 +- server/controllers/api/videos/index.ts | 381 +++++++-------- server/controllers/api/videos/rate.ts | 207 ++++---- server/controllers/client.ts | 26 +- server/helpers/core-utils.ts | 89 +++- server/helpers/database-utils.ts | 69 +-- server/helpers/peertube-crypto.ts | 111 ++--- server/helpers/requests.ts | 81 ++-- server/helpers/utils.ts | 24 +- server/initializers/checker.ts | 52 +- server/initializers/database.ts | 99 ++-- server/initializers/installer.ts | 75 +-- server/initializers/migrations/0005-email-pod.ts | 38 +- server/initializers/migrations/0010-email-user.ts | 39 +- server/initializers/migrations/0015-video-views.ts | 13 +- server/initializers/migrations/0020-video-likes.ts | 13 +- .../initializers/migrations/0025-video-dislikes.ts | 13 +- .../initializers/migrations/0030-video-category.ts | 29 +- .../initializers/migrations/0035-video-licence.ts | 30 +- server/initializers/migrations/0040-video-nsfw.ts | 29 +- .../migrations/0045-user-display-nsfw.ts | 13 +- .../initializers/migrations/0050-video-language.ts | 13 +- server/initializers/migrator.ts | 133 ++---- server/lib/friends.ts | 318 ++++++------- server/lib/jobs/handlers/index.ts | 8 +- server/lib/jobs/handlers/video-transcoder.ts | 22 +- server/lib/jobs/job-scheduler.ts | 117 +++-- server/lib/oauth-model.ts | 15 +- server/lib/request/abstract-request-scheduler.ts | 124 +++-- server/lib/request/request-scheduler.ts | 32 +- .../lib/request/request-video-event-scheduler.ts | 13 +- server/lib/request/request-video-qadu-scheduler.ts | 13 +- server/middlewares/secure.ts | 56 +-- server/middlewares/validators/pods.ts | 44 +- server/middlewares/validators/users.ts | 52 +- server/middlewares/validators/videos.ts | 75 ++- server/models/application/application-interface.ts | 10 +- server/models/application/application.ts | 13 +- server/models/job/job-interface.ts | 4 +- server/models/job/job.ts | 5 +- server/models/oauth/oauth-client-interface.ts | 9 +- server/models/oauth/oauth-client.ts | 9 +- server/models/oauth/oauth-token-interface.ts | 11 +- server/models/oauth/oauth-token.ts | 5 +- server/models/pod/pod-interface.ts | 28 +- server/models/pod/pod.ts | 115 ++--- .../models/request/abstract-request-interface.ts | 12 + server/models/request/index.ts | 1 + server/models/request/request-interface.ts | 16 +- server/models/request/request-to-pod-interface.ts | 8 +- server/models/request/request-to-pod.ts | 7 +- .../request/request-video-event-interface.ts | 22 +- server/models/request/request-video-event.ts | 27 +- .../models/request/request-video-qadu-interface.ts | 22 +- server/models/request/request-video-qadu.ts | 27 +- server/models/request/request.ts | 28 +- server/models/user/user-interface.ts | 26 +- server/models/user/user-video-rate-interface.ts | 4 +- server/models/user/user-video-rate.ts | 5 +- server/models/user/user.ts | 47 +- server/models/video/author-interface.ts | 9 +- server/models/video/author.ts | 23 +- server/models/video/tag-interface.ts | 4 +- server/models/video/tag.ts | 24 +- server/models/video/video-abuse-interface.ts | 5 +- server/models/video/video-abuse.ts | 11 +- server/models/video/video-blacklist-interface.ts | 24 +- server/models/video/video-blacklist.ts | 28 +- server/models/video/video-interface.ts | 70 ++- server/models/video/video-tag.ts | 6 +- server/models/video/video.ts | 369 ++++++-------- server/tests/api/friends-advanced.js | 19 +- 79 files changed, 1926 insertions(+), 2445 deletions(-) create mode 100644 server/models/request/abstract-request-interface.ts (limited to 'server') diff --git a/server/controllers/api/oauth-clients.ts b/server/controllers/api/oauth-clients.ts index b9bc0534f..f7dac598c 100644 --- a/server/controllers/api/oauth-clients.ts +++ b/server/controllers/api/oauth-clients.ts @@ -24,16 +24,17 @@ function getLocalClient (req: express.Request, res: express.Response, next: expr return res.type('json').status(403).end() } - db.OAuthClient.loadFirstClient(function (err, client) { - if (err) return next(err) - if (!client) return next(new Error('No client available.')) - - const json: OAuthClientLocal = { - client_id: client.clientId, - client_secret: client.clientSecret - } - res.json(json) - }) + db.OAuthClient.loadFirstClient() + .then(client => { + if (!client) throw new Error('No client available.') + + const json: OAuthClientLocal = { + client_id: client.clientId, + client_secret: client.clientSecret + } + res.json(json) + }) + .catch(err => next(err)) } // --------------------------------------------------------------------------- diff --git a/server/controllers/api/pods.ts b/server/controllers/api/pods.ts index 283105f6c..0f85ab51d 100644 --- a/server/controllers/api/pods.ts +++ b/server/controllers/api/pods.ts @@ -1,5 +1,4 @@ import * as express from 'express' -import { waterfall } from 'async' import { database as db } from '../../initializers/database' import { CONFIG } from '../../initializers' @@ -57,65 +56,39 @@ export { function addPods (req: express.Request, res: express.Response, next: express.NextFunction) { const informations = req.body - waterfall([ - function addPod (callback) { - const pod = db.Pod.build(informations) - pod.save().asCallback(function (err, podCreated) { - // Be sure about the number of parameters for the callback - return callback(err, podCreated) - }) - }, - - function sendMyVideos (podCreated: PodInstance, callback) { - sendOwnedVideosToPod(podCreated.id) - - callback(null) - }, - - function fetchMyCertificate (callback) { - getMyPublicCert(function (err, cert) { - if (err) { - logger.error('Cannot read cert file.') - return callback(err) - } - - return callback(null, cert) - }) - } - ], function (err, cert) { - if (err) return next(err) - - return res.json({ cert: cert, email: CONFIG.ADMIN.EMAIL }) - }) + const pod = db.Pod.build(informations) + pod.save() + .then(podCreated => { + return sendOwnedVideosToPod(podCreated.id) + }) + .then(() => { + return getMyPublicCert() + }) + .then(cert => { + return res.json({ cert: cert, email: CONFIG.ADMIN.EMAIL }) + }) + .catch(err => next(err)) } function listPods (req: express.Request, res: express.Response, next: express.NextFunction) { - db.Pod.list(function (err, podsList) { - if (err) return next(err) - - res.json(getFormatedObjects(podsList, podsList.length)) - }) + db.Pod.list() + .then(podsList => res.json(getFormatedObjects(podsList, podsList.length))) + .catch(err => next(err)) } function makeFriendsController (req: express.Request, res: express.Response, next: express.NextFunction) { const hosts = req.body.hosts as string[] - makeFriends(hosts, function (err) { - if (err) { - logger.error('Could not make friends.', { error: err }) - return - } - - logger.info('Made friends!') - }) + makeFriends(hosts) + .then(() => logger.info('Made friends!')) + .catch(err => logger.error('Could not make friends.', { error: err })) + // Don't wait the process that could be long res.type('json').status(204).end() } function quitFriendsController (req: express.Request, res: express.Response, next: express.NextFunction) { - quitFriends(function (err) { - if (err) return next(err) - - res.type('json').status(204).end() - }) + quitFriends() + .then(() => res.type('json').status(204).end()) + .catch(err => next(err)) } diff --git a/server/controllers/api/remote/pods.ts b/server/controllers/api/remote/pods.ts index b0d6642c1..6319957d3 100644 --- a/server/controllers/api/remote/pods.ts +++ b/server/controllers/api/remote/pods.ts @@ -1,5 +1,4 @@ import * as express from 'express' -import * as waterfall from 'async/waterfall' import { database as db } from '../../../initializers/database' import { checkSignature, signatureValidator } from '../../../middlewares' @@ -24,17 +23,10 @@ export { function removePods (req: express.Request, res: express.Response, next: express.NextFunction) { const host = req.body.signature.host - waterfall([ - function loadPod (callback) { - db.Pod.loadByHost(host, callback) - }, - - function deletePod (pod, callback) { - pod.destroy().asCallback(callback) - } - ], function (err) { - if (err) return next(err) - - return res.type('json').status(204).end() - }) + db.Pod.loadByHost(host) + .then(pod => { + return pod.destroy() + }) + .then(() => res.type('json').status(204).end()) + .catch(err => next(err)) } diff --git a/server/controllers/api/remote/videos.ts b/server/controllers/api/remote/videos.ts index d9cc08fb4..ebe4eca36 100644 --- a/server/controllers/api/remote/videos.ts +++ b/server/controllers/api/remote/videos.ts @@ -1,6 +1,5 @@ import * as express from 'express' -import * as Sequelize from 'sequelize' -import { eachSeries, waterfall } from 'async' +import * as Promise from 'bluebird' import { database as db } from '../../../initializers/database' import { @@ -16,20 +15,14 @@ import { remoteQaduVideosValidator, remoteEventsVideosValidator } from '../../../middlewares' -import { - logger, - commitTransaction, - retryTransactionWrapper, - rollbackTransaction, - startSerializableTransaction -} from '../../../helpers' +import { logger, retryTransactionWrapper } from '../../../helpers' import { quickAndDirtyUpdatesVideoToFriends } from '../../../lib' import { PodInstance, VideoInstance } from '../../../models' const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] // Functions to call when processing a remote request -const functionsHash = {} +const functionsHash: { [ id: string ]: (...args) => Promise } = {} functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo @@ -72,20 +65,19 @@ function remoteVideos (req: express.Request, res: express.Response, next: expres // We need to process in the same order to keep consistency // TODO: optimization - eachSeries(requests, function (request: any, callbackEach) { + Promise.mapSeries(requests, (request: any) => { 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) + return } - fun.call(this, data, fromPod, callbackEach) - }, function (err) { - if (err) logger.error('Error managing remote videos.', { error: err }) + return fun.call(this, data, fromPod) }) + .catch(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() @@ -95,13 +87,12 @@ function remoteVideosQadu (req: express.Request, res: express.Response, next: ex const requests = req.body.data const fromPod = res.locals.secure.pod - eachSeries(requests, function (request: any, callbackEach) { + Promise.mapSeries(requests, (request: any) => { const videoData = request.data - quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod, callbackEach) - }, function (err) { - if (err) logger.error('Error managing remote videos.', { error: err }) + return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod) }) + .catch(err => logger.error('Error managing remote videos.', { error: err })) return res.type('json').status(204).end() } @@ -110,414 +101,303 @@ function remoteVideosEvents (req: express.Request, res: express.Response, next: const requests = req.body.data const fromPod = res.locals.secure.pod - eachSeries(requests, function (request: any, callbackEach) { + Promise.mapSeries(requests, (request: any) => { const eventData = request.data - processVideosEventsRetryWrapper(eventData, fromPod, callbackEach) - }, function (err) { - if (err) logger.error('Error managing remote videos.', { error: err }) + return processVideosEventsRetryWrapper(eventData, fromPod) }) + .catch(err => logger.error('Error managing remote videos.', { error: err })) return res.type('json').status(204).end() } -function processVideosEventsRetryWrapper (eventData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { +function processVideosEventsRetryWrapper (eventData: any, fromPod: PodInstance) { const options = { arguments: [ eventData, fromPod ], errorMessage: 'Cannot process videos events with many retries.' } - retryTransactionWrapper(processVideosEvents, options, finalCallback) + return retryTransactionWrapper(processVideosEvents, options) } -function processVideosEvents (eventData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { - waterfall([ - startSerializableTransaction, - - function findVideo (t, callback) { - fetchOwnedVideo(eventData.remoteId, function (err, videoInstance) { - return callback(err, t, videoInstance) - }) - }, +function processVideosEvents (eventData: any, fromPod: PodInstance) { - function updateVideoIntoDB (t, videoInstance, callback) { - const options = { transaction: t } + return db.sequelize.transaction(t => { + return fetchOwnedVideo(eventData.remoteId) + .then(videoInstance => { + const options = { transaction: t } - let columnToUpdate - let qaduType + let columnToUpdate + let qaduType - switch (eventData.eventType) { - case REQUEST_VIDEO_EVENT_TYPES.VIEWS: - columnToUpdate = 'views' - qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS - break + switch (eventData.eventType) { + case REQUEST_VIDEO_EVENT_TYPES.VIEWS: + columnToUpdate = 'views' + qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS + break - case REQUEST_VIDEO_EVENT_TYPES.LIKES: - columnToUpdate = 'likes' - qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES - break + case REQUEST_VIDEO_EVENT_TYPES.LIKES: + columnToUpdate = 'likes' + qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES + break - case REQUEST_VIDEO_EVENT_TYPES.DISLIKES: - columnToUpdate = 'dislikes' - qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES - break + case REQUEST_VIDEO_EVENT_TYPES.DISLIKES: + columnToUpdate = 'dislikes' + qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES + break - default: - return callback(new Error('Unknown video event type.')) - } + default: + throw new Error('Unknown video event type.') + } - const query = {} - query[columnToUpdate] = eventData.count + const query = {} + query[columnToUpdate] = eventData.count - videoInstance.increment(query, options).asCallback(function (err) { - return callback(err, t, videoInstance, qaduType) + return videoInstance.increment(query, options).then(() => ({ videoInstance, qaduType })) }) - }, - - function sendQaduToFriends (t, videoInstance, qaduType, callback) { - const qadusParams = [ - { - videoId: videoInstance.id, - type: qaduType - } - ] - - quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) { - return callback(err, t) + .then(({ videoInstance, qaduType }) => { + const qadusParams = [ + { + videoId: videoInstance.id, + type: qaduType + } + ] + + return quickAndDirtyUpdatesVideoToFriends(qadusParams, t) }) - }, - - commitTransaction - - ], function (err: Error, t: Sequelize.Transaction) { - if (err) { - logger.debug('Cannot process a video event.', { error: err }) - return rollbackTransaction(err, t, finalCallback) - } - - logger.info('Remote video event processed for video %s.', eventData.remoteId) - return finalCallback(null) + }) + .then(() => logger.info('Remote video event processed for video %s.', eventData.remoteId)) + .catch(err => { + logger.debug('Cannot process a video event.', { error: err }) + throw err }) } -function quickAndDirtyUpdateVideoRetryWrapper (videoData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { +function quickAndDirtyUpdateVideoRetryWrapper (videoData: any, fromPod: PodInstance) { const options = { arguments: [ videoData, fromPod ], errorMessage: 'Cannot update quick and dirty the remote video with many retries.' } - retryTransactionWrapper(quickAndDirtyUpdateVideo, options, finalCallback) + return retryTransactionWrapper(quickAndDirtyUpdateVideo, options) } -function quickAndDirtyUpdateVideo (videoData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { +function quickAndDirtyUpdateVideo (videoData: any, fromPod: PodInstance) { let videoName - waterfall([ - 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 } + return db.sequelize.transaction(t => { + return fetchRemoteVideo(fromPod.host, videoData.remoteId) + .then(videoInstance => { + const options = { transaction: t } - videoName = videoInstance.name + videoName = videoInstance.name - if (videoData.views) { - videoInstance.set('views', videoData.views) - } + if (videoData.views) { + videoInstance.set('views', videoData.views) + } - if (videoData.likes) { - videoInstance.set('likes', videoData.likes) - } + if (videoData.likes) { + videoInstance.set('likes', videoData.likes) + } - if (videoData.dislikes) { - videoInstance.set('dislikes', videoData.dislikes) - } + if (videoData.dislikes) { + videoInstance.set('dislikes', videoData.dislikes) + } - videoInstance.save(options).asCallback(function (err) { - return callback(err, t) + return videoInstance.save(options) }) - }, - - commitTransaction - - ], function (err: Error, t: Sequelize.Transaction) { - if (err) { - logger.debug('Cannot quick and dirty update the remote video.', { error: err }) - return rollbackTransaction(err, t, finalCallback) - } - - logger.info('Remote video %s quick and dirty updated', videoName) - return finalCallback(null) }) + .then(() => logger.info('Remote video %s quick and dirty updated', videoName)) + .catch(err => logger.debug('Cannot quick and dirty update the remote video.', { error: err })) } // Handle retries on fail -function addRemoteVideoRetryWrapper (videoToCreateData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { +function addRemoteVideoRetryWrapper (videoToCreateData: any, fromPod: PodInstance) { const options = { arguments: [ videoToCreateData, fromPod ], errorMessage: 'Cannot insert the remote video with many retries.' } - retryTransactionWrapper(addRemoteVideo, options, finalCallback) + return retryTransactionWrapper(addRemoteVideo, options) } -function addRemoteVideo (videoToCreateData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { +function addRemoteVideo (videoToCreateData: any, fromPod: PodInstance) { logger.debug('Adding remote video "%s".', videoToCreateData.remoteId) - waterfall([ - - startSerializableTransaction, - - function assertRemoteIdAndHostUnique (t, callback) { - db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId, function (err, video) { - if (err) return callback(err) + return db.sequelize.transaction(t => { + return db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId) + .then(video => { + if (video) throw new Error('RemoteId and host pair is not unique.') - if (video) return callback(new Error('RemoteId and host pair is not unique.')) - - return callback(null, t) + return undefined }) - }, - - function findOrCreateAuthor (t, callback) { - const name = videoToCreateData.author - const podId = fromPod.id - // This author is from another pod so we do not associate a user - const userId = null + .then(() => { + const name = videoToCreateData.author + const podId = fromPod.id + // This author is from another pod so we do not associate a user + const userId = null - db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) { - return callback(err, t, authorInstance) + return db.Author.findOrCreateAuthor(name, podId, userId, t) }) - }, + .then(author => { + const tags = videoToCreateData.tags - function findOrCreateTags (t, author, callback) { - const tags = videoToCreateData.tags - - db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { - return callback(err, t, author, tagInstances) + return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances })) }) - }, - - function createVideoObject (t, author, tagInstances, callback) { - const videoData = { - name: videoToCreateData.name, - remoteId: videoToCreateData.remoteId, - extname: videoToCreateData.extname, - infoHash: videoToCreateData.infoHash, - category: videoToCreateData.category, - licence: videoToCreateData.licence, - language: videoToCreateData.language, - nsfw: videoToCreateData.nsfw, - description: videoToCreateData.description, - authorId: author.id, - duration: videoToCreateData.duration, - createdAt: videoToCreateData.createdAt, - // FIXME: updatedAt does not seems to be considered by Sequelize - updatedAt: videoToCreateData.updatedAt, - views: videoToCreateData.views, - likes: videoToCreateData.likes, - dislikes: videoToCreateData.dislikes - } - - const video = db.Video.build(videoData) - - return callback(null, t, tagInstances, video) - }, - - function generateThumbnail (t, tagInstances, video, callback) { - db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) { - if (err) { - logger.error('Cannot generate thumbnail from data.', { error: err }) - return callback(err) + .then(({ author, tagInstances }) => { + const videoData = { + name: videoToCreateData.name, + remoteId: videoToCreateData.remoteId, + extname: videoToCreateData.extname, + infoHash: videoToCreateData.infoHash, + category: videoToCreateData.category, + licence: videoToCreateData.licence, + language: videoToCreateData.language, + nsfw: videoToCreateData.nsfw, + description: videoToCreateData.description, + authorId: author.id, + duration: videoToCreateData.duration, + createdAt: videoToCreateData.createdAt, + // FIXME: updatedAt does not seems to be considered by Sequelize + updatedAt: videoToCreateData.updatedAt, + views: videoToCreateData.views, + likes: videoToCreateData.likes, + dislikes: videoToCreateData.dislikes } - return callback(err, t, tagInstances, video) + const video = db.Video.build(videoData) + return { tagInstances, video } }) - }, - - function insertVideoIntoDB (t, tagInstances, video, callback) { - const options = { - transaction: t - } - - video.save(options).asCallback(function (err, videoCreated) { - return callback(err, t, tagInstances, videoCreated) + .then(({ tagInstances, video }) => { + return db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData).then(() => ({ tagInstances, video })) }) - }, - - function associateTagsToVideo (t, tagInstances, video, callback) { - const options = { - transaction: t - } + .then(({ tagInstances, video }) => { + const options = { + transaction: t + } - video.setTags(tagInstances, options).asCallback(function (err) { - return callback(err, t) + return video.save(options).then(videoCreated => ({ tagInstances, videoCreated })) }) - }, - - commitTransaction - - ], function (err: Error, t: Sequelize.Transaction) { - if (err) { - // This is just a debug because we will retry the insert - logger.debug('Cannot insert the remote video.', { error: err }) - return rollbackTransaction(err, t, finalCallback) - } + .then(({ tagInstances, videoCreated }) => { + const options = { + transaction: t + } - logger.info('Remote video %s inserted.', videoToCreateData.name) - return finalCallback(null) + return videoCreated.setTags(tagInstances, options) + }) + }) + .then(() => logger.info('Remote video %s inserted.', videoToCreateData.name)) + .catch(err => { + logger.debug('Cannot insert the remote video.', { error: err }) + throw err }) } // Handle retries on fail -function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { +function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: any, fromPod: PodInstance) { const options = { arguments: [ videoAttributesToUpdate, fromPod ], errorMessage: 'Cannot update the remote video with many retries' } - retryTransactionWrapper(updateRemoteVideo, options, finalCallback) + return retryTransactionWrapper(updateRemoteVideo, options) } -function updateRemoteVideo (videoAttributesToUpdate: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { +function updateRemoteVideo (videoAttributesToUpdate: any, fromPod: PodInstance) { logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId) - waterfall([ + return db.sequelize.transaction(t => { + return fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId) + .then(videoInstance => { + const tags = videoAttributesToUpdate.tags - startSerializableTransaction, - - function findVideo (t, callback) { - fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) { - return callback(err, t, videoInstance) + return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoInstance, tagInstances })) }) - }, - - function findOrCreateTags (t, videoInstance, callback) { - const tags = videoAttributesToUpdate.tags - - db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { - return callback(err, t, videoInstance, tagInstances) - }) - }, - - function updateVideoIntoDB (t, videoInstance, tagInstances, callback) { - const options = { transaction: t } - - videoInstance.set('name', videoAttributesToUpdate.name) - videoInstance.set('category', videoAttributesToUpdate.category) - videoInstance.set('licence', videoAttributesToUpdate.licence) - videoInstance.set('language', videoAttributesToUpdate.language) - videoInstance.set('nsfw', videoAttributesToUpdate.nsfw) - videoInstance.set('description', videoAttributesToUpdate.description) - videoInstance.set('infoHash', videoAttributesToUpdate.infoHash) - videoInstance.set('duration', videoAttributesToUpdate.duration) - videoInstance.set('createdAt', videoAttributesToUpdate.createdAt) - videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) - videoInstance.set('extname', videoAttributesToUpdate.extname) - videoInstance.set('views', videoAttributesToUpdate.views) - videoInstance.set('likes', videoAttributesToUpdate.likes) - videoInstance.set('dislikes', videoAttributesToUpdate.dislikes) - - videoInstance.save(options).asCallback(function (err) { - return callback(err, t, videoInstance, tagInstances) + .then(({ videoInstance, tagInstances }) => { + const options = { transaction: t } + + videoInstance.set('name', videoAttributesToUpdate.name) + videoInstance.set('category', videoAttributesToUpdate.category) + videoInstance.set('licence', videoAttributesToUpdate.licence) + videoInstance.set('language', videoAttributesToUpdate.language) + videoInstance.set('nsfw', videoAttributesToUpdate.nsfw) + videoInstance.set('description', videoAttributesToUpdate.description) + videoInstance.set('infoHash', videoAttributesToUpdate.infoHash) + videoInstance.set('duration', videoAttributesToUpdate.duration) + videoInstance.set('createdAt', videoAttributesToUpdate.createdAt) + videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) + videoInstance.set('extname', videoAttributesToUpdate.extname) + videoInstance.set('views', videoAttributesToUpdate.views) + videoInstance.set('likes', videoAttributesToUpdate.likes) + videoInstance.set('dislikes', videoAttributesToUpdate.dislikes) + + return videoInstance.save(options).then(() => ({ videoInstance, tagInstances })) }) - }, - - function associateTagsToVideo (t, videoInstance, tagInstances, callback) { - const options = { transaction: t } + .then(({ videoInstance, tagInstances }) => { + const options = { transaction: t } - videoInstance.setTags(tagInstances, options).asCallback(function (err) { - return callback(err, t) + return videoInstance.setTags(tagInstances, options) }) - }, - - commitTransaction - - ], function (err: Error, t: Sequelize.Transaction) { - if (err) { - // This is just a debug because we will retry the insert - logger.debug('Cannot update the remote video.', { error: err }) - return rollbackTransaction(err, t, finalCallback) - } - - logger.info('Remote video %s updated', videoAttributesToUpdate.name) - return finalCallback(null) + }) + .then(() => logger.info('Remote video %s updated', videoAttributesToUpdate.name)) + .catch(err => { + // This is just a debug because we will retry the insert + logger.debug('Cannot update the remote video.', { error: err }) + throw err }) } -function removeRemoteVideo (videoToRemoveData: any, fromPod: PodInstance, callback: (err: Error) => void) { +function removeRemoteVideo (videoToRemoveData: any, fromPod: PodInstance) { // 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) + return fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId) + .then(video => { + logger.debug('Removing remote video %s.', video.remoteId) + return video.destroy() + }) + .catch(err => { + logger.debug('Could not fetch remote video.', { host: fromPod.host, remoteId: videoToRemoveData.remoteId, error: err }) }) - }) } -function reportAbuseRemoteVideo (reportData: any, fromPod: PodInstance, callback: (err: Error) => void) { - 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) - } - - logger.debug('Reporting remote abuse for video %s.', video.id) - - const videoAbuseData = { - reporterUsername: reportData.reporterUsername, - reason: reportData.reportReason, - reporterPodId: fromPod.id, - videoId: video.id - } +function reportAbuseRemoteVideo (reportData: any, fromPod: PodInstance) { + return fetchOwnedVideo(reportData.videoRemoteId) + .then(video => { + logger.debug('Reporting remote abuse for video %s.', video.id) - db.VideoAbuse.create(videoAbuseData).asCallback(function (err) { - if (err) { - logger.error('Cannot create remote abuse video.', { error: err }) + const videoAbuseData = { + reporterUsername: reportData.reporterUsername, + reason: reportData.reportReason, + reporterPodId: fromPod.id, + videoId: video.id } - return callback(null) + return db.VideoAbuse.create(videoAbuseData) }) - }) + .catch(err => logger.error('Cannot create remote abuse video.', { error: err })) } -function fetchOwnedVideo (id: string, callback: (err: Error, video?: VideoInstance) => void) { - db.Video.load(id, function (err, video) { - if (err || !video) { - if (!err) err = new Error('video not found') +function fetchOwnedVideo (id: string) { + return db.Video.load(id) + .then(video => { + if (!video) throw new Error('Video not found') + return video + }) + .catch(err => { logger.error('Cannot load owned video from id.', { error: err, id }) - return callback(err) - } - - return callback(null, video) - }) + throw err + }) } -function fetchRemoteVideo (podHost: string, remoteId: string, callback: (err: Error, video?: VideoInstance) => void) { - db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) { - if (err || !video) { - if (!err) err = new Error('video not found') +function fetchRemoteVideo (podHost: string, remoteId: string) { + return db.Video.loadByHostAndRemoteId(podHost, remoteId) + .then(video => { + if (!video) throw new Error('Video not found') + return video + }) + .catch(err => { logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId }) - return callback(err) - } - - return callback(null, video) - }) + throw err + }) } diff --git a/server/controllers/api/request-schedulers.ts b/server/controllers/api/request-schedulers.ts index 8dd849007..2a934a512 100644 --- a/server/controllers/api/request-schedulers.ts +++ b/server/controllers/api/request-schedulers.ts @@ -1,5 +1,5 @@ import * as express from 'express' -import { parallel } from 'async' +import * as Promise from 'bluebird' import { AbstractRequestScheduler, @@ -27,33 +27,27 @@ export { // --------------------------------------------------------------------------- function getRequestSchedulersStats (req: express.Request, res: express.Response, next: express.NextFunction) { - parallel({ + Promise.props({ requestScheduler: buildRequestSchedulerStats(getRequestScheduler()), requestVideoQaduScheduler: buildRequestSchedulerStats(getRequestVideoQaduScheduler()), requestVideoEventScheduler: buildRequestSchedulerStats(getRequestVideoEventScheduler()) - }, function (err, result) { - if (err) return next(err) - - return res.json(result) }) + .then(result => res.json(result)) + .catch(err => next(err)) } // --------------------------------------------------------------------------- -function buildRequestSchedulerStats (requestScheduler: AbstractRequestScheduler) { - return function (callback) { - requestScheduler.remainingRequestsCount(function (err, count) { - if (err) return callback(err) - - const result: RequestSchedulerStatsAttributes = { - totalRequests: count, - requestsLimitPods: requestScheduler.limitPods, - requestsLimitPerPod: requestScheduler.limitPerPod, - remainingMilliSeconds: requestScheduler.remainingMilliSeconds(), - milliSecondsInterval: requestScheduler.requestInterval - } - - return callback(null, result) - }) - } +function buildRequestSchedulerStats (requestScheduler: AbstractRequestScheduler) { + return requestScheduler.remainingRequestsCount().then(count => { + const result: RequestSchedulerStatsAttributes = { + totalRequests: count, + requestsLimitPods: requestScheduler.limitPods, + requestsLimitPerPod: requestScheduler.limitPerPod, + remainingMilliSeconds: requestScheduler.remainingMilliSeconds(), + milliSecondsInterval: requestScheduler.requestInterval + } + + return result + }) } diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index ce15353ef..6e0bb474a 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts @@ -1,8 +1,7 @@ import * as express from 'express' -import { waterfall } from 'async' import { database as db } from '../../initializers/database' -import { CONFIG, USER_ROLES } from '../../initializers' +import { USER_ROLES } from '../../initializers' import { logger, getFormatedObjects } from '../../helpers' import { authenticate, @@ -87,78 +86,61 @@ function createUser (req: express.Request, res: express.Response, next: express. role: USER_ROLES.USER }) - user.save().asCallback(function (err) { - if (err) return next(err) - - return res.type('json').status(204).end() - }) + user.save() + .then(() => res.type('json').status(204).end()) + .catch(err => next(err)) } function getUserInformation (req: express.Request, res: express.Response, next: express.NextFunction) { - db.User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) { - if (err) return next(err) - - return res.json(user.toFormatedJSON()) - }) + db.User.loadByUsername(res.locals.oauth.token.user.username) + .then(user => res.json(user.toFormatedJSON())) + .catch(err => next(err)) } function getUserVideoRating (req: express.Request, res: express.Response, next: express.NextFunction) { const videoId = '' + req.params.videoId const userId = +res.locals.oauth.token.User.id - db.UserVideoRate.load(userId, videoId, null, function (err, ratingObj) { - if (err) return next(err) - - const rating = ratingObj ? ratingObj.type : 'none' - - const json: FormatedUserVideoRate = { - videoId, - rating - } - res.json(json) - }) + db.UserVideoRate.load(userId, videoId, null) + .then(ratingObj => { + const rating = ratingObj ? ratingObj.type : 'none' + const json: FormatedUserVideoRate = { + videoId, + rating + } + res.json(json) + }) + .catch(err => next(err)) } function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { - db.User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) { - if (err) return next(err) - - res.json(getFormatedObjects(usersList, usersTotal)) - }) + db.User.listForApi(req.query.start, req.query.count, req.query.sort) + .then(resultList => { + res.json(getFormatedObjects(resultList.data, resultList.total)) + }) + .catch(err => next(err)) } function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) { - waterfall([ - function loadUser (callback) { - db.User.loadById(req.params.id, callback) - }, - - function deleteUser (user, callback) { - user.destroy().asCallback(callback) - } - ], function andFinally (err) { - if (err) { + db.User.loadById(req.params.id) + .then(user => user.destroy()) + .then(() => res.sendStatus(204)) + .catch(err => { logger.error('Errors when removed the user.', { error: err }) return next(err) - } - - return res.sendStatus(204) - }) + }) } function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { - db.User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) { - if (err) return next(err) - - if (req.body.password) user.password = req.body.password - if (req.body.displayNSFW !== undefined) user.displayNSFW = req.body.displayNSFW + db.User.loadByUsername(res.locals.oauth.token.user.username) + .then(user => { + if (req.body.password) user.password = req.body.password + if (req.body.displayNSFW !== undefined) user.displayNSFW = req.body.displayNSFW - user.save().asCallback(function (err) { - if (err) return next(err) - - return res.sendStatus(204) + return user.save() }) - }) + .then(() => res.sendStatus(204)) + .catch(err => next(err)) } function success (req: express.Request, res: express.Response, next: express.NextFunction) { diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts index 78e8e8b3d..fcbd5465f 100644 --- a/server/controllers/api/videos/abuse.ts +++ b/server/controllers/api/videos/abuse.ts @@ -1,16 +1,11 @@ import * as express from 'express' -import * as Sequelize from 'sequelize' -import { waterfall } from 'async' import { database as db } from '../../../initializers/database' import * as friends from '../../../lib/friends' import { logger, getFormatedObjects, - retryTransactionWrapper, - startSerializableTransaction, - commitTransaction, - rollbackTransaction + retryTransactionWrapper } from '../../../helpers' import { authenticate, @@ -21,6 +16,7 @@ import { setVideoAbusesSort, setPagination } from '../../../middlewares' +import { VideoInstance } from '../../../models' const abuseVideoRouter = express.Router() @@ -48,11 +44,9 @@ export { // --------------------------------------------------------------------------- function listVideoAbuses (req: express.Request, res: express.Response, next: express.NextFunction) { - 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)) - }) + db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort) + .then(result => res.json(getFormatedObjects(result.data, result.total))) + .catch(err => next(err)) } function reportVideoAbuseRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { @@ -61,14 +55,12 @@ function reportVideoAbuseRetryWrapper (req: express.Request, res: express.Respon 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() - }) + retryTransactionWrapper(reportVideoAbuse, options) + .then(() => res.type('json').status(204).end()) + .catch(err => next(err)) } -function reportVideoAbuse (req: express.Request, res: express.Response, finalCallback: (err: Error) => void) { +function reportVideoAbuse (req: express.Request, res: express.Response) { const videoInstance = res.locals.video const reporterUsername = res.locals.oauth.token.User.username @@ -79,40 +71,26 @@ function reportVideoAbuse (req: express.Request, res: express.Response, finalCal 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 + return db.sequelize.transaction(t => { + return db.VideoAbuse.create(abuse, { transaction: t }) + .then(abuse => { + // We send the information to the destination pod + if (videoInstance.isOwned() === false) { + const reportData = { + reporterUsername, + reportReason: abuse.reason, + videoRemoteId: videoInstance.remoteId + } + + return friends.reportAbuseVideoToFriend(reportData, videoInstance, t).then(() => videoInstance) } - friends.reportAbuseVideoToFriend(reportData, videoInstance) - } - - return callback(null, t) - }, - - commitTransaction - - ], function andFinally (err: Error, t: Sequelize.Transaction) { - 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) + return videoInstance + }) + }) + .then((videoInstance: VideoInstance) => logger.info('Abuse report for video %s created.', videoInstance.name)) + .catch(err => { + logger.debug('Cannot update the video.', { error: err }) + throw err }) } diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts index 4b42fc2d7..e4be6f0f9 100644 --- a/server/controllers/api/videos/blacklist.ts +++ b/server/controllers/api/videos/blacklist.ts @@ -32,12 +32,10 @@ function addVideoToBlacklist (req: express.Request, res: express.Response, next: videoId: videoInstance.id } - db.BlacklistedVideo.create(toCreate).asCallback(function (err) { - if (err) { + db.BlacklistedVideo.create(toCreate) + .then(() => res.type('json').status(204).end()) + .catch(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.ts b/server/controllers/api/videos/index.ts index 5e8cf2d25..ed1f21d66 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -1,9 +1,7 @@ import * as express from 'express' -import * as Sequelize from 'sequelize' -import * as fs from 'fs' +import * as Promise from 'bluebird' import * as multer from 'multer' import * as path from 'path' -import { waterfall } from 'async' import { database as db } from '../../../initializers/database' import { @@ -35,13 +33,12 @@ import { } from '../../../middlewares' import { logger, - commitTransaction, retryTransactionWrapper, - rollbackTransaction, - startSerializableTransaction, generateRandomString, - getFormatedObjects + getFormatedObjects, + renamePromise } from '../../../helpers' +import { TagInstance } from '../../../models' import { abuseVideoRouter } from './abuse' import { blacklistRouter } from './blacklist' @@ -60,10 +57,15 @@ const storage = multer.diskStorage({ 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) - }) + generateRandomString(16) + .then(randomString => { + const filename = randomString + cb(null, filename + '.' + extension) + }) + .catch(err => { + logger.error('Cannot generate random string for file name.', { error: err }) + throw err + }) } }) @@ -144,125 +146,97 @@ function addVideoRetryWrapper (req: express.Request, res: express.Response, next 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() - }) + retryTransactionWrapper(addVideo, options) + .then(() => { + // TODO : include Location of the new video -> 201 + res.type('json').status(204).end() + }) + .catch(err => next(err)) } -function addVideo (req: express.Request, res: express.Response, videoFile: Express.Multer.File, finalCallback: (err: Error) => void) { +function addVideo (req: express.Request, res: express.Response, videoFile: Express.Multer.File) { const videoInfos = req.body - waterfall([ - - startSerializableTransaction, + return db.sequelize.transaction(t => { + const user = res.locals.oauth.token.User - 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 - const name = user.username - // null because it is OUR pod - const podId = null - const userId = user.id + return db.Author.findOrCreateAuthor(name, podId, userId, t) + .then(author => { + const tags = videoInfos.tags + if (!tags) return { author, tagInstances: undefined } - db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) { - return callback(err, t, authorInstance) + return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances })) }) - }, - - function findOrCreateTags (t, author, callback) { - const tags = videoInfos.tags - - db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { - return callback(err, t, author, tagInstances) + .then(({ author, tagInstances }) => { + 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'], // duration was added by a previous middleware + authorId: author.id + } + + const video = db.Video.build(videoData) + return { author, tagInstances, video } }) - }, - - 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'], // duration was added by a previous middleware - 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) + .then(({ author, tagInstances, video }) => { + const videoDir = CONFIG.STORAGE.VIDEOS_DIR + const source = path.join(videoDir, videoFile.filename) + const destination = path.join(videoDir, video.getVideoFilename()) + + return renamePromise(source, destination) + .then(() => { + // This is important in case if there is another attempt in the retry process + videoFile.filename = video.getVideoFilename() + return { 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) + .then(({ author, tagInstances, video }) => { + const options = { transaction: t } - // Do not forget to add Author informations to the created video - videoCreated.Author = author + return video.save(options) + .then(videoCreated => { + // Do not forget to add Author informations to the created video + videoCreated.Author = author - return callback(err, t, tagInstances, videoCreated) + return { tagInstances, video: videoCreated } + }) }) - }, - - function associateTagsToVideo (t, tagInstances, video, callback) { - const options = { transaction: t } + .then(({ tagInstances, video }) => { + if (!tagInstances) return video - video.setTags(tagInstances, options).asCallback(function (err) { - video.Tags = tagInstances - - return callback(err, t, video) + const options = { transaction: t } + return video.setTags(tagInstances, options) + .then(() => { + video.Tags = tagInstances + return 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) - }) + .then(video => { + // Let transcoding job send the video to friends because the videofile extension might change + if (CONFIG.TRANSCODING.ENABLED === true) return undefined + + return video.toAddRemoteJSON() + .then(remoteVideo => { + // Now we'll add the video's meta data to our friends + return addVideoToFriends(remoteVideo, t) + }) }) - }, - - commitTransaction - - ], function andFinally (err: Error, t: Sequelize.Transaction) { - 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) + }) + .then(() => logger.info('Video with name %s created.', videoInfos.name)) + .catch((err: Error) => { + logger.debug('Cannot insert the video.', { error: err.stack }) + throw err }) } @@ -272,92 +246,75 @@ function updateVideoRetryWrapper (req: express.Request, res: express.Response, n 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() - }) + retryTransactionWrapper(updateVideo, options) + .then(() => { + // TODO : include Location of the new video -> 201 + return res.type('json').status(204).end() + }) + .catch(err => next(err)) } -function updateVideo (req: express.Request, res: express.Response, finalCallback: (err: Error) => void) { +function updateVideo (req: express.Request, res: express.Response) { 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 db.sequelize.transaction(t => { + let tagsPromise: Promise + if (!videoInfosToUpdate.tags) { + tagsPromise = Promise.resolve(null) + } else { + tagsPromise = db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t) + } - return callback(err, t) - }) - } else { - return callback(null, t) - } - }, + return tagsPromise + .then(tagInstances => { + const options = { + transaction: t + } - function sendToFriends (t, callback) { - const json = videoInstance.toUpdateRemoteJSON() + 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) - // Now we'll update the video's meta data to our friends - updateVideoToFriends(json, t, function (err) { - return callback(err, t) + return videoInstance.save(options).then(() => tagInstances) }) - }, - - commitTransaction + .then(tagInstances => { + if (!tagInstances) return - ], function andFinally (err: Error, t: Sequelize.Transaction) { - if (err) { - logger.debug('Cannot update the video.', { error: err }) + const options = { transaction: t } + return videoInstance.setTags(tagInstances, options) + .then(() => { + videoInstance.Tags = tagInstances - // 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 + }) }) + .then(() => { + const json = videoInstance.toUpdateRemoteJSON() - return rollbackTransaction(err, t, finalCallback) - } - + // Now we'll update the video's meta data to our friends + return updateVideoToFriends(json, t) + }) + }) + .then(() => { logger.info('Video with name %s updated.', videoInstance.name) - return finalCallback(null) + }) + .catch(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) + }) + + throw err }) } @@ -366,20 +323,17 @@ function getVideo (req: express.Request, res: express.Response, next: express.Ne 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) - }) + videoInstance.increment('views') + .then(() => { + // 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 + } + return quickAndDirtyUpdateVideoToFriends(qaduParams) + }) + .catch(err => logger.error('Cannot add view to video %d.', videoInstance.id, { error: err })) } else { // Just send the event to our friends const eventParams = { @@ -394,33 +348,24 @@ function getVideo (req: express.Request, res: express.Response, next: express.Ne } function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { - 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)) - }) + db.Video.listForApi(req.query.start, req.query.count, req.query.sort) + .then(result => res.json(getFormatedObjects(result.data, result.total))) + .catch(err => next(err)) } function removeVideo (req: express.Request, res: express.Response, next: express.NextFunction) { const videoInstance = res.locals.video - videoInstance.destroy().asCallback(function (err) { - if (err) { + videoInstance.destroy() + .then(() => res.type('json').status(204).end()) + .catch(err => { logger.error('Errors when removed the video.', { error: err }) return next(err) - } - - return res.type('json').status(204).end() - }) + }) } function searchVideos (req: express.Request, res: express.Response, next: express.NextFunction) { - 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)) - } - ) + db.Video.searchAndPopulateAuthorAndPodAndTags(req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort) + .then(result => res.json(getFormatedObjects(result.data, result.total))) + .catch(err => next(err)) } diff --git a/server/controllers/api/videos/rate.ts b/server/controllers/api/videos/rate.ts index afdd099f8..3d119d98b 100644 --- a/server/controllers/api/videos/rate.ts +++ b/server/controllers/api/videos/rate.ts @@ -1,14 +1,9 @@ import * as express from 'express' -import * as Sequelize from 'sequelize' -import { waterfall } from 'async' import { database as db } from '../../../initializers/database' import { logger, - retryTransactionWrapper, - startSerializableTransaction, - commitTransaction, - rollbackTransaction + retryTransactionWrapper } from '../../../helpers' import { VIDEO_RATE_TYPES, @@ -46,137 +41,109 @@ function rateVideoRetryWrapper (req: express.Request, res: express.Response, nex errorMessage: 'Cannot update the user video rate.' } - retryTransactionWrapper(rateVideo, options, function (err) { - if (err) return next(err) - - return res.type('json').status(204).end() - }) + retryTransactionWrapper(rateVideo, options) + .then(() => res.type('json').status(204).end()) + .catch(err => next(err)) } -function rateVideo (req: express.Request, res: express.Response, finalCallback: (err: Error) => void) { +function rateVideo (req: express.Request, res: express.Response) { 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) - }) - }, + return db.sequelize.transaction(t => { + return db.UserVideoRate.load(userInstance.id, videoInstance.id, t) + .then(previousRate => { + const options = { transaction: t } - function insertUserRateIntoDB (t, previousRate, callback) { - const options = { transaction: t } + let likesToIncrement = 0 + let dislikesToIncrement = 0 - let likesToIncrement = 0 - let dislikesToIncrement = 0 + if (rateType === VIDEO_RATE_TYPES.LIKE) likesToIncrement++ + else if (rateType === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++ - 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-- - // 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.type = rateType + return previousRate.save(options).then(() => ({ t, likesToIncrement, dislikesToIncrement })) + } else { // There was not a previous rate, insert a new one + const query = { + userId: userInstance.id, + videoId: videoInstance.id, + 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 + return db.UserVideoRate.create(query, options).then(() => ({ likesToIncrement, dislikesToIncrement })) } - - 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) + .then(({ likesToIncrement, dislikesToIncrement }) => { + 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 + return videoInstance.increment(incrementQuery, options).then(() => ({ likesToIncrement, dislikesToIncrement })) }) - }, + .then(({ likesToIncrement, dislikesToIncrement }) => { + // No need for an event type, we own the video + if (videoInstance.isOwned()) return { 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 + }) + } - commitTransaction + return addEventsToRemoteVideo(eventsParams, t).then(() => ({ likesToIncrement, dislikesToIncrement })) + }) + .then(({ likesToIncrement, dislikesToIncrement }) => { + // 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 undefined + + const qadusParams = [] + + if (likesToIncrement !== 0) { + qadusParams.push({ + videoId: videoInstance.id, + type: REQUEST_VIDEO_QADU_TYPES.LIKES + }) + } - ], function (err: Error, t: Sequelize.Transaction) { - 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) - } + if (dislikesToIncrement !== 0) { + qadusParams.push({ + videoId: videoInstance.id, + type: REQUEST_VIDEO_QADU_TYPES.DISLIKES + }) + } - logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username) - return finalCallback(null) + return quickAndDirtyUpdatesVideoToFriends(qadusParams, t) + }) + }) + .then(() => logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username)) + .catch(err => { + // This is just a debug because we will retry the insert + logger.debug('Cannot add the user video rate.', { error: err }) + throw err }) } diff --git a/server/controllers/client.ts b/server/controllers/client.ts index 503eff750..e4d69eae7 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts @@ -1,8 +1,7 @@ -import { parallel } from 'async' import * as express from 'express' -import * as fs from 'fs' import { join } from 'path' import * as validator from 'validator' +import * as Promise from 'bluebird' import { database as db } from '../initializers/database' import { @@ -11,7 +10,7 @@ import { STATIC_PATHS, STATIC_MAX_AGE } from '../initializers' -import { root } from '../helpers' +import { root, readFileBufferPromise } from '../helpers' import { VideoInstance } from '../models' const clientsRouter = express.Router() @@ -95,19 +94,15 @@ function generateWatchHtmlPage (req: express.Request, res: express.Response, nex // Let Angular application handle errors if (!validator.isUUID(videoId, 4)) return res.sendFile(indexPath) - parallel({ - file: function (callback) { - fs.readFile(indexPath, callback) - }, + Promise.all([ + readFileBufferPromise(indexPath), + db.Video.loadAndPopulateAuthorAndPodAndTags(videoId) + ]) + .then(([ file, video ]) => { + file = file as Buffer + video = video as VideoInstance - video: function (callback) { - db.Video.loadAndPopulateAuthorAndPodAndTags(videoId, callback) - } - }, function (err: Error, result: { file: Buffer, video: VideoInstance }) { - if (err) return next(err) - - const html = result.file.toString() - const video = result.video + const html = file.toString() // Let Angular application handle errors if (!video) return res.sendFile(indexPath) @@ -115,4 +110,5 @@ function generateWatchHtmlPage (req: express.Request, res: express.Response, nex const htmlStringPageWithTags = addOpenGraphTags(html, video) res.set('Content-Type', 'text/html; charset=UTF-8').send(htmlStringPageWithTags) }) + .catch(err => next(err)) } diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index 32b89b6dd..1e92049f1 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts @@ -4,6 +4,20 @@ */ import { join } from 'path' +import { pseudoRandomBytes } from 'crypto' +import { + readdir, + readFile, + rename, + unlink, + writeFile, + access +} from 'fs' +import * as mkdirp from 'mkdirp' +import * as bcrypt from 'bcrypt' +import * as createTorrent from 'create-torrent' +import * as openssl from 'openssl-wrapper' +import * as Promise from 'bluebird' function isTestInstance () { return process.env.NODE_ENV === 'test' @@ -14,9 +28,82 @@ function root () { return join(__dirname, '..', '..', '..') } +function promisify0 (func: (cb: (err: any, result: A) => void) => void): () => Promise { + return function promisified (): Promise { + return new Promise((resolve: (arg: A) => void, reject: (err: any) => void) => { + func.apply(null, [ (err: any, res: A) => err ? reject(err) : resolve(res) ]) + }) + } +} + +// Thanks to https://gist.github.com/kumasento/617daa7e46f13ecdd9b2 +function promisify1 (func: (arg: T, cb: (err: any, result: A) => void) => void): (arg: T) => Promise { + return function promisified (arg: T): Promise { + return new Promise((resolve: (arg: A) => void, reject: (err: any) => void) => { + func.apply(null, [ arg, (err: any, res: A) => err ? reject(err) : resolve(res) ]) + }) + } +} + +function promisify1WithVoid (func: (arg: T, cb: (err: any) => void) => void): (arg: T) => Promise { + return function promisified (arg: T): Promise { + return new Promise((resolve: () => void, reject: (err: any) => void) => { + func.apply(null, [ arg, (err: any) => err ? reject(err) : resolve() ]) + }) + } +} + +function promisify2 (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise { + return function promisified (arg1: T, arg2: U): Promise { + return new Promise((resolve: (arg: A) => void, reject: (err: any) => void) => { + func.apply(null, [ arg1, arg2, (err: any, res: A) => err ? reject(err) : resolve(res) ]) + }) + } +} + +function promisify2WithVoid (func: (arg1: T, arg2: U, cb: (err: any) => void) => void): (arg1: T, arg2: U) => Promise { + return function promisified (arg1: T, arg2: U): Promise { + return new Promise((resolve: () => void, reject: (err: any) => void) => { + func.apply(null, [ arg1, arg2, (err: any) => err ? reject(err) : resolve() ]) + }) + } +} + +const readFilePromise = promisify2(readFile) +const readFileBufferPromise = promisify1(readFile) +const unlinkPromise = promisify1WithVoid(unlink) +const renamePromise = promisify2WithVoid(rename) +const writeFilePromise = promisify2(writeFile) +const readdirPromise = promisify1(readdir) +const mkdirpPromise = promisify1(mkdirp) +const pseudoRandomBytesPromise = promisify1(pseudoRandomBytes) +const accessPromise = promisify1WithVoid(access) +const opensslExecPromise = promisify2WithVoid(openssl.exec) +const bcryptComparePromise = promisify2(bcrypt.compare) +const bcryptGenSaltPromise = promisify1(bcrypt.genSalt) +const bcryptHashPromise = promisify2(bcrypt.hash) +const createTorrentPromise = promisify2(createTorrent) + // --------------------------------------------------------------------------- export { isTestInstance, - root + root, + + promisify0, + promisify1, + readdirPromise, + readFilePromise, + readFileBufferPromise, + unlinkPromise, + renamePromise, + writeFilePromise, + mkdirpPromise, + pseudoRandomBytesPromise, + accessPromise, + opensslExecPromise, + bcryptComparePromise, + bcryptGenSaltPromise, + bcryptHashPromise, + createTorrentPromise } diff --git a/server/helpers/database-utils.ts b/server/helpers/database-utils.ts index f8ee9a454..f761cfb89 100644 --- a/server/helpers/database-utils.ts +++ b/server/helpers/database-utils.ts @@ -1,70 +1,45 @@ -import * as Sequelize from 'sequelize' // TODO: import from ES6 when retry typing file will include errorFilter function import * as retry from 'async/retry' +import * as Promise from 'bluebird' -import { database as db } from '../initializers/database' import { logger } from './logger' -function commitTransaction (t: Sequelize.Transaction, callback: (err: Error) => void) { - return t.commit().asCallback(callback) -} - -function rollbackTransaction (err: Error, t: Sequelize.Transaction, callback: (err: Error) => void) { - // Try to rollback transaction - if (t) { - // Do not catch err, report the original one - t.rollback().asCallback(function () { - return callback(err) - }) - } else { - return callback(err) - } -} - type RetryTransactionWrapperOptions = { errorMessage: string, arguments?: any[] } -function retryTransactionWrapper (functionToRetry: Function, options: RetryTransactionWrapperOptions, finalCallback: Function) { +function retryTransactionWrapper (functionToRetry: (... args) => Promise, options: RetryTransactionWrapperOptions) { const args = options.arguments ? options.arguments : [] - transactionRetryer( + return transactionRetryer( function (callback) { - return functionToRetry.apply(this, args.concat([ callback ])) - }, - function (err) { - if (err) { - logger.error(options.errorMessage, { error: err }) - } - - // Do not return the error, continue the process - return finalCallback(null) + functionToRetry.apply(this, args) + .then(result => callback(null, result)) + .catch(err => callback(err)) } ) + .catch(err => { + // Do not throw the error, continue the process + logger.error(options.errorMessage, { error: err }) + }) } -function transactionRetryer (func: Function, callback: (err: Error) => void) { - retry({ - times: 5, - - errorFilter: function (err) { - const willRetry = (err.name === 'SequelizeDatabaseError') - logger.debug('Maybe retrying the transaction function.', { willRetry }) - return willRetry - } - }, func, callback) -} +function transactionRetryer (func: Function) { + return new Promise((res, rej) => { + retry({ + times: 5, -function startSerializableTransaction (callback: (err: Error, t: Sequelize.Transaction) => void) { - db.sequelize.transaction(/* { isolationLevel: 'SERIALIZABLE' } */).asCallback(function (err, t) { - // We force to return only two parameters - return callback(err, t) + errorFilter: function (err) { + const willRetry = (err.name === 'SequelizeDatabaseError') + logger.debug('Maybe retrying the transaction function.', { willRetry }) + return willRetry + } + }, func, function (err) { + err ? rej(err) : res() + }) }) } // --------------------------------------------------------------------------- export { - commitTransaction, retryTransactionWrapper, - rollbackTransaction, - startSerializableTransaction, transactionRetryer } diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index 0ac875127..8e8001cd6 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts @@ -1,7 +1,5 @@ import * as crypto from 'crypto' -import * as bcrypt from 'bcrypt' import * as fs from 'fs' -import * as openssl from 'openssl-wrapper' import { join } from 'path' import { @@ -12,6 +10,14 @@ import { BCRYPT_SALT_SIZE, PUBLIC_CERT_NAME } from '../initializers' +import { + readFilePromise, + bcryptComparePromise, + bcryptGenSaltPromise, + bcryptHashPromise, + accessPromise, + opensslExecPromise +} from './core-utils' import { logger } from './logger' function checkSignature (publicKey: string, data: string, hexSignature: string) { @@ -60,46 +66,32 @@ function sign (data: string|Object) { return signature } -function comparePassword (plainPassword: string, hashPassword: string, callback: (err: Error, match?: boolean) => void) { - bcrypt.compare(plainPassword, hashPassword, function (err, isPasswordMatch) { - if (err) return callback(err) - - return callback(null, isPasswordMatch) - }) +function comparePassword (plainPassword: string, hashPassword: string) { + return bcryptComparePromise(plainPassword, hashPassword) } -function createCertsIfNotExist (callback: (err: Error) => void) { - certsExist(function (err, exist) { - if (err) return callback(err) - +function createCertsIfNotExist () { + return certsExist().then(exist => { if (exist === true) { - return callback(null) + return undefined } - createCerts(function (err) { - return callback(err) - }) + return createCerts() }) } -function cryptPassword (password: string, callback: (err: Error, hash?: string) => void) { - bcrypt.genSalt(BCRYPT_SALT_SIZE, function (err, salt) { - if (err) return callback(err) - - bcrypt.hash(password, salt, function (err, hash) { - return callback(err, hash) - }) - }) +function cryptPassword (password: string) { + return bcryptGenSaltPromise(BCRYPT_SALT_SIZE).then(salt => bcryptHashPromise(password, salt)) } -function getMyPrivateCert (callback: (err: Error, privateCert: string) => void) { +function getMyPrivateCert () { const certPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME) - fs.readFile(certPath, 'utf8', callback) + return readFilePromise(certPath, 'utf8') } -function getMyPublicCert (callback: (err: Error, publicCert: string) => void) { +function getMyPublicCert () { const certPath = join(CONFIG.STORAGE.CERT_DIR, PUBLIC_CERT_NAME) - fs.readFile(certPath, 'utf8', callback) + return readFilePromise(certPath, 'utf8') } // --------------------------------------------------------------------------- @@ -116,23 +108,21 @@ export { // --------------------------------------------------------------------------- -function certsExist (callback: (err: Error, certsExist: boolean) => void) { +function certsExist () { const certPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME) - fs.access(certPath, function (err) { - // If there is an error the certificates do not exist - const exists = !err - return callback(null, exists) - }) -} -function createCerts (callback: (err: Error) => void) { - certsExist(function (err, exist) { - if (err) return callback(err) + // If there is an error the certificates do not exist + return accessPromise(certPath) + .then(() => true) + .catch(() => false) +} +function createCerts () { + return certsExist().then(exist => { if (exist === true) { const errorMessage = 'Certs already exist.' logger.warning(errorMessage) - return callback(new Error(errorMessage)) + throw new Error(errorMessage) } logger.info('Generating a RSA key...') @@ -142,30 +132,27 @@ function createCerts (callback: (err: Error) => void) { 'out': privateCertPath, '2048': false } - openssl.exec('genrsa', genRsaOptions, function (err) { - if (err) { - logger.error('Cannot create private key on this pod.') - return callback(err) - } - - logger.info('RSA key generated.') - logger.info('Managing public key...') - - const publicCertPath = join(CONFIG.STORAGE.CERT_DIR, 'peertube.pub') - const rsaOptions = { - 'in': privateCertPath, - 'pubout': true, - 'out': publicCertPath - } - openssl.exec('rsa', rsaOptions, function (err) { - if (err) { - logger.error('Cannot create public key on this pod.') - return callback(err) + return opensslExecPromise('genrsa', genRsaOptions) + .then(() => { + logger.info('RSA key generated.') + logger.info('Managing public key...') + + const publicCertPath = join(CONFIG.STORAGE.CERT_DIR, 'peertube.pub') + const rsaOptions = { + 'in': privateCertPath, + 'pubout': true, + 'out': publicCertPath } - - logger.info('Public key managed.') - return callback(null) + return opensslExecPromise('rsa', rsaOptions) + .then(() => logger.info('Public key managed.')) + .catch(err => { + logger.error('Cannot create public key on this pod.') + throw err + }) + }) + .catch(err => { + logger.error('Cannot create private key on this pod.') + throw err }) - }) }) } diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index b40fc8e39..b31074373 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts @@ -1,5 +1,6 @@ import * as replay from 'request-replay' import * as request from 'request' +import * as Promise from 'bluebird' import { RETRY_REQUESTS, @@ -14,16 +15,18 @@ type MakeRetryRequestParams = { method: 'GET'|'POST', json: Object } -function makeRetryRequest (params: MakeRetryRequestParams, callback: request.RequestCallback) { - replay( - request(params, callback), - { - retries: RETRY_REQUESTS, - factor: 3, - maxTimeout: Infinity, - errorCodes: [ 'EADDRINFO', 'ETIMEDOUT', 'ECONNRESET', 'ESOCKETTIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED' ] - } - ) +function makeRetryRequest (params: MakeRetryRequestParams) { + return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => { + replay( + request(params, (err, response, body) => err ? rej(err) : res({ response, body })), + { + retries: RETRY_REQUESTS, + factor: 3, + maxTimeout: Infinity, + errorCodes: [ 'EADDRINFO', 'ETIMEDOUT', 'ECONNRESET', 'ESOCKETTIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED' ] + } + ) + }) } type MakeSecureRequestParams = { @@ -33,41 +36,43 @@ type MakeSecureRequestParams = { sign: boolean data?: Object } -function makeSecureRequest (params: MakeSecureRequestParams, callback: request.RequestCallback) { - const requestParams = { - url: REMOTE_SCHEME.HTTP + '://' + params.toPod.host + params.path, - json: {} - } +function makeSecureRequest (params: MakeSecureRequestParams) { + return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => { + const requestParams = { + url: REMOTE_SCHEME.HTTP + '://' + params.toPod.host + params.path, + json: {} + } - if (params.method !== 'POST') { - return callback(new Error('Cannot make a secure request with a non POST method.'), null, null) - } + if (params.method !== 'POST') { + return rej(new Error('Cannot make a secure request with a non POST method.')) + } - // Add signature if it is specified in the params - if (params.sign === true) { - const host = CONFIG.WEBSERVER.HOST + // Add signature if it is specified in the params + if (params.sign === true) { + const host = CONFIG.WEBSERVER.HOST - let dataToSign - if (params.data) { - dataToSign = params.data - } else { - // We do not have data to sign so we just take our host - // It is not ideal but the connection should be in HTTPS - dataToSign = host - } + let dataToSign + if (params.data) { + dataToSign = params.data + } else { + // We do not have data to sign so we just take our host + // It is not ideal but the connection should be in HTTPS + dataToSign = host + } - requestParams.json['signature'] = { - host, // Which host we pretend to be - signature: sign(dataToSign) + requestParams.json['signature'] = { + host, // Which host we pretend to be + signature: sign(dataToSign) + } } - } - // If there are data informations - if (params.data) { - requestParams.json['data'] = params.data - } + // If there are data informations + if (params.data) { + requestParams.json['data'] = params.data + } - request.post(requestParams, callback) + request.post(requestParams, (err, response, body) => err ? rej(err) : res({ response, body })) + }) } // --------------------------------------------------------------------------- diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index bbf135fa1..e99a48393 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts @@ -1,25 +1,14 @@ import * as express from 'express' -import { pseudoRandomBytes } from 'crypto' - -import { logger } from './logger' +import { pseudoRandomBytesPromise } from './core-utils' +import { ResultList } from '../../shared' function badRequest (req: express.Request, res: express.Response, next: express.NextFunction) { res.type('json').status(400).end() } -function generateRandomString (size: number, callback: (err: Error, randomString?: string) => void) { - pseudoRandomBytes(size, function (err, raw) { - if (err) return callback(err) - - callback(null, raw.toString('hex')) - }) -} - -function createEmptyCallback () { - return function (err) { - if (err) logger.error('Error in empty callback.', { error: err }) - } +function generateRandomString (size: number) { + return pseudoRandomBytesPromise(size).then(raw => raw.toString('hex')) } interface FormatableToJSON { @@ -33,17 +22,18 @@ function getFormatedObjects (objects: T[], object formatedObjects.push(object.toFormatedJSON()) }) - return { + const res: ResultList = { total: objectsTotal, data: formatedObjects } + + return res } // --------------------------------------------------------------------------- export { badRequest, - createEmptyCallback, generateRandomString, getFormatedObjects } diff --git a/server/initializers/checker.ts b/server/initializers/checker.ts index 7007f2c0b..fb69e05fc 100644 --- a/server/initializers/checker.ts +++ b/server/initializers/checker.ts @@ -2,6 +2,7 @@ import * as config from 'config' import { database as db } from './database' import { CONFIG } from './constants' +import { promisify0 } from '../helpers/core-utils' // Some checks on configuration files function checkConfig () { @@ -35,41 +36,36 @@ function checkMissedConfig () { } // Check the available codecs -function checkFFmpeg (callback: (err: Error) => void) { +function checkFFmpeg () { const Ffmpeg = require('fluent-ffmpeg') - - Ffmpeg.getAvailableCodecs(function (err, codecs) { - if (err) return callback(err) - if (CONFIG.TRANSCODING.ENABLED === false) return callback(null) - - const canEncode = [ 'libx264' ] - canEncode.forEach(function (codec) { - if (codecs[codec] === undefined) { - return callback(new Error('Unknown codec ' + codec + ' in FFmpeg.')) - } - - if (codecs[codec].canEncode !== true) { - return callback(new Error('Unavailable encode codec ' + codec + ' in FFmpeg')) - } + const getAvailableCodecsPromise = promisify0(Ffmpeg.getAvailableCodecs) + + getAvailableCodecsPromise() + .then(codecs => { + if (CONFIG.TRANSCODING.ENABLED === false) return undefined + + const canEncode = [ 'libx264' ] + canEncode.forEach(function (codec) { + if (codecs[codec] === undefined) { + throw new Error('Unknown codec ' + codec + ' in FFmpeg.') + } + + if (codecs[codec].canEncode !== true) { + throw new Error('Unavailable encode codec ' + codec + ' in FFmpeg') + } + }) }) - - return callback(null) - }) } -function clientsExist (callback: (err: Error, clientsExist?: boolean) => void) { - db.OAuthClient.countTotal(function (err, totalClients) { - if (err) return callback(err) - - return callback(null, totalClients !== 0) +function clientsExist () { + return db.OAuthClient.countTotal().then(totalClients => { + return totalClients !== 0 }) } -function usersExist (callback: (err: Error, usersExist?: boolean) => void) { - db.User.countTotal(function (err, totalUsers) { - if (err) return callback(err) - - return callback(null, totalUsers !== 0) +function usersExist () { + return db.User.countTotal().then(totalUsers => { + return totalUsers !== 0 }) } diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 705dec6da..6e3a8d009 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -1,12 +1,12 @@ -import * as fs from 'fs' import { join } from 'path' +import { flattenDepth } from 'lodash' import * as Sequelize from 'sequelize' -import { each } from 'async' +import * as Promise from 'bluebird' import { CONFIG } from './constants' // Do not use barrel, we need to load database first import { logger } from '../helpers/logger' -import { isTestInstance } from '../helpers/core-utils' +import { isTestInstance, readdirPromise } from '../helpers/core-utils' import { ApplicationModel, AuthorModel, @@ -33,7 +33,7 @@ const password = CONFIG.DATABASE.PASSWORD const database: { sequelize?: Sequelize.Sequelize, - init?: (silent: any, callback: any) => void, + init?: (silent: boolean) => Promise, Application?: ApplicationModel, Author?: AuthorModel, @@ -72,19 +72,17 @@ const sequelize = new Sequelize(dbname, username, password, { database.sequelize = sequelize -database.init = function (silent: boolean, callback: (err: Error) => void) { +database.init = function (silent: boolean) { const modelDirectory = join(__dirname, '..', 'models') - getModelFiles(modelDirectory, function (err, filePaths) { - if (err) throw err - - filePaths.forEach(function (filePath) { + return getModelFiles(modelDirectory).then(filePaths => { + filePaths.forEach(filePath => { const model = sequelize.import(filePath) database[model['name']] = model }) - Object.keys(database).forEach(function (modelName) { + Object.keys(database).forEach(modelName => { if ('associate' in database[modelName]) { database[modelName].associate(database) } @@ -92,7 +90,7 @@ database.init = function (silent: boolean, callback: (err: Error) => void) { if (!silent) logger.info('Database %s is ready.', dbname) - return callback(null) + return undefined }) } @@ -104,49 +102,50 @@ export { // --------------------------------------------------------------------------- -function getModelFiles (modelDirectory: string, callback: (err: Error, filePaths: string[]) => void) { - fs.readdir(modelDirectory, function (err, files) { - if (err) throw err - - const directories = files.filter(function (directory) { - // Find directories - if ( - directory.endsWith('.js.map') || - directory === 'index.js' || directory === 'index.ts' || - directory === 'utils.js' || directory === 'utils.ts' - ) return false +function getModelFiles (modelDirectory: string) { + return readdirPromise(modelDirectory) + .then(files => { + const directories: string[] = files.filter(function (directory) { + // Find directories + if ( + directory.endsWith('.js.map') || + directory === 'index.js' || directory === 'index.ts' || + directory === 'utils.js' || directory === 'utils.ts' + ) return false + + return true + }) - return true + return directories }) - - let modelFilePaths: string[] = [] - - // For each directory we read it and append model in the modelFilePaths array - each(directories, function (directory: string, eachCallback: ErrorCallback) { - const modelDirectoryPath = join(modelDirectory, directory) - - fs.readdir(modelDirectoryPath, function (err, files) { - if (err) return eachCallback(err) - - const filteredFiles = files.filter(file => { - if ( - file === 'index.js' || file === 'index.ts' || - file === 'utils.js' || file === 'utils.ts' || - file.endsWith('-interface.js') || file.endsWith('-interface.ts') || - file.endsWith('.js.map') - ) return false - - return true - }).map(file => { - return join(modelDirectoryPath, file) + .then(directories => { + const tasks = [] + + // For each directory we read it and append model in the modelFilePaths array + directories.forEach(directory => { + const modelDirectoryPath = join(modelDirectory, directory) + + const promise = readdirPromise(modelDirectoryPath).then(files => { + const filteredFiles = files.filter(file => { + if ( + file === 'index.js' || file === 'index.ts' || + file === 'utils.js' || file === 'utils.ts' || + file.endsWith('-interface.js') || file.endsWith('-interface.ts') || + file.endsWith('.js.map') + ) return false + + return true + }).map(file => join(modelDirectoryPath, file)) + + return filteredFiles }) - modelFilePaths = modelFilePaths.concat(filteredFiles) - - return eachCallback(null) + tasks.push(promise) }) - }, function (err: Error) { - return callback(err, modelFilePaths) + + return Promise.all(tasks) + }) + .then((filteredFiles: string[][]) => { + return flattenDepth(filteredFiles, 1) }) - }) } diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index f105c8292..1ec24c4ad 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts @@ -1,37 +1,19 @@ import { join } from 'path' import * as config from 'config' -import { each, series } from 'async' -import * as mkdirp from 'mkdirp' import * as passwordGenerator from 'password-generator' +import * as Promise from 'bluebird' import { database as db } from './database' import { USER_ROLES, CONFIG, LAST_MIGRATION_VERSION } from './constants' import { clientsExist, usersExist } from './checker' -import { logger, createCertsIfNotExist, root } from '../helpers' - -function installApplication (callback: (err: Error) => void) { - series([ - function createDatabase (callbackAsync) { - db.sequelize.sync().asCallback(callbackAsync) - // db.sequelize.sync({ force: true }).asCallback(callbackAsync) - }, - - function createDirectories (callbackAsync) { - createDirectoriesIfNotExist(callbackAsync) - }, - - function createCertificates (callbackAsync) { - createCertsIfNotExist(callbackAsync) - }, - - function createOAuthClient (callbackAsync) { - createOAuthClientIfNotExist(callbackAsync) - }, - - function createOAuthUser (callbackAsync) { - createOAuthAdminIfNotExist(callbackAsync) - } - ], callback) +import { logger, createCertsIfNotExist, root, mkdirpPromise } from '../helpers' + +function installApplication () { + return db.sequelize.sync() + .then(() => createDirectoriesIfNotExist()) + .then(() => createCertsIfNotExist()) + .then(() => createOAuthClientIfNotExist()) + .then(() => createOAuthAdminIfNotExist()) } // --------------------------------------------------------------------------- @@ -42,21 +24,22 @@ export { // --------------------------------------------------------------------------- -function createDirectoriesIfNotExist (callback: (err: Error) => void) { +function createDirectoriesIfNotExist () { const storages = config.get('storage') - each(Object.keys(storages), function (key, callbackEach) { + const tasks = [] + Object.keys(storages).forEach(key => { const dir = storages[key] - mkdirp(join(root(), dir), callbackEach) - }, callback) -} + tasks.push(mkdirpPromise(join(root(), dir))) + }) -function createOAuthClientIfNotExist (callback: (err: Error) => void) { - clientsExist(function (err, exist) { - if (err) return callback(err) + return Promise.all(tasks) +} +function createOAuthClientIfNotExist () { + return clientsExist().then(exist => { // Nothing to do, clients already exist - if (exist === true) return callback(null) + if (exist === true) return undefined logger.info('Creating a default OAuth Client.') @@ -69,23 +52,19 @@ function createOAuthClientIfNotExist (callback: (err: Error) => void) { redirectUris: null }) - client.save().asCallback(function (err, createdClient) { - if (err) return callback(err) - + return client.save().then(createdClient => { logger.info('Client id: ' + createdClient.clientId) logger.info('Client secret: ' + createdClient.clientSecret) - return callback(null) + return undefined }) }) } -function createOAuthAdminIfNotExist (callback: (err: Error) => void) { - usersExist(function (err, exist) { - if (err) return callback(err) - +function createOAuthAdminIfNotExist () { + return usersExist().then(exist => { // Nothing to do, users already exist - if (exist === true) return callback(null) + if (exist === true) return undefined logger.info('Creating the administrator.') @@ -116,14 +95,12 @@ function createOAuthAdminIfNotExist (callback: (err: Error) => void) { role } - db.User.create(userData, createOptions).asCallback(function (err, createdUser) { - if (err) return callback(err) - + return db.User.create(userData, createOptions).then(createdUser => { logger.info('Username: ' + username) logger.info('User password: ' + password) logger.info('Creating Application table.') - db.Application.create({ migrationVersion: LAST_MIGRATION_VERSION }).asCallback(callback) + return db.Application.create({ migrationVersion: LAST_MIGRATION_VERSION }) }) }) } diff --git a/server/initializers/migrations/0005-email-pod.ts b/server/initializers/migrations/0005-email-pod.ts index a9200c47f..ceefaad4a 100644 --- a/server/initializers/migrations/0005-email-pod.ts +++ b/server/initializers/migrations/0005-email-pod.ts @@ -1,9 +1,12 @@ -import { waterfall } from 'async' - -// utils = { transaction, queryInterface, sequelize, Sequelize } -function up (utils, finalCallback) { +import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize +}): Promise { const q = utils.queryInterface - const Sequelize = utils.Sequelize const data = { type: Sequelize.STRING(400), @@ -11,27 +14,16 @@ function up (utils, finalCallback) { defaultValue: '' } - waterfall([ - - function addEmailColumn (callback) { - q.addColumn('Pods', 'email', data, { transaction: utils.transaction }).asCallback(function (err) { - return callback(err) - }) - }, - - function updateWithFakeEmails (callback) { + return q.addColumn('Pods', 'email', data) + .then(() => { const query = 'UPDATE "Pods" SET "email" = \'dummy@example.com\'' - utils.sequelize.query(query, { transaction: utils.transaction }).asCallback(function (err) { - return callback(err) - }) - }, - - function nullOnDefault (callback) { + return utils.sequelize.query(query, { transaction: utils.transaction }) + }) + .then(() => { data.defaultValue = null - q.changeColumn('Pods', 'email', data, { transaction: utils.transaction }).asCallback(callback) - } - ], finalCallback) + return q.changeColumn('Pods', 'email', data) + }) } function down (options, callback) { diff --git a/server/initializers/migrations/0010-email-user.ts b/server/initializers/migrations/0010-email-user.ts index 4b5d29394..e8865acdb 100644 --- a/server/initializers/migrations/0010-email-user.ts +++ b/server/initializers/migrations/0010-email-user.ts @@ -1,37 +1,28 @@ -import { waterfall } from 'async' - -// utils = { transaction, queryInterface, sequelize, Sequelize } -function up (utils, finalCallback) { +import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize +}): Promise { const q = utils.queryInterface - const Sequelize = utils.Sequelize const data = { type: Sequelize.STRING(400), allowNull: false, defaultValue: '' } - - waterfall([ - - function addEmailColumn (callback) { - q.addColumn('Users', 'email', data, { transaction: utils.transaction }).asCallback(function (err) { - return callback(err) - }) - }, - - function updateWithFakeEmails (callback) { + return q.addColumn('Users', 'email', data) + .then(() => { const query = 'UPDATE "Users" SET "email" = CONCAT("username", \'@example.com\')' - utils.sequelize.query(query, { transaction: utils.transaction }).asCallback(function (err) { - return callback(err) - }) - }, - - function nullOnDefault (callback) { + return utils.sequelize.query(query, { transaction: utils.transaction }) + }) + .then(() => { data.defaultValue = null - q.changeColumn('Users', 'email', data, { transaction: utils.transaction }).asCallback(callback) - } - ], finalCallback) + return q.changeColumn('Users', 'email', data) + }) } function down (options, callback) { diff --git a/server/initializers/migrations/0015-video-views.ts b/server/initializers/migrations/0015-video-views.ts index e70869404..df274d817 100644 --- a/server/initializers/migrations/0015-video-views.ts +++ b/server/initializers/migrations/0015-video-views.ts @@ -1,7 +1,12 @@ -// utils = { transaction, queryInterface, sequelize, Sequelize } -function up (utils, finalCallback) { +import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize +}): Promise { const q = utils.queryInterface - const Sequelize = utils.Sequelize const data = { type: Sequelize.INTEGER, @@ -9,7 +14,7 @@ function up (utils, finalCallback) { defaultValue: 0 } - q.addColumn('Videos', 'views', data, { transaction: utils.transaction }).asCallback(finalCallback) + return q.addColumn('Videos', 'views', data) } function down (options, callback) { diff --git a/server/initializers/migrations/0020-video-likes.ts b/server/initializers/migrations/0020-video-likes.ts index e435d0657..3d7182d0a 100644 --- a/server/initializers/migrations/0020-video-likes.ts +++ b/server/initializers/migrations/0020-video-likes.ts @@ -1,7 +1,12 @@ -// utils = { transaction, queryInterface, sequelize, Sequelize } -function up (utils, finalCallback) { +import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize +}): Promise { const q = utils.queryInterface - const Sequelize = utils.Sequelize const data = { type: Sequelize.INTEGER, @@ -9,7 +14,7 @@ function up (utils, finalCallback) { defaultValue: 0 } - q.addColumn('Videos', 'likes', data, { transaction: utils.transaction }).asCallback(finalCallback) + return q.addColumn('Videos', 'likes', data) } function down (options, callback) { diff --git a/server/initializers/migrations/0025-video-dislikes.ts b/server/initializers/migrations/0025-video-dislikes.ts index 57e54e904..ed41095dc 100644 --- a/server/initializers/migrations/0025-video-dislikes.ts +++ b/server/initializers/migrations/0025-video-dislikes.ts @@ -1,7 +1,12 @@ -// utils = { transaction, queryInterface, sequelize, Sequelize } -function up (utils, finalCallback) { +import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize +}): Promise { const q = utils.queryInterface - const Sequelize = utils.Sequelize const data = { type: Sequelize.INTEGER, @@ -9,7 +14,7 @@ function up (utils, finalCallback) { defaultValue: 0 } - q.addColumn('Videos', 'dislikes', data, { transaction: utils.transaction }).asCallback(finalCallback) + return q.addColumn('Videos', 'dislikes', data) } function down (options, callback) { diff --git a/server/initializers/migrations/0030-video-category.ts b/server/initializers/migrations/0030-video-category.ts index 1073f449c..f5adee8f9 100644 --- a/server/initializers/migrations/0030-video-category.ts +++ b/server/initializers/migrations/0030-video-category.ts @@ -1,9 +1,12 @@ -import { waterfall } from 'async' - -// utils = { transaction, queryInterface, sequelize, Sequelize } -function up (utils, finalCallback) { +import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize +}): Promise { const q = utils.queryInterface - const Sequelize = utils.Sequelize const data = { type: Sequelize.INTEGER, @@ -11,20 +14,12 @@ function up (utils, finalCallback) { defaultValue: 0 } - waterfall([ - - function addCategoryColumn (callback) { - q.addColumn('Videos', 'category', data, { transaction: utils.transaction }).asCallback(function (err) { - return callback(err) - }) - }, - - function nullOnDefault (callback) { + return q.addColumn('Videos', 'category', data) + .then(() => { data.defaultValue = null - q.changeColumn('Videos', 'category', data, { transaction: utils.transaction }).asCallback(callback) - } - ], finalCallback) + return q.changeColumn('Videos', 'category', data) + }) } function down (options, callback) { diff --git a/server/initializers/migrations/0035-video-licence.ts b/server/initializers/migrations/0035-video-licence.ts index 9316b3c37..00c64d8e7 100644 --- a/server/initializers/migrations/0035-video-licence.ts +++ b/server/initializers/migrations/0035-video-licence.ts @@ -1,9 +1,12 @@ -import { waterfall } from 'async' - -// utils = { transaction, queryInterface, sequelize, Sequelize } -function up (utils, finalCallback) { +import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize +}): Promise { const q = utils.queryInterface - const Sequelize = utils.Sequelize const data = { type: Sequelize.INTEGER, @@ -11,20 +14,11 @@ function up (utils, finalCallback) { defaultValue: 0 } - waterfall([ - - function addLicenceColumn (callback) { - q.addColumn('Videos', 'licence', data, { transaction: utils.transaction }).asCallback(function (err) { - return callback(err) - }) - }, - - function nullOnDefault (callback) { + return q.addColumn('Videos', 'licence', data) + .then(() => { data.defaultValue = null - - q.changeColumn('Videos', 'licence', data, { transaction: utils.transaction }).asCallback(callback) - } - ], finalCallback) + return q.changeColumn('Videos', 'licence', data) + }) } function down (options, callback) { diff --git a/server/initializers/migrations/0040-video-nsfw.ts b/server/initializers/migrations/0040-video-nsfw.ts index c61f496f1..046876b61 100644 --- a/server/initializers/migrations/0040-video-nsfw.ts +++ b/server/initializers/migrations/0040-video-nsfw.ts @@ -1,9 +1,12 @@ -import { waterfall } from 'async' - -// utils = { transaction, queryInterface, sequelize, Sequelize } -function up (utils, finalCallback) { +import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize +}): Promise { const q = utils.queryInterface - const Sequelize = utils.Sequelize const data = { type: Sequelize.BOOLEAN, @@ -11,20 +14,12 @@ function up (utils, finalCallback) { defaultValue: false } - waterfall([ - - function addNSFWColumn (callback) { - q.addColumn('Videos', 'nsfw', data, { transaction: utils.transaction }).asCallback(function (err) { - return callback(err) - }) - }, - - function nullOnDefault (callback) { + return q.addColumn('Videos', 'nsfw', data) + .then(() => { data.defaultValue = null - q.changeColumn('Videos', 'nsfw', data, { transaction: utils.transaction }).asCallback(callback) - } - ], finalCallback) + return q.changeColumn('Videos', 'nsfw', data) + }) } function down (options, callback) { diff --git a/server/initializers/migrations/0045-user-display-nsfw.ts b/server/initializers/migrations/0045-user-display-nsfw.ts index 1ca317795..75bd3bbea 100644 --- a/server/initializers/migrations/0045-user-display-nsfw.ts +++ b/server/initializers/migrations/0045-user-display-nsfw.ts @@ -1,7 +1,12 @@ -// utils = { transaction, queryInterface, sequelize, Sequelize } -function up (utils, finalCallback) { +import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize +}): Promise { const q = utils.queryInterface - const Sequelize = utils.Sequelize const data = { type: Sequelize.BOOLEAN, @@ -9,7 +14,7 @@ function up (utils, finalCallback) { defaultValue: false } - q.addColumn('Users', 'displayNSFW', data, { transaction: utils.transaction }).asCallback(finalCallback) + return q.addColumn('Users', 'displayNSFW', data) } function down (options, callback) { diff --git a/server/initializers/migrations/0050-video-language.ts b/server/initializers/migrations/0050-video-language.ts index 95d0a473a..ed08f5866 100644 --- a/server/initializers/migrations/0050-video-language.ts +++ b/server/initializers/migrations/0050-video-language.ts @@ -1,7 +1,12 @@ -// utils = { transaction, queryInterface, sequelize, Sequelize } -function up (utils, finalCallback) { +import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize +}): Promise { const q = utils.queryInterface - const Sequelize = utils.Sequelize const data = { type: Sequelize.INTEGER, @@ -9,7 +14,7 @@ function up (utils, finalCallback) { defaultValue: null } - q.addColumn('Videos', 'language', data, { transaction: utils.transaction }).asCallback(finalCallback) + return q.addColumn('Videos', 'language', data) } function down (options, callback) { diff --git a/server/initializers/migrator.ts b/server/initializers/migrator.ts index d8faaebc6..d381551b5 100644 --- a/server/initializers/migrator.ts +++ b/server/initializers/migrator.ts @@ -1,70 +1,54 @@ -import { waterfall, eachSeries } from 'async' -import * as fs from 'fs' import * as path from 'path' -import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' import { database as db } from './database' import { LAST_MIGRATION_VERSION } from './constants' -import { logger } from '../helpers' - -function migrate (finalCallback: (err: Error) => void) { - waterfall([ - - function checkApplicationTableExists (callback) { - db.sequelize.getQueryInterface().showAllTables().asCallback(function (err, tables) { - if (err) return callback(err) - - // No tables, we don't need to migrate anything - // The installer will do that - if (tables.length === 0) return finalCallback(null) - - return callback(null) - }) - }, - - function loadMigrationVersion (callback) { - db.Application.loadMigrationVersion(callback) - }, - - function createMigrationRowIfNotExists (actualVersion, callback) { +import { logger, readdirPromise } from '../helpers' + +function migrate () { + const p = db.sequelize.getQueryInterface().showAllTables() + .then(tables => { + // No tables, we don't need to migrate anything + // The installer will do that + if (tables.length === 0) throw null + }) + .then(() => { + return db.Application.loadMigrationVersion() + }) + .then(actualVersion => { if (actualVersion === null) { - db.Application.create({ - migrationVersion: 0 - }, function (err) { - return callback(err, 0) - }) + return db.Application.create({ migrationVersion: 0 }).then(() => 0) } - return callback(null, actualVersion) - }, - - function abortMigrationIfNotNeeded (actualVersion, callback) { - // No need migrations - if (actualVersion >= LAST_MIGRATION_VERSION) return finalCallback(null) - - return callback(null, actualVersion) - }, + return actualVersion + }) + .then(actualVersion => { + // No need migrations, abort + if (actualVersion >= LAST_MIGRATION_VERSION) throw null - function getMigrations (actualVersion, callback) { + return actualVersion + }) + .then(actualVersion => { // If there are a new migration scripts logger.info('Begin migrations.') - getMigrationScripts(function (err, migrationScripts) { - return callback(err, actualVersion, migrationScripts) + return getMigrationScripts().then(migrationScripts => ({ actualVersion, migrationScripts })) + }) + .then(({ actualVersion, migrationScripts }) => { + return Promise.mapSeries(migrationScripts, entity => { + return executeMigration(actualVersion, entity) }) - }, + }) + .then(() => { + logger.info('Migrations finished. New migration version schema: %s', LAST_MIGRATION_VERSION) + }) + .catch(err => { + if (err === null) return undefined - function doMigrations (actualVersion, migrationScripts, callback) { - eachSeries(migrationScripts, function (entity: any, callbackEach) { - executeMigration(actualVersion, entity, callbackEach) - }, function (err) { - if (err) return callback(err) + throw err + }) - logger.info('Migrations finished. New migration version schema: %s', LAST_MIGRATION_VERSION) - return callback(null) - }) - } - ], finalCallback) + return p } // --------------------------------------------------------------------------- @@ -75,12 +59,12 @@ export { // --------------------------------------------------------------------------- -type GetMigrationScriptsCallback = (err: Error, filesToMigrate?: { version: string, script: string }[]) => void -function getMigrationScripts (callback: GetMigrationScriptsCallback) { - fs.readdir(path.join(__dirname, 'migrations'), function (err, files) { - if (err) return callback(err) - - const filesToMigrate = [] +function getMigrationScripts () { + return readdirPromise(path.join(__dirname, 'migrations')).then(files => { + const filesToMigrate: { + version: string, + script: string + }[] = [] files.forEach(function (file) { // Filename is something like 'version-blabla.js' @@ -91,15 +75,15 @@ function getMigrationScripts (callback: GetMigrationScriptsCallback) { }) }) - return callback(err, filesToMigrate) + return filesToMigrate }) } -function executeMigration (actualVersion: number, entity: { version: string, script: string }, callback: (err: Error) => void) { +function executeMigration (actualVersion: number, entity: { version: string, script: string }) { const versionScript = parseInt(entity.version, 10) // Do not execute old migration scripts - if (versionScript <= actualVersion) return callback(null) + if (versionScript <= actualVersion) return undefined // Load the migration module and run it const migrationScriptName = entity.script @@ -107,30 +91,17 @@ function executeMigration (actualVersion: number, entity: { version: string, scr const migrationScript = require(path.join(__dirname, 'migrations', migrationScriptName)) - db.sequelize.transaction().asCallback(function (err, t) { - if (err) return callback(err) - + return db.sequelize.transaction(t => { const options = { transaction: t, queryInterface: db.sequelize.getQueryInterface(), - sequelize: db.sequelize, - Sequelize: Sequelize + sequelize: db.sequelize } - migrationScript.up(options, function (err) { - if (err) { - t.rollback() - return callback(err) - } - - // Update the new migration version - db.Application.updateMigrationVersion(versionScript, t, function (err) { - if (err) { - t.rollback() - return callback(err) - } - t.commit().asCallback(callback) + migrationScript.up(options) + .then(() => { + // Update the new migration version + db.Application.updateMigrationVersion(versionScript, t) }) - }) }) } diff --git a/server/lib/friends.ts b/server/lib/friends.ts index 522cb82b3..498144318 100644 --- a/server/lib/friends.ts +++ b/server/lib/friends.ts @@ -1,6 +1,6 @@ -import { each, eachLimit, eachSeries, series, waterfall } from 'async' import * as request from 'request' import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' import { database as db } from '../initializers/database' import { @@ -15,8 +15,7 @@ import { logger, getMyPublicCert, makeSecureRequest, - makeRetryRequest, - createEmptyCallback + makeRetryRequest } from '../helpers' import { RequestScheduler, @@ -53,24 +52,24 @@ function activateSchedulers () { requestVideoEventScheduler.activate() } -function addVideoToFriends (videoData: Object, transaction: Sequelize.Transaction, callback: (err: Error) => void) { +function addVideoToFriends (videoData: Object, transaction: Sequelize.Transaction) { const options = { type: ENDPOINT_ACTIONS.ADD, endpoint: REQUEST_ENDPOINTS.VIDEOS, data: videoData, transaction } - createRequest(options, callback) + return createRequest(options) } -function updateVideoToFriends (videoData: Object, transaction: Sequelize.Transaction, callback: (err: Error) => void) { +function updateVideoToFriends (videoData: Object, transaction: Sequelize.Transaction) { const options = { type: ENDPOINT_ACTIONS.UPDATE, endpoint: REQUEST_ENDPOINTS.VIDEOS, data: videoData, transaction } - createRequest(options, callback) + return createRequest(options) } function removeVideoToFriends (videoParams: Object) { @@ -80,121 +79,93 @@ function removeVideoToFriends (videoParams: Object) { data: videoParams, transaction: null } - createRequest(options) + return createRequest(options) } -function reportAbuseVideoToFriend (reportData: Object, video: VideoInstance) { +function reportAbuseVideoToFriend (reportData: Object, video: VideoInstance, transaction: Sequelize.Transaction) { const options = { type: ENDPOINT_ACTIONS.REPORT_ABUSE, endpoint: REQUEST_ENDPOINTS.VIDEOS, data: reportData, toIds: [ video.Author.podId ], - transaction: null + transaction } - createRequest(options) + return createRequest(options) } -function quickAndDirtyUpdateVideoToFriends (qaduParam: QaduParam, transaction?: Sequelize.Transaction, callback?: (err: Error) => void) { +function quickAndDirtyUpdateVideoToFriends (qaduParam: QaduParam, transaction?: Sequelize.Transaction) { const options = { videoId: qaduParam.videoId, type: qaduParam.type, transaction } - return createVideoQaduRequest(options, callback) + return createVideoQaduRequest(options) } -function quickAndDirtyUpdatesVideoToFriends ( - qadusParams: QaduParam[], - transaction: Sequelize.Transaction, - finalCallback: (err: Error) => void -) { +function quickAndDirtyUpdatesVideoToFriends (qadusParams: QaduParam[], transaction: Sequelize.Transaction) { const tasks = [] qadusParams.forEach(function (qaduParams) { - const fun = function (callback) { - quickAndDirtyUpdateVideoToFriends(qaduParams, transaction, callback) - } - - tasks.push(fun) + tasks.push(quickAndDirtyUpdateVideoToFriends(qaduParams, transaction)) }) - series(tasks, finalCallback) + return Promise.all(tasks) } -function addEventToRemoteVideo (eventParam: EventParam, transaction?: Sequelize.Transaction, callback?: (err: Error) => void) { +function addEventToRemoteVideo (eventParam: EventParam, transaction?: Sequelize.Transaction) { const options = { videoId: eventParam.videoId, type: eventParam.type, transaction } - createVideoEventRequest(options, callback) + return createVideoEventRequest(options) } -function addEventsToRemoteVideo (eventsParams: EventParam[], transaction: Sequelize.Transaction, finalCallback: (err: Error) => void) { +function addEventsToRemoteVideo (eventsParams: EventParam[], transaction: Sequelize.Transaction) { const tasks = [] eventsParams.forEach(function (eventParams) { - const fun = function (callback) { - addEventToRemoteVideo(eventParams, transaction, callback) - } - - tasks.push(fun) + tasks.push(addEventToRemoteVideo(eventParams, transaction)) }) - series(tasks, finalCallback) + return Promise.all(tasks) } -function hasFriends (callback: (err: Error, hasFriends?: boolean) => void) { - db.Pod.countAll(function (err, count) { - if (err) return callback(err) - - const hasFriends = (count !== 0) - callback(null, hasFriends) - }) +function hasFriends () { + return db.Pod.countAll().then(count => count !== 0) } -function makeFriends (hosts: string[], callback: (err: Error) => void) { +function makeFriends (hosts: string[]) { const podsScore = {} logger.info('Make friends!') - getMyPublicCert(function (err, cert) { - if (err) { - logger.error('Cannot read public cert.') - return callback(err) - } - - eachSeries(hosts, function (host, callbackEach) { - computeForeignPodsList(host, podsScore, callbackEach) - }, function (err: Error) { - if (err) return callback(err) - + return getMyPublicCert() + .then(cert => { + return Promise.mapSeries(hosts, host => { + return computeForeignPodsList(host, podsScore) + }).then(() => cert) + }) + .then(cert => { logger.debug('Pods scores computed.', { podsScore: podsScore }) const podsList = computeWinningPods(hosts, podsScore) logger.debug('Pods that we keep.', { podsToKeep: podsList }) - makeRequestsToWinningPods(cert, podsList, callback) + return makeRequestsToWinningPods(cert, podsList) }) - }) } -function quitFriends (callback: (err: Error) => void) { +function quitFriends () { // Stop pool requests requestScheduler.deactivate() - waterfall([ - function flushRequests (callbackAsync) { - requestScheduler.flush(err => callbackAsync(err)) - }, - - function flushVideoQaduRequests (callbackAsync) { - requestVideoQaduScheduler.flush(err => callbackAsync(err)) - }, - - function getPodsList (callbackAsync) { - return db.Pod.list(callbackAsync) - }, - - function announceIQuitMyFriends (pods, callbackAsync) { + return requestScheduler.flush() + .then(() => { + return requestVideoQaduScheduler.flush() + }) + .then(() => { + return db.Pod.list() + }) + .then(pods => { const requestParams = { method: 'POST' as 'POST', path: '/api/' + API_VERSION + '/remote/pods/remove', @@ -205,61 +176,57 @@ function quitFriends (callback: (err: Error) => void) { // Announce we quit them // We don't care if the request fails // The other pod will exclude us automatically after a while - eachLimit(pods, REQUESTS_IN_PARALLEL, function (pod, callbackEach) { + return Promise.map(pods, pod => { requestParams.toPod = pod - makeSecureRequest(requestParams, callbackEach) - }, function (err) { - if (err) { - logger.error('Some errors while quitting friends.', { err: err }) - // Don't stop the process - } - - return callbackAsync(null, pods) + return makeSecureRequest(requestParams) + }, { concurrency: REQUESTS_IN_PARALLEL }) + .then(() => pods) + .catch(err => { + logger.error('Some errors while quitting friends.', { err: err }) + // Don't stop the process }) - }, - - function removePodsFromDB (pods, callbackAsync) { - each(pods, function (pod: any, callbackEach) { - pod.destroy().asCallback(callbackEach) - }, callbackAsync) - } - ], function (err: Error) { - // Don't forget to re activate the scheduler, even if there was an error - requestScheduler.activate() - - if (err) return callback(err) + }) + .then(pods => { + const tasks = [] + pods.forEach(pod => tasks.push(pod.destroy())) - logger.info('Removed all remote videos.') - return callback(null) - }) + return Promise.all(pods) + }) + .then(() => { + logger.info('Removed all remote videos.') + // Don't forget to re activate the scheduler, even if there was an error + return requestScheduler.activate() + }) + .finally(() => requestScheduler.activate()) } function sendOwnedVideosToPod (podId: number) { - db.Video.listOwnedAndPopulateAuthorAndTags(function (err, videosList) { - if (err) { - logger.error('Cannot get the list of videos we own.') - return - } - - videosList.forEach(function (video) { - video.toAddRemoteJSON(function (err, remoteVideo) { - if (err) { - logger.error('Cannot convert video to remote.', { error: err }) - // Don't break the process - return - } - - const options = { - type: 'add', - endpoint: REQUEST_ENDPOINTS.VIDEOS, - data: remoteVideo, - toIds: [ podId ], - transaction: null - } - createRequest(options) + db.Video.listOwnedAndPopulateAuthorAndTags() + .then(videosList => { + const tasks = [] + videosList.forEach(video => { + const promise = video.toAddRemoteJSON() + .then(remoteVideo => { + const options = { + type: 'add', + endpoint: REQUEST_ENDPOINTS.VIDEOS, + data: remoteVideo, + toIds: [ podId ], + transaction: null + } + return createRequest(options) + }) + .catch(err => { + logger.error('Cannot convert video to remote.', { error: err }) + // Don't break the process + return undefined + }) + + tasks.push(promise) }) + + return Promise.all(tasks) }) - }) } function getRequestScheduler () { @@ -297,23 +264,22 @@ export { // --------------------------------------------------------------------------- -function computeForeignPodsList (host: string, podsScore: { [ host: string ]: number }, callback: (err: Error) => void) { - getForeignPodsList(host, function (err, res) { - if (err) return callback(err) - +function computeForeignPodsList (host: string, podsScore: { [ host: string ]: number }) { + // TODO: type res + return getForeignPodsList(host).then((res: any) => { const foreignPodsList = res.data // Let's give 1 point to the pod we ask the friends list foreignPodsList.push({ host }) - foreignPodsList.forEach(function (foreignPod) { + foreignPodsList.forEach(foreignPod => { const foreignPodHost = foreignPod.host if (podsScore[foreignPodHost]) podsScore[foreignPodHost]++ else podsScore[foreignPodHost] = 1 }) - return callback(null) + return undefined }) } @@ -323,7 +289,7 @@ function computeWinningPods (hosts: string[], podsScore: { [ host: string ]: num const podsList = [] const baseScore = hosts.length / 2 - Object.keys(podsScore).forEach(function (podHost) { + Object.keys(podsScore).forEach(podHost => { // If the pod is not me and with a good score we add it if (isMe(podHost) === false && podsScore[podHost] > baseScore) { podsList.push({ host: podHost }) @@ -333,28 +299,30 @@ function computeWinningPods (hosts: string[], podsScore: { [ host: string ]: num return podsList } -function getForeignPodsList (host: string, callback: (err: Error, foreignPodsList?: any) => void) { - const path = '/api/' + API_VERSION + '/pods' +function getForeignPodsList (host: string) { + return new Promise((res, rej) => { + const path = '/api/' + API_VERSION + '/pods' - request.get(REMOTE_SCHEME.HTTP + '://' + host + path, function (err, response, body) { - if (err) return callback(err) + request.get(REMOTE_SCHEME.HTTP + '://' + host + path, function (err, response, body) { + if (err) return rej(err) - try { - const json = JSON.parse(body) - return callback(null, json) - } catch (err) { - return callback(err) - } + try { + const json = JSON.parse(body) + return res(json) + } catch (err) { + return rej(err) + } + }) }) } -function makeRequestsToWinningPods (cert: string, podsList: PodInstance[], callback: (err: Error) => void) { +function makeRequestsToWinningPods (cert: string, podsList: PodInstance[]) { // Stop pool requests requestScheduler.deactivate() // Flush pool requests requestScheduler.forceSend() - eachLimit(podsList, REQUESTS_IN_PARALLEL, function (pod: PodInstance, callbackEach) { + return Promise.map(podsList, pod => { const params = { url: REMOTE_SCHEME.HTTP + '://' + pod.host + '/api/' + API_VERSION + '/pods/', method: 'POST' as 'POST', @@ -365,38 +333,35 @@ function makeRequestsToWinningPods (cert: string, podsList: PodInstance[], callb } } - makeRetryRequest(params, function (err, res, body: { cert: string, email: string }) { - if (err) { - logger.error('Error with adding %s pod.', pod.host, { error: err }) + return makeRetryRequest(params) + .then(({ response, body }) => { + body = body as { cert: string, email: string } + + if (response.statusCode === 200) { + const podObj = db.Pod.build({ host: pod.host, publicKey: body.cert, email: body.email }) + return podObj.save() + .then(podCreated => { + + // Add our videos to the request scheduler + sendOwnedVideosToPod(podCreated.id) + }) + .catch(err => { + logger.error('Cannot add friend %s pod.', pod.host, { error: err }) + }) + } else { + logger.error('Status not 200 for %s pod.', pod.host) + } + }) + .catch(err => { + logger.error('Error with adding %s pod.', pod.host, { error: err.stack }) // Don't break the process - return callbackEach() - } - - if (res.statusCode === 200) { - const podObj = db.Pod.build({ host: pod.host, publicKey: body.cert, email: body.email }) - podObj.save().asCallback(function (err, podCreated) { - if (err) { - logger.error('Cannot add friend %s pod.', pod.host, { error: err }) - return callbackEach() - } - - // Add our videos to the request scheduler - sendOwnedVideosToPod(podCreated.id) - - return callbackEach() - }) - } else { - logger.error('Status not 200 for %s pod.', pod.host) - return callbackEach() - } - }) - }, function endRequests () { + }) + }, { concurrency: REQUESTS_IN_PARALLEL }) + .then(() => logger.debug('makeRequestsToWinningPods finished.')) + .finally(() => { // Final callback, we've ended all the requests // Now we made new friends, we can re activate the pool of requests requestScheduler.activate() - - logger.debug('makeRequestsToWinningPods finished.') - return callback(null) }) } @@ -408,33 +373,22 @@ type CreateRequestOptions = { toIds?: number[] transaction: Sequelize.Transaction } -function createRequest (options: CreateRequestOptions, callback?: (err: Error) => void) { - if (!callback) callback = function () { /* empty */ } - - if (options.toIds !== undefined) return requestScheduler.createRequest(options as RequestSchedulerOptions, callback) +function createRequest (options: CreateRequestOptions) { + if (options.toIds !== undefined) return requestScheduler.createRequest(options as RequestSchedulerOptions) // If the "toIds" pods is not specified, we send the request to all our friends - db.Pod.listAllIds(options.transaction, function (err, podIds) { - if (err) { - logger.error('Cannot get pod ids', { error: err }) - return - } - + return db.Pod.listAllIds(options.transaction).then(podIds => { const newOptions = Object.assign(options, { toIds: podIds }) - return requestScheduler.createRequest(newOptions, callback) + return requestScheduler.createRequest(newOptions) }) } -function createVideoQaduRequest (options: RequestVideoQaduSchedulerOptions, callback: (err: Error) => void) { - if (!callback) callback = createEmptyCallback() - - requestVideoQaduScheduler.createRequest(options, callback) +function createVideoQaduRequest (options: RequestVideoQaduSchedulerOptions) { + return requestVideoQaduScheduler.createRequest(options) } -function createVideoEventRequest (options: RequestVideoEventSchedulerOptions, callback: (err: Error) => void) { - if (!callback) callback = createEmptyCallback() - - requestVideoEventScheduler.createRequest(options, callback) +function createVideoEventRequest (options: RequestVideoEventSchedulerOptions) { + return requestVideoEventScheduler.createRequest(options) } function isMe (host: string) { diff --git a/server/lib/jobs/handlers/index.ts b/server/lib/jobs/handlers/index.ts index 7d0263b15..8abddae35 100644 --- a/server/lib/jobs/handlers/index.ts +++ b/server/lib/jobs/handlers/index.ts @@ -1,11 +1,9 @@ import * as videoTranscoder from './video-transcoder' -import { VideoInstance } from '../../../models' - export interface JobHandler { - process (data: object, callback: (err: Error, videoInstance?: T) => void) - onError (err: Error, jobId: number, video: T, callback: (err: Error) => void) - onSuccess (data: any, jobId: number, video: T, callback: (err: Error) => void) + process (data: object): T + onError (err: Error, jobId: number) + onSuccess (jobId: number, jobResult: T) } const jobHandlers: { [ handlerName: string ]: JobHandler } = { diff --git a/server/lib/jobs/handlers/video-transcoder.ts b/server/lib/jobs/handlers/video-transcoder.ts index 6f606a7d3..e829ca813 100644 --- a/server/lib/jobs/handlers/video-transcoder.ts +++ b/server/lib/jobs/handlers/video-transcoder.ts @@ -3,29 +3,23 @@ import { logger } from '../../../helpers' import { addVideoToFriends } from '../../../lib' import { VideoInstance } from '../../../models' -function process (data: { id: string }, callback: (err: Error, videoInstance?: VideoInstance) => void) { - db.Video.loadAndPopulateAuthorAndPodAndTags(data.id, function (err, video) { - if (err) return callback(err) - - video.transcodeVideofile(function (err) { - return callback(err, video) - }) +function process (data: { id: string }) { + return db.Video.loadAndPopulateAuthorAndPodAndTags(data.id).then(video => { + return video.transcodeVideofile().then(() => video) }) } -function onError (err: Error, jobId: number, video: VideoInstance, callback: (err: Error) => void) { +function onError (err: Error, jobId: number) { logger.error('Error when transcoding video file in job %d.', jobId, { error: err }) - return callback(null) + return Promise.resolve() } -function onSuccess (data: any, jobId: number, video: VideoInstance, callback: (err: Error) => void) { +function onSuccess (jobId: number, video: VideoInstance) { logger.info('Job %d is a success.', jobId) - video.toAddRemoteJSON(function (err, remoteVideo) { - if (err) return callback(err) - + video.toAddRemoteJSON().then(remoteVideo => { // Now we'll add the video's meta data to our friends - addVideoToFriends(remoteVideo, null, callback) + return addVideoToFriends(remoteVideo, null) }) } diff --git a/server/lib/jobs/job-scheduler.ts b/server/lib/jobs/job-scheduler.ts index 2f01387e7..248dc7978 100644 --- a/server/lib/jobs/job-scheduler.ts +++ b/server/lib/jobs/job-scheduler.ts @@ -32,37 +32,35 @@ class JobScheduler { // Finish processing jobs from a previous start const state = JOB_STATES.PROCESSING - db.Job.listWithLimit(limit, state, (err, jobs) => { - this.enqueueJobs(err, jobsQueue, jobs) - - forever( - next => { - if (jobsQueue.length() !== 0) { - // Finish processing the queue first - return setTimeout(next, JOBS_FETCHING_INTERVAL) - } - - const state = JOB_STATES.PENDING - db.Job.listWithLimit(limit, state, (err, jobs) => { - if (err) { - logger.error('Cannot list pending jobs.', { error: err }) - } else { - jobs.forEach(job => { - jobsQueue.push(job) - }) + db.Job.listWithLimit(limit, state) + .then(jobs => { + this.enqueueJobs(jobsQueue, jobs) + + forever( + next => { + if (jobsQueue.length() !== 0) { + // Finish processing the queue first + return setTimeout(next, JOBS_FETCHING_INTERVAL) } - // Optimization: we could use "drain" from queue object - return setTimeout(next, JOBS_FETCHING_INTERVAL) - }) - }, + const state = JOB_STATES.PENDING + db.Job.listWithLimit(limit, state) + .then(jobs => { + this.enqueueJobs(jobsQueue, jobs) - err => { logger.error('Error in job scheduler queue.', { error: err }) } - ) - }) + // Optimization: we could use "drain" from queue object + return setTimeout(next, JOBS_FETCHING_INTERVAL) + }) + .catch(err => logger.error('Cannot list pending jobs.', { error: err })) + }, + + err => logger.error('Error in job scheduler queue.', { error: err }) + ) + }) + .catch(err => logger.error('Cannot list pending jobs.', { error: err })) } - createJob (transaction: Sequelize.Transaction, handlerName: string, handlerInputData: object, callback: (err: Error) => void) { + createJob (transaction: Sequelize.Transaction, handlerName: string, handlerInputData: object) { const createQuery = { state: JOB_STATES.PENDING, handlerName, @@ -70,67 +68,62 @@ class JobScheduler { } const options = { transaction } - db.Job.create(createQuery, options).asCallback(callback) + return db.Job.create(createQuery, options) } - private enqueueJobs (err: Error, jobsQueue: AsyncQueue, jobs: JobInstance[]) { - if (err) { - logger.error('Cannot list pending jobs.', { error: err }) - } else { - jobs.forEach(job => { - jobsQueue.push(job) - }) - } + private enqueueJobs (jobsQueue: AsyncQueue, jobs: JobInstance[]) { + jobs.forEach(job => jobsQueue.push(job)) } private processJob (job: JobInstance, callback: (err: Error) => void) { const jobHandler = jobHandlers[job.handlerName] + if (jobHandler === undefined) { + logger.error('Unknown job handler for job %s.', job.handlerName) + return callback(null) + } logger.info('Processing job %d with handler %s.', job.id, job.handlerName) job.state = JOB_STATES.PROCESSING - job.save().asCallback(err => { - if (err) return this.cannotSaveJobError(err, callback) - - if (jobHandler === undefined) { - logger.error('Unknown job handler for job %s.', job.handlerName) - return callback(null) - } + return job.save() + .then(() => { + return jobHandler.process(job.handlerInputData) + }) + .then( + result => { + return this.onJobSuccess(jobHandler, job, result) + }, - return jobHandler.process(job.handlerInputData, (err, result) => { - if (err) { + err => { logger.error('Error in job handler %s.', job.handlerName, { error: err }) - return this.onJobError(jobHandler, job, result, callback) + return this.onJobError(jobHandler, job, err) } - - return this.onJobSuccess(jobHandler, job, result, callback) + ) + .then(() => callback(null)) + .catch(err => { + this.cannotSaveJobError(err) + return callback(err) }) - }) } - private onJobError (jobHandler: JobHandler, job: JobInstance, jobResult: any, callback: (err: Error) => void) { + private onJobError (jobHandler: JobHandler, job: JobInstance, err: Error) { job.state = JOB_STATES.ERROR - job.save().asCallback(err => { - if (err) return this.cannotSaveJobError(err, callback) - - return jobHandler.onError(err, job.id, jobResult, callback) - }) + return job.save() + .then(() => jobHandler.onError(err, job.id)) + .catch(err => this.cannotSaveJobError(err)) } - private onJobSuccess (jobHandler: JobHandler, job: JobInstance, jobResult: any, callback: (err: Error) => void) { + private onJobSuccess (jobHandler: JobHandler, job: JobInstance, jobResult: any) { job.state = JOB_STATES.SUCCESS - job.save().asCallback(err => { - if (err) return this.cannotSaveJobError(err, callback) - - return jobHandler.onSuccess(err, job.id, jobResult, callback) - }) + return job.save() + .then(() => jobHandler.onSuccess(job.id, jobResult)) + .catch(err => this.cannotSaveJobError(err)) } - private cannotSaveJobError (err: Error, callback: (err: Error) => void) { + private cannotSaveJobError (err: Error) { logger.error('Cannot save new job state.', { error: err }) - return callback(err) } } diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts index 7cf42e94c..f34c9c667 100644 --- a/server/lib/oauth-model.ts +++ b/server/lib/oauth-model.ts @@ -30,17 +30,10 @@ function getUser (username: string, password: string) { return db.User.getByUsername(username).then(function (user) { if (!user) return null - // We need to return a promise - return new Promise(function (resolve, reject) { - return user.isPasswordMatch(password, function (err, isPasswordMatch) { - if (err) return reject(err) + return user.isPasswordMatch(password).then(passwordMatch => { + if (passwordMatch === false) return null - if (isPasswordMatch === true) { - return resolve(user) - } - - return resolve(null) - }) + return user }) }) } @@ -80,8 +73,6 @@ function saveToken (token: TokenInfo, client: OAuthClientInstance, user: UserIns tokenCreated.user = user return tokenCreated - }).catch(function (err) { - throw err }) } diff --git a/server/lib/request/abstract-request-scheduler.ts b/server/lib/request/abstract-request-scheduler.ts index e81ab9c36..dd77fddb7 100644 --- a/server/lib/request/abstract-request-scheduler.ts +++ b/server/lib/request/abstract-request-scheduler.ts @@ -1,15 +1,16 @@ -import * as eachLimit from 'async/eachLimit' +import { isEmpty } from 'lodash' +import * as Promise from 'bluebird' import { database as db } from '../../initializers/database' import { logger, makeSecureRequest } from '../../helpers' -import { PodInstance } from '../../models' +import { AbstractRequestClass, AbstractRequestToPodClass, PodInstance } from '../../models' import { API_VERSION, REQUESTS_IN_PARALLEL, REQUESTS_INTERVAL } from '../../initializers' -abstract class AbstractRequestScheduler { +abstract class AbstractRequestScheduler { requestInterval: number limitPods: number limitPerPod: number @@ -24,9 +25,9 @@ abstract class AbstractRequestScheduler { this.requestInterval = REQUESTS_INTERVAL } - abstract getRequestModel () - abstract getRequestToPodModel () - abstract buildRequestObjects (requests: any) + abstract getRequestModel (): AbstractRequestClass + abstract getRequestToPodModel (): AbstractRequestToPodClass + abstract buildRequestObjects (requestsGrouped: T): {} activate () { logger.info('Requests scheduler activated.') @@ -55,20 +56,18 @@ abstract class AbstractRequestScheduler { return REQUESTS_INTERVAL - (Date.now() - this.lastRequestTimestamp) } - remainingRequestsCount (callback: (err: Error, total: number) => void) { - return this.getRequestModel().countTotalRequests(callback) + remainingRequestsCount () { + return this.getRequestModel().countTotalRequests() } - flush (callback: (err: Error) => void) { - this.getRequestModel().removeAll(callback) + flush () { + return this.getRequestModel().removeAll() } // --------------------------------------------------------------------------- // Make a requests to friends of a certain type - protected makeRequest (toPod: PodInstance, requestEndpoint: string, requestsToMake: Object, callback) { - if (!callback) callback = function () { /* empty */ } - + protected makeRequest (toPod: PodInstance, requestEndpoint: string, requestsToMake: Object) { const params = { toPod: toPod, sign: true, // Prove our identity @@ -79,65 +78,64 @@ abstract class AbstractRequestScheduler { // Make multiple retry requests to all of pods // The function fire some useful callbacks - makeSecureRequest(params, (err, res) => { - if (err || (res.statusCode !== 200 && res.statusCode !== 201 && res.statusCode !== 204)) { - err = err ? err.message : 'Status code not 20x : ' + res.statusCode + return makeSecureRequest(params) + .then(({ response, body }) => { + if (response.statusCode !== 200 && response.statusCode !== 201 && response.statusCode !== 204) { + throw new Error('Status code not 20x : ' + response.statusCode) + } + }) + .catch(err => { logger.error('Error sending secure request to %s pod.', toPod.host, { error: err }) - return callback(err) - } - - return callback(null) - }) + throw err + }) } // Make all the requests of the scheduler protected makeRequests () { - this.getRequestModel().listWithLimitAndRandom(this.limitPods, this.limitPerPod, (err, requests) => { - if (err) { - logger.error('Cannot get the list of "%s".', this.description, { err: err }) - return // Abort - } - - // If there are no requests, abort - if (requests.length === 0) { - logger.info('No "%s" to make.', this.description) - return - } - - // We want to group requests by destinations pod and endpoint - const requestsToMakeGrouped = this.buildRequestObjects(requests) - - logger.info('Making "%s" to friends.', this.description) - - const goodPods = [] - const badPods = [] - - eachLimit(Object.keys(requestsToMakeGrouped), REQUESTS_IN_PARALLEL, (hashKey, callbackEach) => { - const requestToMake = requestsToMakeGrouped[hashKey] - const toPod = requestToMake.toPod - - this.makeRequest(toPod, requestToMake.endpoint, requestToMake.datas, (err) => { - if (err) { - badPods.push(requestToMake.toPod.id) - return callbackEach() - } - - logger.debug('Removing requests for pod %s.', requestToMake.toPod.id, { requestsIds: requestToMake.ids }) - goodPods.push(requestToMake.toPod.id) - - // Remove the pod id of these request ids - this.getRequestToPodModel().removeByRequestIdsAndPod(requestToMake.ids, requestToMake.toPod.id, callbackEach) + return this.getRequestModel().listWithLimitAndRandom(this.limitPods, this.limitPerPod) + .then((requestsGrouped: T) => { + // We want to group requests by destinations pod and endpoint + const requestsToMake = this.buildRequestObjects(requestsGrouped) + + // If there are no requests, abort + if (isEmpty(requestsToMake) === true) { + logger.info('No "%s" to make.', this.description) + return { goodPods: [], badPods: [] } + } + + logger.info('Making "%s" to friends.', this.description) + + const goodPods = [] + const badPods = [] + + return Promise.map(Object.keys(requestsToMake), hashKey => { + const requestToMake = requestsToMake[hashKey] + const toPod: PodInstance = requestToMake.toPod + + return this.makeRequest(toPod, requestToMake.endpoint, requestToMake.datas) + .then(() => { + logger.debug('Removing requests for pod %s.', requestToMake.toPod.id, { requestsIds: requestToMake.ids }) + goodPods.push(requestToMake.toPod.id) + + this.afterRequestHook() + + // Remove the pod id of these request ids + return this.getRequestToPodModel().removeByRequestIdsAndPod(requestToMake.ids, requestToMake.toPod.id) + }) + .catch(err => { + badPods.push(requestToMake.toPod.id) + logger.info('Cannot make request to %s.', toPod.host, { error: err }) + }) + }, { concurrency: REQUESTS_IN_PARALLEL }).then(() => ({ goodPods, badPods })) + }) + .then(({ goodPods, badPods }) => { + this.afterRequestsHook() - this.afterRequestHook() - }) - }, () => { // All the requests were made, we update the pods score - db.Pod.updatePodsScore(goodPods, badPods) - - this.afterRequestsHook() + return db.Pod.updatePodsScore(goodPods, badPods) }) - }) + .catch(err => logger.error('Cannot get the list of "%s".', this.description, { error: err.stack })) } protected afterRequestHook () { diff --git a/server/lib/request/request-scheduler.ts b/server/lib/request/request-scheduler.ts index 575e0227c..0dd796fb0 100644 --- a/server/lib/request/request-scheduler.ts +++ b/server/lib/request/request-scheduler.ts @@ -3,10 +3,8 @@ import * as Sequelize from 'sequelize' import { database as db } from '../../initializers/database' import { AbstractRequestScheduler } from './abstract-request-scheduler' import { logger } from '../../helpers' -import { - REQUESTS_LIMIT_PODS, - REQUESTS_LIMIT_PER_POD -} from '../../initializers' +import { REQUESTS_LIMIT_PODS, REQUESTS_LIMIT_PER_POD } from '../../initializers' +import { RequestsGrouped } from '../../models' import { RequestEndpoint } from '../../../shared' export type RequestSchedulerOptions = { @@ -17,7 +15,7 @@ export type RequestSchedulerOptions = { transaction: Sequelize.Transaction } -class RequestScheduler extends AbstractRequestScheduler { +class RequestScheduler extends AbstractRequestScheduler { constructor () { super() @@ -36,11 +34,11 @@ class RequestScheduler extends AbstractRequestScheduler { return db.RequestToPod } - buildRequestObjects (requests: { [ toPodId: number ]: any }) { + buildRequestObjects (requestsGrouped: RequestsGrouped) { const requestsToMakeGrouped = {} - Object.keys(requests).forEach(toPodId => { - requests[toPodId].forEach(data => { + Object.keys(requestsGrouped).forEach(toPodId => { + requestsGrouped[toPodId].forEach(data => { const request = data.request const pod = data.pod const hashKey = toPodId + request.endpoint @@ -62,12 +60,12 @@ class RequestScheduler extends AbstractRequestScheduler { return requestsToMakeGrouped } - createRequest ({ type, endpoint, data, toIds, transaction }: RequestSchedulerOptions, callback: (err: Error) => void) { + createRequest ({ type, endpoint, data, toIds, transaction }: RequestSchedulerOptions) { // TODO: check the setPods works const podIds = [] // If there are no destination pods abort - if (toIds.length === 0) return callback(null) + if (toIds.length === 0) return undefined toIds.forEach(toPod => { podIds.push(toPod) @@ -85,20 +83,18 @@ class RequestScheduler extends AbstractRequestScheduler { transaction } - return db.Request.create(createQuery, dbRequestOptions).asCallback((err, request) => { - if (err) return callback(err) - - return request.setPods(podIds, dbRequestOptions).asCallback(callback) - }) + return db.Request.create(createQuery, dbRequestOptions) + .then(request => { + return request.setPods(podIds, dbRequestOptions) + }) } // --------------------------------------------------------------------------- afterRequestsHook () { // Flush requests with no pod - this.getRequestModel().removeWithEmptyTo(err => { - if (err) logger.error('Error when removing requests with no pods.', { error: err }) - }) + this.getRequestModel().removeWithEmptyTo() + .catch(err => logger.error('Error when removing requests with no pods.', { error: err })) } } diff --git a/server/lib/request/request-video-event-scheduler.ts b/server/lib/request/request-video-event-scheduler.ts index 4bb76f4c9..d4d714c02 100644 --- a/server/lib/request/request-video-event-scheduler.ts +++ b/server/lib/request/request-video-event-scheduler.ts @@ -7,6 +7,7 @@ import { REQUESTS_VIDEO_EVENT_LIMIT_PER_POD, REQUEST_VIDEO_EVENT_ENDPOINT } from '../../initializers' +import { RequestsVideoEventGrouped } from '../../models' import { RequestVideoEventType } from '../../../shared' export type RequestVideoEventSchedulerOptions = { @@ -16,7 +17,7 @@ export type RequestVideoEventSchedulerOptions = { transaction?: Sequelize.Transaction } -class RequestVideoEventScheduler extends AbstractRequestScheduler { +class RequestVideoEventScheduler extends AbstractRequestScheduler { constructor () { super() @@ -35,7 +36,7 @@ class RequestVideoEventScheduler extends AbstractRequestScheduler { return db.RequestVideoEvent } - buildRequestObjects (eventsToProcess: { [ toPodId: number ]: any }[]) { + buildRequestObjects (eventRequests: RequestsVideoEventGrouped) { const requestsToMakeGrouped = {} /* Example: @@ -50,8 +51,8 @@ class RequestVideoEventScheduler extends AbstractRequestScheduler { // We group video events per video and per pod // We add the counts of the same event types - Object.keys(eventsToProcess).forEach(toPodId => { - eventsToProcess[toPodId].forEach(eventToProcess => { + Object.keys(eventRequests).forEach(toPodId => { + eventRequests[toPodId].forEach(eventToProcess => { if (!eventsPerVideoPerPod[toPodId]) eventsPerVideoPerPod[toPodId] = {} if (!requestsToMakeGrouped[toPodId]) { @@ -97,7 +98,7 @@ class RequestVideoEventScheduler extends AbstractRequestScheduler { return requestsToMakeGrouped } - createRequest ({ type, videoId, count, transaction }: RequestVideoEventSchedulerOptions, callback: (err: Error) => void) { + createRequest ({ type, videoId, count, transaction }: RequestVideoEventSchedulerOptions) { if (count === undefined) count = 1 const dbRequestOptions: Sequelize.CreateOptions = {} @@ -109,7 +110,7 @@ class RequestVideoEventScheduler extends AbstractRequestScheduler { videoId } - return db.RequestVideoEvent.create(createQuery, dbRequestOptions).asCallback(callback) + return db.RequestVideoEvent.create(createQuery, dbRequestOptions) } } diff --git a/server/lib/request/request-video-qadu-scheduler.ts b/server/lib/request/request-video-qadu-scheduler.ts index d7169cc81..5ec7de9c2 100644 --- a/server/lib/request/request-video-qadu-scheduler.ts +++ b/server/lib/request/request-video-qadu-scheduler.ts @@ -9,6 +9,7 @@ import { REQUEST_VIDEO_QADU_ENDPOINT, REQUEST_VIDEO_QADU_TYPES } from '../../initializers' +import { RequestsVideoQaduGrouped } from '../../models' import { RequestVideoQaduType } from '../../../shared' export type RequestVideoQaduSchedulerOptions = { @@ -17,7 +18,7 @@ export type RequestVideoQaduSchedulerOptions = { transaction?: Sequelize.Transaction } -class RequestVideoQaduScheduler extends AbstractRequestScheduler { +class RequestVideoQaduScheduler extends AbstractRequestScheduler { constructor () { super() @@ -36,7 +37,7 @@ class RequestVideoQaduScheduler extends AbstractRequestScheduler { return db.RequestVideoQadu } - buildRequestObjects (requests: { [ toPodId: number ]: any }[]) { + buildRequestObjects (requests: RequestsVideoQaduGrouped) { const requestsToMakeGrouped = {} Object.keys(requests).forEach(toPodId => { @@ -105,20 +106,18 @@ class RequestVideoQaduScheduler extends AbstractRequestScheduler { return requestsToMakeGrouped } - createRequest ({ type, videoId, transaction }: RequestVideoQaduSchedulerOptions, callback: (err: Error) => void) { + createRequest ({ type, videoId, transaction }: RequestVideoQaduSchedulerOptions) { const dbRequestOptions: Sequelize.BulkCreateOptions = {} if (transaction) dbRequestOptions.transaction = transaction // Send the update to all our friends - db.Pod.listAllIds(transaction, function (err, podIds) { - if (err) return callback(err) - + return db.Pod.listAllIds(transaction).then(podIds => { const queries = [] podIds.forEach(podId => { queries.push({ type, videoId, podId }) }) - return db.RequestVideoQadu.bulkCreate(queries, dbRequestOptions).asCallback(callback) + return db.RequestVideoQadu.bulkCreate(queries, dbRequestOptions) }) } } diff --git a/server/middlewares/secure.ts b/server/middlewares/secure.ts index fbfd08c7b..0fa9ee9d2 100644 --- a/server/middlewares/secure.ts +++ b/server/middlewares/secure.ts @@ -9,41 +9,41 @@ import { function checkSignature (req: express.Request, res: express.Response, next: express.NextFunction) { const host = req.body.signature.host - db.Pod.loadByHost(host, function (err, pod) { - if (err) { - logger.error('Cannot get signed host in body.', { error: err }) - return res.sendStatus(500) - } + db.Pod.loadByHost(host) + .then(pod => { + if (pod === null) { + logger.error('Unknown pod %s.', host) + return res.sendStatus(403) + } - if (pod === null) { - logger.error('Unknown pod %s.', host) - return res.sendStatus(403) - } + logger.debug('Checking signature from %s.', host) - logger.debug('Checking signature from %s.', host) + let signatureShouldBe + // If there is data in the body the sender used it for its signature + // If there is no data we just use its host as signature + if (req.body.data) { + signatureShouldBe = req.body.data + } else { + signatureShouldBe = host + } - let signatureShouldBe - // If there is data in the body the sender used it for its signature - // If there is no data we just use its host as signature - if (req.body.data) { - signatureShouldBe = req.body.data - } else { - signatureShouldBe = host - } + const signatureOk = peertubeCryptoCheckSignature(pod.publicKey, signatureShouldBe, req.body.signature.signature) - const signatureOk = peertubeCryptoCheckSignature(pod.publicKey, signatureShouldBe, req.body.signature.signature) + if (signatureOk === true) { + res.locals.secure = { + pod + } - if (signatureOk === true) { - res.locals.secure = { - pod + return next() } - return next() - } - - logger.error('Signature is not okay in body for %s.', req.body.signature.host) - return res.sendStatus(403) - }) + logger.error('Signature is not okay in body for %s.', req.body.signature.host) + return res.sendStatus(403) + }) + .catch(err => { + logger.error('Cannot get signed host in body.', { error: err }) + return res.sendStatus(500) + }) } // --------------------------------------------------------------------------- diff --git a/server/middlewares/validators/pods.ts b/server/middlewares/validators/pods.ts index d8eb90168..da7fc2bd6 100644 --- a/server/middlewares/validators/pods.ts +++ b/server/middlewares/validators/pods.ts @@ -19,19 +19,19 @@ function makeFriendsValidator (req: express.Request, res: express.Response, next logger.debug('Checking makeFriends parameters', { parameters: req.body }) checkErrors(req, res, function () { - hasFriends(function (err, heHasFriends) { - if (err) { + hasFriends() + .then(heHasFriends => { + if (heHasFriends === true) { + // We need to quit our friends before make new ones + return res.sendStatus(409) + } + + return next() + }) + .catch(err => { logger.error('Cannot know if we have friends.', { error: err }) res.sendStatus(500) - } - - if (heHasFriends === true) { - // We need to quit our friends before make new ones - return res.sendStatus(409) - } - - return next() - }) + }) }) } @@ -42,19 +42,19 @@ function podsAddValidator (req: express.Request, res: express.Response, next: ex logger.debug('Checking podsAdd parameters', { parameters: req.body }) checkErrors(req, res, function () { - db.Pod.loadByHost(req.body.host, function (err, pod) { - if (err) { + db.Pod.loadByHost(req.body.host) + .then(pod => { + // Pod with this host already exists + if (pod) { + return res.sendStatus(409) + } + + return next() + }) + .catch(err => { logger.error('Cannot load pod by host.', { error: err }) res.sendStatus(500) - } - - // Pod with this host already exists - if (pod) { - return res.sendStatus(409) - } - - return next() - }) + }) }) } diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index b7b9ef370..c06735047 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -13,16 +13,16 @@ function usersAddValidator (req: express.Request, res: express.Response, next: e logger.debug('Checking usersAdd parameters', { parameters: req.body }) checkErrors(req, res, function () { - db.User.loadByUsernameOrEmail(req.body.username, req.body.email, function (err, user) { - if (err) { + db.User.loadByUsernameOrEmail(req.body.username, req.body.email) + .then(user => { + if (user) return res.status(409).send('User already exists.') + + next() + }) + .catch(err => { logger.error('Error in usersAdd request validator.', { error: err }) return res.sendStatus(500) - } - - if (user) return res.status(409).send('User already exists.') - - next() - }) + }) }) } @@ -32,18 +32,18 @@ function usersRemoveValidator (req: express.Request, res: express.Response, next logger.debug('Checking usersRemove parameters', { parameters: req.params }) checkErrors(req, res, function () { - db.User.loadById(req.params.id, function (err, user) { - if (err) { - logger.error('Error in usersRemove request validator.', { error: err }) - return res.sendStatus(500) - } - - if (!user) return res.status(404).send('User not found') + db.User.loadById(req.params.id) + .then(user => { + if (!user) return res.status(404).send('User not found') - if (user.username === 'root') return res.status(400).send('Cannot remove the root user') + if (user.username === 'root') return res.status(400).send('Cannot remove the root user') - next() - }) + next() + }) + .catch(err => { + logger.error('Error in usersRemove request validator.', { error: err }) + return res.sendStatus(500) + }) }) } @@ -64,16 +64,16 @@ function usersVideoRatingValidator (req: express.Request, res: express.Response, logger.debug('Checking usersVideoRating parameters', { parameters: req.params }) checkErrors(req, res, function () { - db.Video.load(req.params.videoId, function (err, video) { - if (err) { + db.Video.load(req.params.videoId) + .then(video => { + if (!video) return res.status(404).send('Video not found') + + next() + }) + .catch(err => { logger.error('Error in user request validator.', { error: err }) return res.sendStatus(500) - } - - if (!video) return res.status(404).send('Video not found') - - next() - }) + }) }) } diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts index 03742a522..ec452cade 100644 --- a/server/middlewares/validators/videos.ts +++ b/server/middlewares/validators/videos.ts @@ -1,5 +1,4 @@ import 'express-validator' -import * as multer from 'multer' import * as express from 'express' import { database as db } from '../../initializers/database' @@ -24,18 +23,19 @@ function videosAddValidator (req: express.Request, res: express.Response, next: checkErrors(req, res, function () { const videoFile = req.files.videofile[0] - db.Video.getDurationFromFile(videoFile.path, function (err, duration) { - if (err) { - return res.status(400).send('Cannot retrieve metadata of the file.') - } - - if (!isVideoDurationValid(duration)) { - return res.status(400).send('Duration of the video file is too big (max: ' + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).') - } + db.Video.getDurationFromFile(videoFile.path) + .then(duration => { + if (!isVideoDurationValid('' + duration)) { + return res.status(400).send('Duration of the video file is too big (max: ' + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).') + } - videoFile['duration'] = duration - next() - }) + videoFile['duration'] = duration + next() + }) + .catch(err => { + logger.error('Error in getting duration from file.', { error: err }) + res.status(400).send('Cannot retrieve metadata of the file.') + }) }) } @@ -157,43 +157,42 @@ export { // --------------------------------------------------------------------------- function checkVideoExists (id: string, res: express.Response, callback: () => void) { - db.Video.loadAndPopulateAuthorAndPodAndTags(id, function (err, video) { - if (err) { - logger.error('Error in video request validator.', { error: err }) - return res.sendStatus(500) - } - + db.Video.loadAndPopulateAuthorAndPodAndTags(id).then(video => { if (!video) return res.status(404).send('Video not found') res.locals.video = video callback() }) + .catch(err => { + logger.error('Error in video request validator.', { error: err }) + return res.sendStatus(500) + }) } function checkUserCanDeleteVideo (userId: number, res: express.Response, callback: () => void) { // Retrieve the user who did the request - db.User.loadById(userId, function (err, user) { - if (err) { - logger.error('Error in video request validator.', { error: err }) - return res.sendStatus(500) - } - - // Check if the user can delete the video - // The user can delete it if s/he is an admin - // Or if s/he is the video's author - if (user.isAdmin() === false) { - if (res.locals.video.isOwned() === false) { - return res.status(403).send('Cannot remove video of another pod') - } - - if (res.locals.video.Author.userId !== res.locals.oauth.token.User.id) { - return res.status(403).send('Cannot remove video of another user') + db.User.loadById(userId) + .then(user => { + // Check if the user can delete the video + // The user can delete it if s/he is an admin + // Or if s/he is the video's author + if (user.isAdmin() === false) { + if (res.locals.video.isOwned() === false) { + return res.status(403).send('Cannot remove video of another pod') + } + + if (res.locals.video.Author.userId !== res.locals.oauth.token.User.id) { + return res.status(403).send('Cannot remove video of another user') + } } - } - // If we reach this comment, we can delete the video - callback() - }) + // If we reach this comment, we can delete the video + callback() + }) + .catch(err => { + logger.error('Error in video request validator.', { error: err }) + return res.sendStatus(500) + }) } function checkVideoIsBlacklistable (req: express.Request, res: express.Response, callback: () => void) { diff --git a/server/models/application/application-interface.ts b/server/models/application/application-interface.ts index c03513db1..33254ba2d 100644 --- a/server/models/application/application-interface.ts +++ b/server/models/application/application-interface.ts @@ -1,11 +1,13 @@ import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' export namespace ApplicationMethods { - export type LoadMigrationVersionCallback = (err: Error, version: number) => void - export type LoadMigrationVersion = (callback: LoadMigrationVersionCallback) => void + export type LoadMigrationVersion = () => Promise - export type UpdateMigrationVersionCallback = (err: Error, applicationInstance: ApplicationAttributes) => void - export type UpdateMigrationVersion = (newVersion: number, transaction: Sequelize.Transaction, callback: UpdateMigrationVersionCallback) => void + export type UpdateMigrationVersion = ( + newVersion: number, + transaction: Sequelize.Transaction + ) => Promise<[ number, ApplicationInstance[] ]> } export interface ApplicationClass { diff --git a/server/models/application/application.ts b/server/models/application/application.ts index 0e9a1ebb3..507b7a843 100644 --- a/server/models/application/application.ts +++ b/server/models/application/application.ts @@ -2,7 +2,6 @@ import * as Sequelize from 'sequelize' import { addMethodsToModel } from '../utils' import { - ApplicationClass, ApplicationAttributes, ApplicationInstance, @@ -35,23 +34,19 @@ export default function defineApplication (sequelize: Sequelize.Sequelize, DataT // --------------------------------------------------------------------------- -loadMigrationVersion = function (callback: ApplicationMethods.LoadMigrationVersionCallback) { +loadMigrationVersion = function () { const query = { attributes: [ 'migrationVersion' ] } - return Application.findOne(query).asCallback(function (err, data) { - const version = data ? data.migrationVersion : null - - return callback(err, version) - }) + return Application.findOne(query).then(data => data ? data.migrationVersion : null) } -updateMigrationVersion = function (newVersion: number, transaction: Sequelize.Transaction, callback: ApplicationMethods.UpdateMigrationVersionCallback) { +updateMigrationVersion = function (newVersion: number, transaction: Sequelize.Transaction) { const options: Sequelize.UpdateOptions = { where: {}, transaction: transaction } - return Application.update({ migrationVersion: newVersion }, options).asCallback(callback) + return Application.update({ migrationVersion: newVersion }, options) } diff --git a/server/models/job/job-interface.ts b/server/models/job/job-interface.ts index 31b377367..ba5622977 100644 --- a/server/models/job/job-interface.ts +++ b/server/models/job/job-interface.ts @@ -1,10 +1,10 @@ import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' import { JobState } from '../../../shared/models/job.model' export namespace JobMethods { - export type ListWithLimitCallback = (err: Error, jobInstances: JobInstance[]) => void - export type ListWithLimit = (limit: number, state: JobState, callback: ListWithLimitCallback) => void + export type ListWithLimit = (limit: number, state: JobState) => Promise } export interface JobClass { diff --git a/server/models/job/job.ts b/server/models/job/job.ts index 38e4e8f30..968f9d71d 100644 --- a/server/models/job/job.ts +++ b/server/models/job/job.ts @@ -5,7 +5,6 @@ import { JOB_STATES } from '../../initializers' import { addMethodsToModel } from '../utils' import { - JobClass, JobInstance, JobAttributes, @@ -49,7 +48,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se // --------------------------------------------------------------------------- -listWithLimit = function (limit: number, state: JobState, callback: JobMethods.ListWithLimitCallback) { +listWithLimit = function (limit: number, state: JobState) { const query = { order: [ [ 'id', 'ASC' ] @@ -60,5 +59,5 @@ listWithLimit = function (limit: number, state: JobState, callback: JobMethods.L } } - return Job.findAll(query).asCallback(callback) + return Job.findAll(query) } diff --git a/server/models/oauth/oauth-client-interface.ts b/server/models/oauth/oauth-client-interface.ts index 3b4325740..3526e4159 100644 --- a/server/models/oauth/oauth-client-interface.ts +++ b/server/models/oauth/oauth-client-interface.ts @@ -1,13 +1,12 @@ import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' export namespace OAuthClientMethods { - export type CountTotalCallback = (err: Error, total: number) => void - export type CountTotal = (callback: CountTotalCallback) => void + export type CountTotal = () => Promise - export type LoadFirstClientCallback = (err: Error, client: OAuthClientInstance) => void - export type LoadFirstClient = (callback: LoadFirstClientCallback) => void + export type LoadFirstClient = () => Promise - export type GetByIdAndSecret = (clientId, clientSecret) => void + export type GetByIdAndSecret = (clientId: string, clientSecret: string) => Promise } export interface OAuthClientClass { diff --git a/server/models/oauth/oauth-client.ts b/server/models/oauth/oauth-client.ts index fbc2a3393..9cc68771d 100644 --- a/server/models/oauth/oauth-client.ts +++ b/server/models/oauth/oauth-client.ts @@ -2,7 +2,6 @@ import * as Sequelize from 'sequelize' import { addMethodsToModel } from '../utils' import { - OAuthClientClass, OAuthClientInstance, OAuthClientAttributes, @@ -67,12 +66,12 @@ function associate (models) { }) } -countTotal = function (callback: OAuthClientMethods.CountTotalCallback) { - return OAuthClient.count().asCallback(callback) +countTotal = function () { + return OAuthClient.count() } -loadFirstClient = function (callback: OAuthClientMethods.LoadFirstClientCallback) { - return OAuthClient.findOne().asCallback(callback) +loadFirstClient = function () { + return OAuthClient.findOne() } getByIdAndSecret = function (clientId: string, clientSecret: string) { diff --git a/server/models/oauth/oauth-token-interface.ts b/server/models/oauth/oauth-token-interface.ts index 815ad5eef..f2ddafa54 100644 --- a/server/models/oauth/oauth-token-interface.ts +++ b/server/models/oauth/oauth-token-interface.ts @@ -1,5 +1,5 @@ import * as Sequelize from 'sequelize' -import * as Bluebird from 'bluebird' +import * as Promise from 'bluebird' import { UserModel } from '../user' @@ -15,12 +15,11 @@ export type OAuthTokenInfo = { } export namespace OAuthTokenMethods { - export type GetByRefreshTokenAndPopulateClient = (refreshToken: string) => Bluebird - export type GetByTokenAndPopulateUser = (bearerToken: string) => Bluebird - export type GetByRefreshTokenAndPopulateUser = (refreshToken: string) => Bluebird + export type GetByRefreshTokenAndPopulateClient = (refreshToken: string) => Promise + export type GetByTokenAndPopulateUser = (bearerToken: string) => Promise + export type GetByRefreshTokenAndPopulateUser = (refreshToken: string) => Promise - export type RemoveByUserIdCallback = (err: Error) => void - export type RemoveByUserId = (userId, callback) => void + export type RemoveByUserId = (userId) => Promise } export interface OAuthTokenClass { diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts index eab9cf858..8c6883465 100644 --- a/server/models/oauth/oauth-token.ts +++ b/server/models/oauth/oauth-token.ts @@ -4,7 +4,6 @@ import { logger } from '../../helpers' import { addMethodsToModel } from '../utils' import { - OAuthTokenClass, OAuthTokenInstance, OAuthTokenAttributes, @@ -149,12 +148,12 @@ getByRefreshTokenAndPopulateUser = function (refreshToken: string) { }) } -removeByUserId = function (userId, callback) { +removeByUserId = function (userId: number) { const query = { where: { userId: userId } } - return OAuthToken.destroy(query).asCallback(callback) + return OAuthToken.destroy(query) } diff --git a/server/models/pod/pod-interface.ts b/server/models/pod/pod-interface.ts index d88847c45..f6963d47e 100644 --- a/server/models/pod/pod-interface.ts +++ b/server/models/pod/pod-interface.ts @@ -1,4 +1,5 @@ import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' // Don't use barrel, import just what we need import { Pod as FormatedPod } from '../../../shared/models/pod.model' @@ -6,32 +7,23 @@ import { Pod as FormatedPod } from '../../../shared/models/pod.model' export namespace PodMethods { export type ToFormatedJSON = (this: PodInstance) => FormatedPod - export type CountAllCallback = (err: Error, total: number) => void - export type CountAll = (callback) => void + export type CountAll = () => Promise - export type IncrementScoresCallback = (err: Error) => void - export type IncrementScores = (ids: number[], value: number, callback?: IncrementScoresCallback) => void + export type IncrementScores = (ids: number[], value: number) => Promise<[ number, PodInstance[] ]> - export type ListCallback = (err: Error, podInstances?: PodInstance[]) => void - export type List = (callback: ListCallback) => void + export type List = () => Promise - export type ListAllIdsCallback = (err: Error, ids?: number[]) => void - export type ListAllIds = (transaction: Sequelize.Transaction, callback: ListAllIdsCallback) => void + export type ListAllIds = (transaction: Sequelize.Transaction) => Promise - export type ListRandomPodIdsWithRequestCallback = (err: Error, podInstanceIds?: number[]) => void - export type ListRandomPodIdsWithRequest = (limit: number, tableWithPods: string, tableWithPodsJoins: string, callback: ListRandomPodIdsWithRequestCallback) => void + export type ListRandomPodIdsWithRequest = (limit: number, tableWithPods: string, tableWithPodsJoins: string) => Promise - export type ListBadPodsCallback = (err: Error, podInstances?: PodInstance[]) => void - export type ListBadPods = (callback: ListBadPodsCallback) => void + export type ListBadPods = () => Promise - export type LoadCallback = (err: Error, podInstance: PodInstance) => void - export type Load = (id: number, callback: LoadCallback) => void + export type Load = (id: number) => Promise - export type LoadByHostCallback = (err: Error, podInstance: PodInstance) => void - export type LoadByHost = (host: string, callback: LoadByHostCallback) => void + export type LoadByHost = (host: string) => Promise - export type RemoveAllCallback = (err: Error) => void - export type RemoveAll = (callback: RemoveAllCallback) => void + export type RemoveAll = () => Promise export type UpdatePodsScore = (goodPods: number[], badPods: number[]) => void } diff --git a/server/models/pod/pod.ts b/server/models/pod/pod.ts index 4fe7fda1c..9209380fc 100644 --- a/server/models/pod/pod.ts +++ b/server/models/pod/pod.ts @@ -1,4 +1,3 @@ -import { each, waterfall } from 'async' import { map } from 'lodash' import * as Sequelize from 'sequelize' @@ -7,7 +6,6 @@ import { logger, isHostValid } from '../../helpers' import { addMethodsToModel } from '../utils' import { - PodClass, PodInstance, PodAttributes, @@ -118,13 +116,11 @@ function associate (models) { }) } -countAll = function (callback: PodMethods.CountAllCallback) { - return Pod.count().asCallback(callback) +countAll = function () { + return Pod.count() } -incrementScores = function (ids: number[], value: number, callback?: PodMethods.IncrementScoresCallback) { - if (!callback) callback = function () { /* empty */ } - +incrementScores = function (ids: number[], value: number) { const update = { score: Sequelize.literal('score +' + value) } @@ -139,33 +135,28 @@ incrementScores = function (ids: number[], value: number, callback?: PodMethods. validate: false } - return Pod.update(update, options).asCallback(callback) + return Pod.update(update, options) } -list = function (callback: PodMethods.ListCallback) { - return Pod.findAll().asCallback(callback) +list = function () { + return Pod.findAll() } -listAllIds = function (transaction: Sequelize.Transaction, callback: PodMethods.ListAllIdsCallback) { - const query: any = { - attributes: [ 'id' ] +listAllIds = function (transaction: Sequelize.Transaction) { + const query: Sequelize.FindOptions = { + attributes: [ 'id' ], + transaction } - if (transaction !== null) query.transaction = transaction - - return Pod.findAll(query).asCallback(function (err: Error, pods) { - if (err) return callback(err) - - return callback(null, map(pods, 'id')) + return Pod.findAll(query).then(pods => { + return map(pods, 'id') }) } -listRandomPodIdsWithRequest = function (limit: number, tableWithPods: string, tableWithPodsJoins: string, callback: PodMethods.ListRandomPodIdsWithRequestCallback) { - Pod.count().asCallback(function (err, count) { - if (err) return callback(err) - +listRandomPodIdsWithRequest = function (limit: number, tableWithPods: string, tableWithPodsJoins: string) { + return Pod.count().then(count => { // Optimization... - if (count === 0) return callback(null, []) + if (count === 0) return [] let start = Math.floor(Math.random() * count) - limit if (start < 0) start = 0 @@ -186,56 +177,55 @@ listRandomPodIdsWithRequest = function (limit: number, tableWithPods: string, ta } } - return Pod.findAll(query).asCallback(function (err, pods) { - if (err) return callback(err) - - return callback(null, map(pods, 'id')) + return Pod.findAll(query).then(pods => { + return map(pods, 'id') }) }) } -listBadPods = function (callback: PodMethods.ListBadPodsCallback) { +listBadPods = function () { const query = { where: { score: { $lte: 0 } } } - return Pod.findAll(query).asCallback(callback) + return Pod.findAll(query) } -load = function (id: number, callback: PodMethods.LoadCallback) { - return Pod.findById(id).asCallback(callback) +load = function (id: number) { + return Pod.findById(id) } -loadByHost = function (host: string, callback: PodMethods.LoadByHostCallback) { +loadByHost = function (host: string) { const query = { where: { host: host } } - return Pod.findOne(query).asCallback(callback) + return Pod.findOne(query) } -removeAll = function (callback: PodMethods.RemoveAllCallback) { - return Pod.destroy().asCallback(callback) +removeAll = function () { + return Pod.destroy() } updatePodsScore = function (goodPods: number[], badPods: number[]) { logger.info('Updating %d good pods and %d bad pods scores.', goodPods.length, badPods.length) if (goodPods.length !== 0) { - incrementScores(goodPods, PODS_SCORE.BONUS, function (err) { - if (err) logger.error('Cannot increment scores of good pods.', { error: err }) + incrementScores(goodPods, PODS_SCORE.BONUS).catch(err => { + logger.error('Cannot increment scores of good pods.', { error: err }) }) } if (badPods.length !== 0) { - incrementScores(badPods, PODS_SCORE.MALUS, function (err) { - if (err) logger.error('Cannot decrement scores of bad pods.', { error: err }) - removeBadPods() - }) + incrementScores(badPods, PODS_SCORE.MALUS) + .then(() => removeBadPods()) + .catch(err => { + if (err) logger.error('Cannot decrement scores of bad pods.', { error: err }) + }) } } @@ -243,32 +233,19 @@ updatePodsScore = function (goodPods: number[], badPods: number[]) { // Remove pods with a score of 0 (too many requests where they were unreachable) function removeBadPods () { - waterfall([ - function findBadPods (callback) { - listBadPods(function (err, pods) { - if (err) { - logger.error('Cannot find bad pods.', { error: err }) - return callback(err) - } - - return callback(null, pods) - }) - }, - - function removeTheseBadPods (pods, callback) { - each(pods, function (pod: any, callbackEach) { - pod.destroy().asCallback(callbackEach) - }, function (err) { - return callback(err, pods.length) - }) - } - ], function (err, numberOfPodsRemoved) { - if (err) { + return listBadPods() + .then(pods => { + const podsRemovePromises = pods.map(pod => pod.destroy()) + return Promise.all(podsRemovePromises).then(() => pods.length) + }) + .then(numberOfPodsRemoved => { + if (numberOfPodsRemoved) { + logger.info('Removed %d pods.', numberOfPodsRemoved) + } else { + logger.info('No need to remove bad pods.') + } + }) + .catch(err => { logger.error('Cannot remove bad pods.', { error: err }) - } else if (numberOfPodsRemoved) { - logger.info('Removed %d pods.', numberOfPodsRemoved) - } else { - logger.info('No need to remove bad pods.') - } - }) + }) } diff --git a/server/models/request/abstract-request-interface.ts b/server/models/request/abstract-request-interface.ts new file mode 100644 index 000000000..a384f4d27 --- /dev/null +++ b/server/models/request/abstract-request-interface.ts @@ -0,0 +1,12 @@ +import * as Promise from 'bluebird' + +export interface AbstractRequestClass { + countTotalRequests: () => Promise + listWithLimitAndRandom: (limitPods: number, limitRequestsPerPod: number) => Promise + removeWithEmptyTo: () => Promise + removeAll: () => Promise +} + +export interface AbstractRequestToPodClass { + removeByRequestIdsAndPod: (ids: number[], podId: number) => Promise +} diff --git a/server/models/request/index.ts b/server/models/request/index.ts index 824c140f5..3dd6aedc2 100644 --- a/server/models/request/index.ts +++ b/server/models/request/index.ts @@ -1,3 +1,4 @@ +export * from './abstract-request-interface' export * from './request-interface' export * from './request-to-pod-interface' export * from './request-video-event-interface' diff --git a/server/models/request/request-interface.ts b/server/models/request/request-interface.ts index 483850633..7b0ee4df9 100644 --- a/server/models/request/request-interface.ts +++ b/server/models/request/request-interface.ts @@ -1,5 +1,7 @@ import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' +import { AbstractRequestClass } from './abstract-request-interface' import { PodInstance, PodAttributes } from '../pod' import { RequestEndpoint } from '../../../shared/models/request-scheduler.model' @@ -11,20 +13,16 @@ export type RequestsGrouped = { } export namespace RequestMethods { - export type CountTotalRequestsCallback = (err: Error, total: number) => void - export type CountTotalRequests = (callback: CountTotalRequestsCallback) => void + export type CountTotalRequests = () => Promise - export type ListWithLimitAndRandomCallback = (err: Error, requestsGrouped?: RequestsGrouped) => void - export type ListWithLimitAndRandom = (limitPods, limitRequestsPerPod, callback: ListWithLimitAndRandomCallback) => void + export type ListWithLimitAndRandom = (limitPods: number, limitRequestsPerPod: number) => Promise - export type RemoveWithEmptyToCallback = (err: Error) => void - export type RemoveWithEmptyTo = (callback: RemoveWithEmptyToCallback) => void + export type RemoveWithEmptyTo = () => Promise - export type RemoveAllCallback = (err: Error) => void - export type RemoveAll = (callback: RemoveAllCallback) => void + export type RemoveAll = () => Promise } -export interface RequestClass { +export interface RequestClass extends AbstractRequestClass { countTotalRequests: RequestMethods.CountTotalRequests listWithLimitAndRandom: RequestMethods.ListWithLimitAndRandom removeWithEmptyTo: RequestMethods.RemoveWithEmptyTo diff --git a/server/models/request/request-to-pod-interface.ts b/server/models/request/request-to-pod-interface.ts index 6d75ca6e5..7ca99f4d4 100644 --- a/server/models/request/request-to-pod-interface.ts +++ b/server/models/request/request-to-pod-interface.ts @@ -1,11 +1,13 @@ import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +import { AbstractRequestToPodClass } from './abstract-request-interface' export namespace RequestToPodMethods { - export type RemoveByRequestIdsAndPodCallback = (err: Error) => void - export type RemoveByRequestIdsAndPod = (requestsIds: number[], podId: number, callback?: RemoveByRequestIdsAndPodCallback) => void + export type RemoveByRequestIdsAndPod = (requestsIds: number[], podId: number) => Promise } -export interface RequestToPodClass { +export interface RequestToPodClass extends AbstractRequestToPodClass { removeByRequestIdsAndPod: RequestToPodMethods.RemoveByRequestIdsAndPod } diff --git a/server/models/request/request-to-pod.ts b/server/models/request/request-to-pod.ts index 67331be1d..6678ed290 100644 --- a/server/models/request/request-to-pod.ts +++ b/server/models/request/request-to-pod.ts @@ -2,7 +2,6 @@ import * as Sequelize from 'sequelize' import { addMethodsToModel } from '../utils' import { - RequestToPodClass, RequestToPodInstance, RequestToPodAttributes, @@ -38,9 +37,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da // --------------------------------------------------------------------------- -removeByRequestIdsAndPod = function (requestsIds: number[], podId: number, callback?: RequestToPodMethods.RemoveByRequestIdsAndPodCallback) { - if (!callback) callback = function () { /* empty */ } - +removeByRequestIdsAndPod = function (requestsIds: number[], podId: number) { const query = { where: { requestId: { @@ -50,5 +47,5 @@ removeByRequestIdsAndPod = function (requestsIds: number[], podId: number, callb } } - RequestToPod.destroy(query).asCallback(callback) + return RequestToPod.destroy(query) } diff --git a/server/models/request/request-video-event-interface.ts b/server/models/request/request-video-event-interface.ts index 3ed03339a..a5032e1b1 100644 --- a/server/models/request/request-video-event-interface.ts +++ b/server/models/request/request-video-event-interface.ts @@ -1,5 +1,7 @@ import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' +import { AbstractRequestClass, AbstractRequestToPodClass } from './abstract-request-interface' import { VideoInstance } from '../video' import { PodInstance } from '../pod' @@ -16,20 +18,16 @@ export type RequestsVideoEventGrouped = { } export namespace RequestVideoEventMethods { - export type CountTotalRequestsCallback = (err: Error, total: number) => void - export type CountTotalRequests = (callback: CountTotalRequestsCallback) => void + export type CountTotalRequests = () => Promise - export type ListWithLimitAndRandomCallback = (err: Error, requestsGrouped?: RequestsVideoEventGrouped) => void - export type ListWithLimitAndRandom = (limitPods: number, limitRequestsPerPod: number, callback: ListWithLimitAndRandomCallback) => void + export type ListWithLimitAndRandom = (limitPods: number, limitRequestsPerPod: number) => Promise - export type RemoveByRequestIdsAndPodCallback = () => void - export type RemoveByRequestIdsAndPod = (ids: number[], podId: number, callback: RemoveByRequestIdsAndPodCallback) => void + export type RemoveByRequestIdsAndPod = (ids: number[], podId: number) => Promise - export type RemoveAllCallback = () => void - export type RemoveAll = (callback: RemoveAllCallback) => void + export type RemoveAll = () => Promise } -export interface RequestVideoEventClass { +export interface RequestVideoEventClass extends AbstractRequestClass, AbstractRequestToPodClass { countTotalRequests: RequestVideoEventMethods.CountTotalRequests listWithLimitAndRandom: RequestVideoEventMethods.ListWithLimitAndRandom removeByRequestIdsAndPod: RequestVideoEventMethods.RemoveByRequestIdsAndPod @@ -41,10 +39,12 @@ export interface RequestVideoEventAttributes { count: number } -export interface RequestVideoEventInstance extends RequestVideoEventClass, RequestVideoEventAttributes, Sequelize.Instance { +export interface RequestVideoEventInstance + extends RequestVideoEventClass, RequestVideoEventAttributes, Sequelize.Instance { id: number Video: VideoInstance } -export interface RequestVideoEventModel extends RequestVideoEventClass, Sequelize.Model {} +export interface RequestVideoEventModel + extends RequestVideoEventClass, Sequelize.Model {} diff --git a/server/models/request/request-video-event.ts b/server/models/request/request-video-event.ts index f552ef50b..90ea15702 100644 --- a/server/models/request/request-video-event.ts +++ b/server/models/request/request-video-event.ts @@ -10,7 +10,6 @@ import { REQUEST_VIDEO_EVENT_TYPES } from '../../initializers' import { isVideoEventCountValid } from '../../helpers' import { addMethodsToModel } from '../utils' import { - RequestVideoEventClass, RequestVideoEventInstance, RequestVideoEventAttributes, @@ -77,23 +76,21 @@ function associate (models) { }) } -countTotalRequests = function (callback: RequestVideoEventMethods.CountTotalRequestsCallback) { +countTotalRequests = function () { const query = {} - return RequestVideoEvent.count(query).asCallback(callback) + return RequestVideoEvent.count(query) } -listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number, callback: RequestVideoEventMethods.ListWithLimitAndRandomCallback) { +listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number) { const Pod = db.Pod // We make a join between videos and authors to find the podId of our video event requests const podJoins = 'INNER JOIN "Videos" ON "Videos"."authorId" = "Authors"."id" ' + 'INNER JOIN "RequestVideoEvents" ON "RequestVideoEvents"."videoId" = "Videos"."id"' - Pod.listRandomPodIdsWithRequest(limitPods, 'Authors', podJoins, function (err, podIds) { - if (err) return callback(err) - + return Pod.listRandomPodIdsWithRequest(limitPods, 'Authors', podJoins).then(podIds => { // We don't have friends that have requests - if (podIds.length === 0) return callback(null, []) + if (podIds.length === 0) return [] const query = { order: [ @@ -121,16 +118,14 @@ listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: numbe ] } - RequestVideoEvent.findAll(query).asCallback(function (err, requests) { - if (err) return callback(err) - + return RequestVideoEvent.findAll(query).then(requests => { const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod) - return callback(err, requestsGrouped) + return requestsGrouped }) }) } -removeByRequestIdsAndPod = function (ids: number[], podId: number, callback: RequestVideoEventMethods.RemoveByRequestIdsAndPodCallback) { +removeByRequestIdsAndPod = function (ids: number[], podId: number) { const query = { where: { id: { @@ -152,12 +147,12 @@ removeByRequestIdsAndPod = function (ids: number[], podId: number, callback: Req ] } - RequestVideoEvent.destroy(query).asCallback(callback) + return RequestVideoEvent.destroy(query) } -removeAll = function (callback: RequestVideoEventMethods.RemoveAllCallback) { +removeAll = function () { // Delete all requests - RequestVideoEvent.truncate({ cascade: true }).asCallback(callback) + return RequestVideoEvent.truncate({ cascade: true }) } // --------------------------------------------------------------------------- diff --git a/server/models/request/request-video-qadu-interface.ts b/server/models/request/request-video-qadu-interface.ts index 805771cda..9a172a4d4 100644 --- a/server/models/request/request-video-qadu-interface.ts +++ b/server/models/request/request-video-qadu-interface.ts @@ -1,5 +1,7 @@ import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' +import { AbstractRequestClass, AbstractRequestToPodClass } from './abstract-request-interface' import { VideoInstance } from '../video' import { PodInstance } from '../pod' @@ -14,20 +16,16 @@ export type RequestsVideoQaduGrouped = { } export namespace RequestVideoQaduMethods { - export type CountTotalRequestsCallback = (err: Error, total: number) => void - export type CountTotalRequests = (callback: CountTotalRequestsCallback) => void + export type CountTotalRequests = () => Promise - export type ListWithLimitAndRandomCallback = (err: Error, requestsGrouped?: RequestsVideoQaduGrouped) => void - export type ListWithLimitAndRandom = (limitPods: number, limitRequestsPerPod: number, callback: ListWithLimitAndRandomCallback) => void + export type ListWithLimitAndRandom = (limitPods: number, limitRequestsPerPod: number) => Promise - export type RemoveByRequestIdsAndPodCallback = () => void - export type RemoveByRequestIdsAndPod = (ids: number[], podId: number, callback: RemoveByRequestIdsAndPodCallback) => void + export type RemoveByRequestIdsAndPod = (ids: number[], podId: number) => Promise - export type RemoveAllCallback = () => void - export type RemoveAll = (callback: RemoveAllCallback) => void + export type RemoveAll = () => Promise } -export interface RequestVideoQaduClass { +export interface RequestVideoQaduClass extends AbstractRequestClass, AbstractRequestToPodClass { countTotalRequests: RequestVideoQaduMethods.CountTotalRequests listWithLimitAndRandom: RequestVideoQaduMethods.ListWithLimitAndRandom removeByRequestIdsAndPod: RequestVideoQaduMethods.RemoveByRequestIdsAndPod @@ -38,11 +36,13 @@ export interface RequestVideoQaduAttributes { type: RequestVideoQaduType } -export interface RequestVideoQaduInstance extends RequestVideoQaduClass, RequestVideoQaduAttributes, Sequelize.Instance { +export interface RequestVideoQaduInstance + extends RequestVideoQaduClass, RequestVideoQaduAttributes, Sequelize.Instance { id: number Pod: PodInstance Video: VideoInstance } -export interface RequestVideoQaduModel extends RequestVideoQaduClass, Sequelize.Model {} +export interface RequestVideoQaduModel + extends RequestVideoQaduClass, Sequelize.Model {} diff --git a/server/models/request/request-video-qadu.ts b/server/models/request/request-video-qadu.ts index da62239f5..74e28f129 100644 --- a/server/models/request/request-video-qadu.ts +++ b/server/models/request/request-video-qadu.ts @@ -16,7 +16,6 @@ import { database as db } from '../../initializers/database' import { REQUEST_VIDEO_QADU_TYPES } from '../../initializers' import { addMethodsToModel } from '../utils' import { - RequestVideoQaduClass, RequestVideoQaduInstance, RequestVideoQaduAttributes, @@ -83,20 +82,18 @@ function associate (models) { }) } -countTotalRequests = function (callback: RequestVideoQaduMethods.CountTotalRequestsCallback) { +countTotalRequests = function () { const query = {} - return RequestVideoQadu.count(query).asCallback(callback) + return RequestVideoQadu.count(query) } -listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number, callback: RequestVideoQaduMethods.ListWithLimitAndRandomCallback) { +listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number) { const Pod = db.Pod const tableJoin = '' - Pod.listRandomPodIdsWithRequest(limitPods, 'RequestVideoQadus', tableJoin, function (err, podIds) { - if (err) return callback(err) - + return Pod.listRandomPodIdsWithRequest(limitPods, 'RequestVideoQadus', tableJoin).then(podIds => { // We don't have friends that have requests - if (podIds.length === 0) return callback(null, []) + if (podIds.length === 0) return [] const query = { include: [ @@ -114,16 +111,14 @@ listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: numbe ] } - RequestVideoQadu.findAll(query).asCallback(function (err, requests) { - if (err) return callback(err) - + return RequestVideoQadu.findAll(query).then(requests => { const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod) - return callback(err, requestsGrouped) + return requestsGrouped }) }) } -removeByRequestIdsAndPod = function (ids: number[], podId: number, callback: RequestVideoQaduMethods.RemoveByRequestIdsAndPodCallback) { +removeByRequestIdsAndPod = function (ids: number[], podId: number) { const query = { where: { id: { @@ -133,12 +128,12 @@ removeByRequestIdsAndPod = function (ids: number[], podId: number, callback: Req } } - RequestVideoQadu.destroy(query).asCallback(callback) + return RequestVideoQadu.destroy(query) } -removeAll = function (callback: RequestVideoQaduMethods.RemoveAllCallback) { +removeAll = function () { // Delete all requests - RequestVideoQadu.truncate({ cascade: true }).asCallback(callback) + return RequestVideoQadu.truncate({ cascade: true }) } // --------------------------------------------------------------------------- diff --git a/server/models/request/request.ts b/server/models/request/request.ts index 66e7da845..c3ce2cd4e 100644 --- a/server/models/request/request.ts +++ b/server/models/request/request.ts @@ -5,7 +5,6 @@ import { database as db } from '../../initializers/database' import { REQUEST_ENDPOINTS } from '../../initializers' import { addMethodsToModel } from '../utils' import { - RequestClass, RequestInstance, RequestAttributes, @@ -60,25 +59,23 @@ function associate (models) { }) } -countTotalRequests = function (callback: RequestMethods.CountTotalRequestsCallback) { +countTotalRequests = function () { // We need to include Pod because there are no cascade delete when a pod is removed // So we could count requests that do not have existing pod anymore const query = { include: [ Request['sequelize'].models.Pod ] } - return Request.count(query).asCallback(callback) + return Request.count(query) } -listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number, callback: RequestMethods.ListWithLimitAndRandomCallback) { +listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number) { const Pod = db.Pod const tableJoin = '' - Pod.listRandomPodIdsWithRequest(limitPods, 'RequestToPods', '', function (err, podIds) { - if (err) return callback(err) - + return Pod.listRandomPodIdsWithRequest(limitPods, 'RequestToPods', tableJoin).then(podIds => { // We don't have friends that have requests - if (podIds.length === 0) return callback(null, []) + if (podIds.length === 0) return [] // The first x requests of these pods // It is very important to sort by id ASC to keep the requests order! @@ -98,23 +95,20 @@ listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: numbe ] } - Request.findAll(query).asCallback(function (err, requests) { - if (err) return callback(err) + return Request.findAll(query).then(requests => { const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod) - return callback(err, requestsGrouped) + return requestsGrouped }) }) } -removeAll = function (callback: RequestMethods.RemoveAllCallback) { +removeAll = function () { // Delete all requests - Request.truncate({ cascade: true }).asCallback(callback) + return Request.truncate({ cascade: true }) } -removeWithEmptyTo = function (callback?: RequestMethods.RemoveWithEmptyToCallback) { - if (!callback) callback = function () { /* empty */ } - +removeWithEmptyTo = function () { const query = { where: { id: { @@ -125,7 +119,7 @@ removeWithEmptyTo = function (callback?: RequestMethods.RemoveWithEmptyToCallbac } } - Request.destroy(query).asCallback(callback) + return Request.destroy(query) } // --------------------------------------------------------------------------- diff --git a/server/models/user/user-interface.ts b/server/models/user/user-interface.ts index 48c67678b..f743945f8 100644 --- a/server/models/user/user-interface.ts +++ b/server/models/user/user-interface.ts @@ -1,35 +1,29 @@ import * as Sequelize from 'sequelize' -import * as Bluebird from 'bluebird' +import * as Promise from 'bluebird' // Don't use barrel, import just what we need import { UserRole, User as FormatedUser } from '../../../shared/models/user.model' +import { ResultList } from '../../../shared/models/result-list.model' export namespace UserMethods { - export type IsPasswordMatchCallback = (err: Error, same: boolean) => void - export type IsPasswordMatch = (this: UserInstance, password: string, callback: IsPasswordMatchCallback) => void + export type IsPasswordMatch = (this: UserInstance, password: string) => Promise export type ToFormatedJSON = (this: UserInstance) => FormatedUser export type IsAdmin = (this: UserInstance) => boolean - export type CountTotalCallback = (err: Error, total: number) => void - export type CountTotal = (callback: CountTotalCallback) => void + export type CountTotal = () => Promise - export type GetByUsername = (username: string) => Bluebird + export type GetByUsername = (username: string) => Promise - export type ListCallback = (err: Error, userInstances: UserInstance[]) => void - export type List = (callback: ListCallback) => void + export type List = () => Promise - export type ListForApiCallback = (err: Error, userInstances?: UserInstance[], total?: number) => void - export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void + export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList > - export type LoadByIdCallback = (err: Error, userInstance: UserInstance) => void - export type LoadById = (id: number, callback: LoadByIdCallback) => void + export type LoadById = (id: number) => Promise - export type LoadByUsernameCallback = (err: Error, userInstance: UserInstance) => void - export type LoadByUsername = (username: string, callback: LoadByUsernameCallback) => void + export type LoadByUsername = (username: string) => Promise - export type LoadByUsernameOrEmailCallback = (err: Error, userInstance: UserInstance) => void - export type LoadByUsernameOrEmail = (username: string, email: string, callback: LoadByUsernameOrEmailCallback) => void + export type LoadByUsernameOrEmail = (username: string, email: string) => Promise } export interface UserClass { diff --git a/server/models/user/user-video-rate-interface.ts b/server/models/user/user-video-rate-interface.ts index a726639b1..e0b65a13d 100644 --- a/server/models/user/user-video-rate-interface.ts +++ b/server/models/user/user-video-rate-interface.ts @@ -1,10 +1,10 @@ import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' import { VideoRateType } from '../../../shared/models/user-video-rate.model' export namespace UserVideoRateMethods { - export type LoadCallback = (err: Error, userVideoRateInstance: UserVideoRateInstance) => void - export type Load = (userId: number, videoId: string, transaction: Sequelize.Transaction, callback: LoadCallback) => void + export type Load = (userId: number, videoId: string, transaction: Sequelize.Transaction) => Promise } export interface UserVideoRateClass { diff --git a/server/models/user/user-video-rate.ts b/server/models/user/user-video-rate.ts index 4bdd35bc9..37d0222cf 100644 --- a/server/models/user/user-video-rate.ts +++ b/server/models/user/user-video-rate.ts @@ -8,7 +8,6 @@ import { VIDEO_RATE_TYPES } from '../../initializers' import { addMethodsToModel } from '../utils' import { - UserVideoRateClass, UserVideoRateInstance, UserVideoRateAttributes, @@ -66,7 +65,7 @@ function associate (models) { }) } -load = function (userId: number, videoId: string, transaction: Sequelize.Transaction, callback: UserVideoRateMethods.LoadCallback) { +load = function (userId: number, videoId: string, transaction: Sequelize.Transaction) { const options: Sequelize.FindOptions = { where: { userId, @@ -75,5 +74,5 @@ load = function (userId: number, videoId: string, transaction: Sequelize.Transac } if (transaction) options.transaction = transaction - return UserVideoRate.findOne(options).asCallback(callback) + return UserVideoRate.findOne(options) } diff --git a/server/models/user/user.ts b/server/models/user/user.ts index 6b2410259..5ff81e741 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts @@ -13,7 +13,6 @@ import { import { addMethodsToModel } from '../utils' import { - UserClass, UserInstance, UserAttributes, @@ -118,21 +117,16 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da } function beforeCreateOrUpdate (user: UserInstance) { - return new Promise(function (resolve, reject) { - cryptPassword(user.password, function (err, hash) { - if (err) return reject(err) - - user.password = hash - - return resolve() - }) + return cryptPassword(user.password).then(hash => { + user.password = hash + return undefined }) } // ------------------------------ METHODS ------------------------------ -isPasswordMatch = function (this: UserInstance, password: string, callback: UserMethods.IsPasswordMatchCallback) { - return comparePassword(password, this.password, callback) +isPasswordMatch = function (this: UserInstance, password: string) { + return comparePassword(password, this.password) } toFormatedJSON = function (this: UserInstance) { @@ -164,8 +158,8 @@ function associate (models) { }) } -countTotal = function (callback: UserMethods.CountTotalCallback) { - return this.count().asCallback(callback) +countTotal = function () { + return this.count() } getByUsername = function (username: string) { @@ -178,44 +172,45 @@ getByUsername = function (username: string) { return User.findOne(query) } -list = function (callback: UserMethods.ListCallback) { - return User.find().asCallback(callback) +list = function () { + return User.findAll() } -listForApi = function (start: number, count: number, sort: string, callback: UserMethods.ListForApiCallback) { +listForApi = function (start: number, count: number, sort: string) { const query = { offset: start, limit: count, order: [ getSort(sort) ] } - return User.findAndCountAll(query).asCallback(function (err, result) { - if (err) return callback(err) - - return callback(null, result.rows, result.count) + return User.findAndCountAll(query).then(({ rows, count }) => { + return { + data: rows, + total: count + } }) } -loadById = function (id: number, callback: UserMethods.LoadByIdCallback) { - return User.findById(id).asCallback(callback) +loadById = function (id: number) { + return User.findById(id) } -loadByUsername = function (username: string, callback: UserMethods.LoadByUsernameCallback) { +loadByUsername = function (username: string) { const query = { where: { username: username } } - return User.findOne(query).asCallback(callback) + return User.findOne(query) } -loadByUsernameOrEmail = function (username: string, email: string, callback: UserMethods.LoadByUsernameOrEmailCallback) { +loadByUsernameOrEmail = function (username: string, email: string) { const query = { where: { $or: [ { username }, { email } ] } } - return User.findOne(query).asCallback(callback) + return User.findOne(query) } diff --git a/server/models/video/author-interface.ts b/server/models/video/author-interface.ts index c1b30848c..dbcb85b17 100644 --- a/server/models/video/author-interface.ts +++ b/server/models/video/author-interface.ts @@ -1,10 +1,15 @@ import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' import { PodInstance } from '../pod' export namespace AuthorMethods { - export type FindOrCreateAuthorCallback = (err: Error, authorInstance?: AuthorInstance) => void - export type FindOrCreateAuthor = (name: string, podId: number, userId: number, transaction: Sequelize.Transaction, callback: FindOrCreateAuthorCallback) => void + export type FindOrCreateAuthor = ( + name: string, + podId: number, + userId: number, + transaction: Sequelize.Transaction + ) => Promise } export interface AuthorClass { diff --git a/server/models/video/author.ts b/server/models/video/author.ts index 4a115e328..3222c4834 100644 --- a/server/models/video/author.ts +++ b/server/models/video/author.ts @@ -4,7 +4,6 @@ import { isUserUsernameValid } from '../../helpers' import { addMethodsToModel } from '../utils' import { - AuthorClass, AuthorInstance, AuthorAttributes, @@ -74,30 +73,18 @@ function associate (models) { }) } -findOrCreateAuthor = function ( - name: string, - podId: number, - userId: number, - transaction: Sequelize.Transaction, - callback: AuthorMethods.FindOrCreateAuthorCallback -) { +findOrCreateAuthor = function (name: string, podId: number, userId: number, transaction: Sequelize.Transaction) { const author = { name, podId, userId } - const query: any = { + const query: Sequelize.FindOrInitializeOptions = { where: author, - defaults: author + defaults: author, + transaction } - if (transaction !== null) query.transaction = transaction - - Author.findOrCreate(query).asCallback(function (err, result) { - if (err) return callback(err) - - // [ instance, wasCreated ] - return callback(null, result[0]) - }) + return Author.findOrCreate(query).then(([ authorInstance ]) => authorInstance) } diff --git a/server/models/video/tag-interface.ts b/server/models/video/tag-interface.ts index e045e7ca5..08e5c3246 100644 --- a/server/models/video/tag-interface.ts +++ b/server/models/video/tag-interface.ts @@ -1,8 +1,8 @@ import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' export namespace TagMethods { - export type FindOrCreateTagsCallback = (err: Error, tagInstances: TagInstance[]) => void - export type FindOrCreateTags = (tags: string[], transaction: Sequelize.Transaction, callback: FindOrCreateTagsCallback) => void + export type FindOrCreateTags = (tags: string[], transaction: Sequelize.Transaction) => Promise } export interface TagClass { diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts index 3c657d751..d0d8353d7 100644 --- a/server/models/video/tag.ts +++ b/server/models/video/tag.ts @@ -1,9 +1,8 @@ -import { each } from 'async' import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' import { addMethodsToModel } from '../utils' import { - TagClass, TagInstance, TagAttributes, @@ -52,10 +51,9 @@ function associate (models) { }) } -findOrCreateTags = function (tags: string[], transaction: Sequelize.Transaction, callback: TagMethods.FindOrCreateTagsCallback) { - const tagInstances = [] - - each(tags, function (tag, callbackEach) { +findOrCreateTags = function (tags: string[], transaction: Sequelize.Transaction) { + const tasks: Promise[] = [] + tags.forEach(tag => { const query: any = { where: { name: tag @@ -67,15 +65,9 @@ findOrCreateTags = function (tags: string[], transaction: Sequelize.Transaction, if (transaction) query.transaction = transaction - Tag.findOrCreate(query).asCallback(function (err, res) { - if (err) return callbackEach(err) - - // res = [ tag, isCreated ] - const tag = res[0] - tagInstances.push(tag) - return callbackEach() - }) - }, function (err) { - return callback(err, tagInstances) + const promise = Tag.findOrCreate(query).then(([ tagInstance ]) => tagInstance) + tasks.push(promise) }) + + return Promise.all(tasks) } diff --git a/server/models/video/video-abuse-interface.ts b/server/models/video/video-abuse-interface.ts index c85d09091..75647fe0e 100644 --- a/server/models/video/video-abuse-interface.ts +++ b/server/models/video/video-abuse-interface.ts @@ -1,6 +1,8 @@ import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' import { PodInstance } from '../pod' +import { ResultList } from '../../../shared' // Don't use barrel, import just what we need import { VideoAbuse as FormatedVideoAbuse } from '../../../shared/models/video-abuse.model' @@ -8,8 +10,7 @@ import { VideoAbuse as FormatedVideoAbuse } from '../../../shared/models/video-a export namespace VideoAbuseMethods { export type ToFormatedJSON = (this: VideoAbuseInstance) => FormatedVideoAbuse - export type ListForApiCallback = (err: Error, videoAbuseInstances?: VideoAbuseInstance[], total?: number) => void - export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void + export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList > } export interface VideoAbuseClass { diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index bc5f01e21..ab1a3ea7d 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts @@ -5,7 +5,6 @@ import { isVideoAbuseReporterUsernameValid, isVideoAbuseReasonValid } from '../. import { addMethodsToModel, getSort } from '../utils' import { - VideoAbuseClass, VideoAbuseInstance, VideoAbuseAttributes, @@ -109,7 +108,7 @@ function associate (models) { }) } -listForApi = function (start: number, count: number, sort: string, callback: VideoAbuseMethods.ListForApiCallback) { +listForApi = function (start: number, count: number, sort: string) { const query = { offset: start, limit: count, @@ -122,11 +121,7 @@ listForApi = function (start: number, count: number, sort: string, callback: Vid ] } - return VideoAbuse.findAndCountAll(query).asCallback(function (err, result) { - if (err) return callback(err) - - return callback(null, result.rows, result.count) + return VideoAbuse.findAndCountAll(query).then(({ rows, count }) => { + return { total: count, data: rows } }) } - - diff --git a/server/models/video/video-blacklist-interface.ts b/server/models/video/video-blacklist-interface.ts index d4d6528d1..5ca423801 100644 --- a/server/models/video/video-blacklist-interface.ts +++ b/server/models/video/video-blacklist-interface.ts @@ -1,4 +1,7 @@ import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +import { ResultList } from '../../../shared' // Don't use barrel, import just what we need import { BlacklistedVideo as FormatedBlacklistedVideo } from '../../../shared/models/video-blacklist.model' @@ -6,20 +9,15 @@ import { BlacklistedVideo as FormatedBlacklistedVideo } from '../../../shared/mo export namespace BlacklistedVideoMethods { export type ToFormatedJSON = (this: BlacklistedVideoInstance) => FormatedBlacklistedVideo - export type CountTotalCallback = (err: Error, total: number) => void - export type CountTotal = (callback: CountTotalCallback) => void + export type CountTotal = () => Promise - export type ListCallback = (err: Error, backlistedVideoInstances: BlacklistedVideoInstance[]) => void - export type List = (callback: ListCallback) => void + export type List = () => Promise - export type ListForApiCallback = (err: Error, blacklistedVIdeoInstances?: BlacklistedVideoInstance[], total?: number) => void - export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void + export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList > - export type LoadByIdCallback = (err: Error, blacklistedVideoInstance: BlacklistedVideoInstance) => void - export type LoadById = (id: number, callback: LoadByIdCallback) => void + export type LoadById = (id: number) => Promise - export type LoadByVideoIdCallback = (err: Error, blacklistedVideoInstance: BlacklistedVideoInstance) => void - export type LoadByVideoId = (id: string, callback: LoadByVideoIdCallback) => void + export type LoadByVideoId = (id: string) => Promise } export interface BlacklistedVideoClass { @@ -35,7 +33,8 @@ export interface BlacklistedVideoAttributes { videoId: string } -export interface BlacklistedVideoInstance extends BlacklistedVideoClass, BlacklistedVideoAttributes, Sequelize.Instance { +export interface BlacklistedVideoInstance + extends BlacklistedVideoClass, BlacklistedVideoAttributes, Sequelize.Instance { id: number createdAt: Date updatedAt: Date @@ -43,4 +42,5 @@ export interface BlacklistedVideoInstance extends BlacklistedVideoClass, Blackli toFormatedJSON: BlacklistedVideoMethods.ToFormatedJSON } -export interface BlacklistedVideoModel extends BlacklistedVideoClass, Sequelize.Model {} +export interface BlacklistedVideoModel + extends BlacklistedVideoClass, Sequelize.Model {} diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index 3576c96f6..8c42dbc21 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts @@ -2,7 +2,6 @@ import * as Sequelize from 'sequelize' import { addMethodsToModel, getSort } from '../utils' import { - BlacklistedVideoClass, BlacklistedVideoInstance, BlacklistedVideoAttributes, @@ -66,38 +65,39 @@ function associate (models) { }) } -countTotal = function (callback: BlacklistedVideoMethods.CountTotalCallback) { - return BlacklistedVideo.count().asCallback(callback) +countTotal = function () { + return BlacklistedVideo.count() } -list = function (callback: BlacklistedVideoMethods.ListCallback) { - return BlacklistedVideo.findAll().asCallback(callback) +list = function () { + return BlacklistedVideo.findAll() } -listForApi = function (start: number, count: number, sort: string, callback: BlacklistedVideoMethods.ListForApiCallback) { +listForApi = function (start: number, count: number, sort: string) { const query = { offset: start, limit: count, order: [ getSort(sort) ] } - return BlacklistedVideo.findAndCountAll(query).asCallback(function (err, result) { - if (err) return callback(err) - - return callback(null, result.rows, result.count) + return BlacklistedVideo.findAndCountAll(query).then(({ rows, count }) => { + return { + data: rows, + total: count + } }) } -loadById = function (id: number, callback: BlacklistedVideoMethods.LoadByIdCallback) { - return BlacklistedVideo.findById(id).asCallback(callback) +loadById = function (id: number) { + return BlacklistedVideo.findById(id) } -loadByVideoId = function (id: string, callback: BlacklistedVideoMethods.LoadByIdCallback) { +loadByVideoId = function (id: string) { const query = { where: { videoId: id } } - return BlacklistedVideo.find(query).asCallback(callback) + return BlacklistedVideo.findOne(query) } diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts index 4b591b9e7..c3e3365d5 100644 --- a/server/models/video/video-interface.ts +++ b/server/models/video/video-interface.ts @@ -1,10 +1,12 @@ import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' import { AuthorInstance } from './author-interface' -import { VideoTagInstance } from './video-tag-interface' +import { TagAttributes, TagInstance } from './tag-interface' // Don't use barrel, import just what we need import { Video as FormatedVideo } from '../../../shared/models/video.model' +import { ResultList } from '../../../shared/models/result-list.model' export type FormatedAddRemoteVideo = { name: string @@ -56,46 +58,32 @@ export namespace VideoMethods { export type IsOwned = (this: VideoInstance) => boolean export type ToFormatedJSON = (this: VideoInstance) => FormatedVideo - export type ToAddRemoteJSONCallback = (err: Error, videoFormated?: FormatedAddRemoteVideo) => void - export type ToAddRemoteJSON = (this: VideoInstance, callback: ToAddRemoteJSONCallback) => void - + export type ToAddRemoteJSON = (this: VideoInstance) => Promise export type ToUpdateRemoteJSON = (this: VideoInstance) => FormatedUpdateRemoteVideo - export type TranscodeVideofileCallback = (err: Error) => void - export type TranscodeVideofile = (this: VideoInstance, callback: TranscodeVideofileCallback) => void - - export type GenerateThumbnailFromDataCallback = (err: Error, thumbnailName?: string) => void - export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string, callback: GenerateThumbnailFromDataCallback) => void - - export type GetDurationFromFileCallback = (err: Error, duration?: number) => void - export type GetDurationFromFile = (videoPath, callback) => void - - export type ListCallback = (err: Error, videoInstances: VideoInstance[]) => void - export type List = (callback: ListCallback) => void - - export type ListForApiCallback = (err: Error, videoInstances?: VideoInstance[], total?: number) => void - export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void - - export type LoadByHostAndRemoteIdCallback = (err: Error, videoInstance: VideoInstance) => void - export type LoadByHostAndRemoteId = (fromHost: string, remoteId: string, callback: LoadByHostAndRemoteIdCallback) => void - - export type ListOwnedAndPopulateAuthorAndTagsCallback = (err: Error, videoInstances: VideoInstance[]) => void - export type ListOwnedAndPopulateAuthorAndTags = (callback: ListOwnedAndPopulateAuthorAndTagsCallback) => void - - export type ListOwnedByAuthorCallback = (err: Error, videoInstances: VideoInstance[]) => void - export type ListOwnedByAuthor = (author: string, callback: ListOwnedByAuthorCallback) => void - - export type LoadCallback = (err: Error, videoInstance: VideoInstance) => void - export type Load = (id: string, callback: LoadCallback) => void - - export type LoadAndPopulateAuthorCallback = (err: Error, videoInstance: VideoInstance) => void - export type LoadAndPopulateAuthor = (id: string, callback: LoadAndPopulateAuthorCallback) => void - - export type LoadAndPopulateAuthorAndPodAndTagsCallback = (err: Error, videoInstance: VideoInstance) => void - export type LoadAndPopulateAuthorAndPodAndTags = (id: string, callback: LoadAndPopulateAuthorAndPodAndTagsCallback) => void - - export type SearchAndPopulateAuthorAndPodAndTagsCallback = (err: Error, videoInstances?: VideoInstance[], total?: number) => void - export type SearchAndPopulateAuthorAndPodAndTags = (value: string, field: string, start: number, count: number, sort: string, callback: SearchAndPopulateAuthorAndPodAndTagsCallback) => void + export type TranscodeVideofile = (this: VideoInstance) => Promise + + // Return thumbnail name + export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise + export type GetDurationFromFile = (videoPath: string) => Promise + + export type List = () => Promise + export type ListOwnedAndPopulateAuthorAndTags = () => Promise + export type ListOwnedByAuthor = (author: string) => Promise + + export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList > + export type SearchAndPopulateAuthorAndPodAndTags = ( + value: string, + field: string, + start: number, + count: number, + sort: string + ) => Promise< ResultList > + + export type Load = (id: string) => Promise + export type LoadByHostAndRemoteId = (fromHost: string, remoteId: string) => Promise + export type LoadAndPopulateAuthor = (id: string) => Promise + export type LoadAndPopulateAuthorAndPodAndTags = (id: string) => Promise } export interface VideoClass { @@ -139,7 +127,7 @@ export interface VideoAttributes { dislikes?: number Author?: AuthorInstance - Tags?: VideoTagInstance[] + Tags?: TagInstance[] } export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance { @@ -157,6 +145,8 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In toAddRemoteJSON: VideoMethods.ToAddRemoteJSON toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON transcodeVideofile: VideoMethods.TranscodeVideofile + + setTags: Sequelize.HasManySetAssociationsMixin } export interface VideoModel extends VideoClass, Sequelize.Model {} diff --git a/server/models/video/video-tag.ts b/server/models/video/video-tag.ts index 71ca85332..ac45374f8 100644 --- a/server/models/video/video-tag.ts +++ b/server/models/video/video-tag.ts @@ -1,12 +1,8 @@ import * as Sequelize from 'sequelize' -import { addMethodsToModel } from '../utils' import { - VideoTagClass, VideoTagInstance, - VideoTagAttributes, - - VideoTagMethods + VideoTagAttributes } from './video-tag-interface' let VideoTag: Sequelize.Model diff --git a/server/models/video/video.ts b/server/models/video/video.ts index e66ebee2d..629051ae4 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -1,17 +1,15 @@ import * as safeBuffer from 'safe-buffer' const Buffer = safeBuffer.Buffer -import * as createTorrent from 'create-torrent' import * as ffmpeg from 'fluent-ffmpeg' -import * as fs from 'fs' import * as magnetUtil from 'magnet-uri' import { map, values } from 'lodash' -import { parallel, series } from 'async' import * as parseTorrent from 'parse-torrent' import { join } from 'path' import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' import { database as db } from '../../initializers/database' -import { VideoTagInstance } from './video-tag-interface' +import { TagInstance } from './tag-interface' import { logger, isVideoNameValid, @@ -21,7 +19,12 @@ import { isVideoNSFWValid, isVideoDescriptionValid, isVideoInfoHashValid, - isVideoDurationValid + isVideoDurationValid, + readFileBufferPromise, + unlinkPromise, + renamePromise, + writeFilePromise, + createTorrentPromise } from '../../helpers' import { CONSTRAINTS_FIELDS, @@ -37,7 +40,6 @@ import { JobScheduler, removeVideoToFriends } from '../../lib' import { addMethodsToModel, getSort } from '../utils' import { - VideoClass, VideoInstance, VideoAttributes, @@ -260,7 +262,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da toFormatedJSON, toAddRemoteJSON, toUpdateRemoteJSON, - transcodeVideofile, + transcodeVideofile ] addMethodsToModel(Video, classMethods, instanceMethods) @@ -276,91 +278,53 @@ function beforeValidate (video: VideoInstance) { } function beforeCreate (video: VideoInstance, options: { transaction: Sequelize.Transaction }) { - return new Promise(function (resolve, reject) { + if (video.isOwned()) { + const videoPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) const tasks = [] - if (video.isOwned()) { - const videoPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) - - tasks.push( - function createVideoTorrent (callback) { - createTorrentFromVideo(video, videoPath, callback) - }, - - function createVideoThumbnail (callback) { - createThumbnail(video, videoPath, callback) - }, - - function createVideoPreview (callback) { - createPreview(video, videoPath, callback) - } - ) - - if (CONFIG.TRANSCODING.ENABLED === true) { - tasks.push( - function createVideoTranscoderJob (callback) { - const dataInput = { - id: video.id - } + tasks.push( + createTorrentFromVideo(video, videoPath), + createThumbnail(video, videoPath), + createPreview(video, videoPath) + ) - JobScheduler.Instance.createJob(options.transaction, 'videoTranscoder', dataInput, callback) - } - ) + if (CONFIG.TRANSCODING.ENABLED === true) { + const dataInput = { + id: video.id } - return parallel(tasks, function (err) { - if (err) return reject(err) - - return resolve() - }) + tasks.push( + JobScheduler.Instance.createJob(options.transaction, 'videoTranscoder', dataInput) + ) } - return resolve() - }) + return Promise.all(tasks) + } + + return Promise.resolve() } function afterDestroy (video: VideoInstance) { - return new Promise(function (resolve, reject) { - const tasks = [] - - tasks.push( - function (callback) { - removeThumbnail(video, callback) - } - ) - - if (video.isOwned()) { - tasks.push( - function removeVideoFile (callback) { - removeFile(video, callback) - }, + const tasks = [] - function removeVideoTorrent (callback) { - removeTorrent(video, callback) - }, - - function removeVideoPreview (callback) { - removePreview(video, callback) - }, - - function notifyFriends (callback) { - const params = { - remoteId: video.id - } - - removeVideoToFriends(params) + tasks.push( + removeThumbnail(video) + ) - return callback() - } - ) + if (video.isOwned()) { + const removeVideoToFriendsParams = { + remoteId: video.id } - parallel(tasks, function (err) { - if (err) return reject(err) + tasks.push( + removeFile(video), + removeTorrent(video), + removePreview(video), + removeVideoToFriends(removeVideoToFriendsParams) + ) + } - return resolve() - }) - }) + return Promise.all(tasks) } // ------------------------------ METHODS ------------------------------ @@ -488,7 +452,7 @@ toFormatedJSON = function (this: VideoInstance) { views: this.views, likes: this.likes, dislikes: this.dislikes, - tags: map(this.Tags, 'name'), + tags: map(this.Tags, 'name'), thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), createdAt: this.createdAt, updatedAt: this.updatedAt @@ -497,15 +461,11 @@ toFormatedJSON = function (this: VideoInstance) { return json } -toAddRemoteJSON = function (this: VideoInstance, callback: VideoMethods.ToAddRemoteJSONCallback) { +toAddRemoteJSON = function (this: VideoInstance) { // Get thumbnail data to send to the other pod const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) - fs.readFile(thumbnailPath, (err, thumbnailData) => { - if (err) { - logger.error('Cannot read the thumbnail of the video') - return callback(err) - } + return readFileBufferPromise(thumbnailPath).then(thumbnailData => { const remoteVideo = { name: this.name, category: this.category, @@ -518,7 +478,7 @@ toAddRemoteJSON = function (this: VideoInstance, callback: VideoMethods.ToAddRem author: this.Author.name, duration: this.duration, thumbnailData: thumbnailData.toString('binary'), - tags: map(this.Tags, 'name'), + tags: map(this.Tags, 'name'), createdAt: this.createdAt, updatedAt: this.updatedAt, extname: this.extname, @@ -527,7 +487,7 @@ toAddRemoteJSON = function (this: VideoInstance, callback: VideoMethods.ToAddRem dislikes: this.dislikes } - return callback(null, remoteVideo) + return remoteVideo }) } @@ -543,7 +503,7 @@ toUpdateRemoteJSON = function (this: VideoInstance) { remoteId: this.id, author: this.Author.name, duration: this.duration, - tags: map(this.Tags, 'name'), + tags: map(this.Tags, 'name'), createdAt: this.createdAt, updatedAt: this.updatedAt, extname: this.extname, @@ -555,7 +515,7 @@ toUpdateRemoteJSON = function (this: VideoInstance) { return json } -transcodeVideofile = function (this: VideoInstance, finalCallback: VideoMethods.TranscodeVideofileCallback) { +transcodeVideofile = function (this: VideoInstance) { const video = this const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR @@ -563,78 +523,73 @@ transcodeVideofile = function (this: VideoInstance, finalCallback: VideoMethods. const videoInputPath = join(videosDirectory, video.getVideoFilename()) const videoOutputPath = join(videosDirectory, video.id + '-transcoded' + newExtname) - ffmpeg(videoInputPath) - .output(videoOutputPath) - .videoCodec('libx264') - .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) - .outputOption('-movflags faststart') - .on('error', finalCallback) - .on('end', function () { - series([ - function removeOldFile (callback) { - fs.unlink(videoInputPath, callback) - }, - - function moveNewFile (callback) { - // Important to do this before getVideoFilename() to take in account the new file extension - video.set('extname', newExtname) - - const newVideoPath = join(videosDirectory, video.getVideoFilename()) - fs.rename(videoOutputPath, newVideoPath, callback) - }, - - function torrent (callback) { - const newVideoPath = join(videosDirectory, video.getVideoFilename()) - createTorrentFromVideo(video, newVideoPath, callback) - }, - - function videoExtension (callback) { - video.save().asCallback(callback) - } - - ], function (err: Error) { - if (err) { - // Autodesctruction... - video.destroy().asCallback(function (err) { - if (err) logger.error('Cannot destruct video after transcoding failure.', { error: err }) + return new Promise((res, rej) => { + ffmpeg(videoInputPath) + .output(videoOutputPath) + .videoCodec('libx264') + .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) + .outputOption('-movflags faststart') + .on('error', rej) + .on('end', () => { + + return unlinkPromise(videoInputPath) + .then(() => { + // Important to do this before getVideoFilename() to take in account the new file extension + video.set('extname', newExtname) + + const newVideoPath = join(videosDirectory, video.getVideoFilename()) + return renamePromise(videoOutputPath, newVideoPath) }) + .then(() => { + const newVideoPath = join(videosDirectory, video.getVideoFilename()) + return createTorrentFromVideo(video, newVideoPath) + }) + .then(() => { + return video.save() + }) + .then(() => { + return res() + }) + .catch(err => { + // Autodesctruction... + video.destroy().asCallback(function (err) { + if (err) logger.error('Cannot destruct video after transcoding failure.', { error: err }) + }) - return finalCallback(err) - } - - return finalCallback(null) + return rej(err) + }) }) - }) - .run() + .run() + }) } // ------------------------------ STATICS ------------------------------ -generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string, callback: VideoMethods.GenerateThumbnailFromDataCallback) { +generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) { // Creating the thumbnail for a remote video const thumbnailName = video.getThumbnailName() const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) - fs.writeFile(thumbnailPath, Buffer.from(thumbnailData, 'binary'), function (err) { - if (err) return callback(err) - - return callback(null, thumbnailName) + return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => { + return thumbnailName }) } -getDurationFromFile = function (videoPath: string, callback: VideoMethods.GetDurationFromFileCallback) { - ffmpeg.ffprobe(videoPath, function (err, metadata) { - if (err) return callback(err) +getDurationFromFile = function (videoPath: string) { + return new Promise((res, rej) => { + ffmpeg.ffprobe(videoPath, function (err, metadata) { + if (err) return rej(err) - return callback(null, Math.floor(metadata.format.duration)) + return res(Math.floor(metadata.format.duration)) + }) }) } -list = function (callback: VideoMethods.ListCallback) { - return Video.findAll().asCallback(callback) +list = function () { + return Video.findAll() } -listForApi = function (start: number, count: number, sort: string, callback: VideoMethods.ListForApiCallback) { +listForApi = function (start: number, count: number, sort: string) { // Exclude Blakclisted videos from the list const query = { distinct: true, @@ -652,14 +607,15 @@ listForApi = function (start: number, count: number, sort: string, callback: Vid where: createBaseVideosWhere() } - return Video.findAndCountAll(query).asCallback(function (err, result) { - if (err) return callback(err) - - return callback(null, result.rows, result.count) + return Video.findAndCountAll(query).then(({ rows, count }) => { + return { + data: rows, + total: count + } }) } -loadByHostAndRemoteId = function (fromHost: string, remoteId: string, callback: VideoMethods.LoadByHostAndRemoteIdCallback) { +loadByHostAndRemoteId = function (fromHost: string, remoteId: string) { const query = { where: { remoteId: remoteId @@ -680,10 +636,10 @@ loadByHostAndRemoteId = function (fromHost: string, remoteId: string, callback: ] } - return Video.findOne(query).asCallback(callback) + return Video.findOne(query) } -listOwnedAndPopulateAuthorAndTags = function (callback: VideoMethods.ListOwnedAndPopulateAuthorAndTagsCallback) { +listOwnedAndPopulateAuthorAndTags = function () { // If remoteId is null this is *our* video const query = { where: { @@ -692,10 +648,10 @@ listOwnedAndPopulateAuthorAndTags = function (callback: VideoMethods.ListOwnedAn include: [ Video['sequelize'].models.Author, Video['sequelize'].models.Tag ] } - return Video.findAll(query).asCallback(callback) + return Video.findAll(query) } -listOwnedByAuthor = function (author: string, callback: VideoMethods.ListOwnedByAuthorCallback) { +listOwnedByAuthor = function (author: string) { const query = { where: { remoteId: null @@ -710,22 +666,22 @@ listOwnedByAuthor = function (author: string, callback: VideoMethods.ListOwnedBy ] } - return Video.findAll(query).asCallback(callback) + return Video.findAll(query) } -load = function (id: string, callback: VideoMethods.LoadCallback) { - return Video.findById(id).asCallback(callback) +load = function (id: string) { + return Video.findById(id) } -loadAndPopulateAuthor = function (id: string, callback: VideoMethods.LoadAndPopulateAuthorCallback) { +loadAndPopulateAuthor = function (id: string) { const options = { include: [ Video['sequelize'].models.Author ] } - return Video.findById(id, options).asCallback(callback) + return Video.findById(id, options) } -loadAndPopulateAuthorAndPodAndTags = function (id: string, callback: VideoMethods.LoadAndPopulateAuthorAndPodAndTagsCallback) { +loadAndPopulateAuthorAndPodAndTags = function (id: string) { const options = { include: [ { @@ -736,17 +692,10 @@ loadAndPopulateAuthorAndPodAndTags = function (id: string, callback: VideoMethod ] } - return Video.findById(id, options).asCallback(callback) + return Video.findById(id, options) } -searchAndPopulateAuthorAndPodAndTags = function ( - value: string, - field: string, - start: number, - count: number, - sort: string, - callback: VideoMethods.SearchAndPopulateAuthorAndPodAndTagsCallback -) { +searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) { const podInclude: any = { model: Video['sequelize'].models.Pod, required: false @@ -778,7 +727,11 @@ searchAndPopulateAuthorAndPodAndTags = function ( } else if (field === 'tags') { const escapedValue = Video['sequelize'].escape('%' + value + '%') query.where.id.$in = Video['sequelize'].literal( - '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')' + `(SELECT "VideoTags"."videoId" + FROM "Tags" + INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" + WHERE name LIKE ${escapedValue} + )` ) } else if (field === 'host') { // FIXME: Include our pod? (not stored in the database) @@ -810,10 +763,11 @@ searchAndPopulateAuthorAndPodAndTags = function ( // query.include.push([ Video['sequelize'].models.Tag ]) } - return Video.findAndCountAll(query).asCallback(function (err, result) { - if (err) return callback(err) - - return callback(null, result.rows, result.count) + return Video.findAndCountAll(query).then(({ rows, count }) => { + return { + data: rows, + total: count + } }) } @@ -829,27 +783,27 @@ function createBaseVideosWhere () { } } -function removeThumbnail (video: VideoInstance, callback: (err: Error) => void) { +function removeThumbnail (video: VideoInstance) { const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()) - fs.unlink(thumbnailPath, callback) + return unlinkPromise(thumbnailPath) } -function removeFile (video: VideoInstance, callback: (err: Error) => void) { +function removeFile (video: VideoInstance) { const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) - fs.unlink(filePath, callback) + return unlinkPromise(filePath) } -function removeTorrent (video: VideoInstance, callback: (err: Error) => void) { +function removeTorrent (video: VideoInstance) { const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) - fs.unlink(torrenPath, callback) + return unlinkPromise(torrenPath) } -function removePreview (video: VideoInstance, callback: (err: Error) => void) { +function removePreview (video: VideoInstance) { // Same name than video thumnail - fs.unlink(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName(), callback) + return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName()) } -function createTorrentFromVideo (video: VideoInstance, videoPath: string, callback: (err: Error) => void) { +function createTorrentFromVideo (video: VideoInstance, videoPath: string) { const options = { announceList: [ [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] @@ -859,30 +813,27 @@ function createTorrentFromVideo (video: VideoInstance, videoPath: string, callba ] } - createTorrent(videoPath, options, function (err, torrent) { - if (err) return callback(err) - - const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) - fs.writeFile(filePath, torrent, function (err) { - if (err) return callback(err) - + return createTorrentPromise(videoPath, options) + .then(torrent => { + const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) + return writeFilePromise(filePath, torrent).then(() => torrent) + }) + .then(torrent => { const parsedTorrent = parseTorrent(torrent) video.set('infoHash', parsedTorrent.infoHash) - video.validate().asCallback(callback) + return video.validate() }) - }) } -function createPreview (video: VideoInstance, videoPath: string, callback: (err: Error) => void) { - generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null, callback) +function createPreview (video: VideoInstance, videoPath: string) { + return generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null) } -function createThumbnail (video: VideoInstance, videoPath: string, callback: (err: Error) => void) { - generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE, callback) +function createThumbnail (video: VideoInstance, videoPath: string) { + return generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE) } -type GenerateImageCallback = (err: Error, imageName: string) => void -function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string, callback?: GenerateImageCallback) { +function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) { const options: any = { filename: imageName, count: 1, @@ -893,29 +844,25 @@ function generateImage (video: VideoInstance, videoPath: string, folder: string, options.size = size } - ffmpeg(videoPath) - .on('error', callback) - .on('end', function () { - callback(null, imageName) - }) - .thumbnail(options) + return new Promise((res, rej) => { + ffmpeg(videoPath) + .on('error', rej) + .on('end', function () { + return res(imageName) + }) + .thumbnail(options) + }) } -function removeFromBlacklist (video: VideoInstance, callback: (err: Error) => void) { +function removeFromBlacklist (video: VideoInstance) { // Find the blacklisted video - db.BlacklistedVideo.loadByVideoId(video.id, function (err, video) { - // If an error occured, stop here - if (err) { - logger.error('Error when fetching video from blacklist.', { error: err }) - return callback(err) + return db.BlacklistedVideo.loadByVideoId(video.id).then(video => { + // Not found the video, skip + if (!video) { + return null } // If we found the video, remove it from the blacklist - if (video) { - video.destroy().asCallback(callback) - } else { - // If haven't found it, simply ignore it and do nothing - return callback(null) - } + return video.destroy() }) } diff --git a/server/tests/api/friends-advanced.js b/server/tests/api/friends-advanced.js index 237cb533d..917583a42 100644 --- a/server/tests/api/friends-advanced.js +++ b/server/tests/api/friends-advanced.js @@ -171,6 +171,23 @@ describe('Test advanced friends', function () { function (next) { setTimeout(next, 22000) }, + // Check the pods 1, 2 expulsed pod 4 + function (next) { + each([ 1, 2 ], function (i, callback) { + getFriendsList(i, function (err, res) { + if (err) throw err + + // Pod 4 should not be our friend + const result = res.body.data + expect(result.length).to.equal(2) + for (const pod of result) { + expect(pod.host).not.equal(servers[3].host) + } + + callback() + }) + }, next) + }, // Rerun server 4 function (next) { serversUtils.runServer(4, function (server) { @@ -187,7 +204,7 @@ describe('Test advanced friends', function () { next() }) }, - // Pod 6 ask pod 1, 2 and 3 + // Pod 6 asks pod 1, 2 and 3 function (next) { makeFriends(6, next) }, -- cgit v1.2.3