aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-07-05 13:26:25 +0200
committerChocobozzz <florian.bigard@gmail.com>2017-07-05 14:14:16 +0200
commit6fcd19ba737f1f5614a56c6925adb882dea43b8d (patch)
tree3365a96d82bc7f00ae504a568725c8e914150cf8 /server
parent5fe7e898316e18369c3e1aba307b55077adc7bfb (diff)
downloadPeerTube-6fcd19ba737f1f5614a56c6925adb882dea43b8d.tar.gz
PeerTube-6fcd19ba737f1f5614a56c6925adb882dea43b8d.tar.zst
PeerTube-6fcd19ba737f1f5614a56c6925adb882dea43b8d.zip
Move to promises
Closes https://github.com/Chocobozzz/PeerTube/issues/74
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/oauth-clients.ts21
-rw-r--r--server/controllers/api/pods.ts71
-rw-r--r--server/controllers/api/remote/pods.ts20
-rw-r--r--server/controllers/api/remote/videos.ts528
-rw-r--r--server/controllers/api/request-schedulers.ts38
-rw-r--r--server/controllers/api/users.ts86
-rw-r--r--server/controllers/api/videos/abuse.ts78
-rw-r--r--server/controllers/api/videos/blacklist.ts10
-rw-r--r--server/controllers/api/videos/index.ts381
-rw-r--r--server/controllers/api/videos/rate.ts207
-rw-r--r--server/controllers/client.ts26
-rw-r--r--server/helpers/core-utils.ts89
-rw-r--r--server/helpers/database-utils.ts69
-rw-r--r--server/helpers/peertube-crypto.ts111
-rw-r--r--server/helpers/requests.ts81
-rw-r--r--server/helpers/utils.ts24
-rw-r--r--server/initializers/checker.ts52
-rw-r--r--server/initializers/database.ts99
-rw-r--r--server/initializers/installer.ts75
-rw-r--r--server/initializers/migrations/0005-email-pod.ts38
-rw-r--r--server/initializers/migrations/0010-email-user.ts39
-rw-r--r--server/initializers/migrations/0015-video-views.ts13
-rw-r--r--server/initializers/migrations/0020-video-likes.ts13
-rw-r--r--server/initializers/migrations/0025-video-dislikes.ts13
-rw-r--r--server/initializers/migrations/0030-video-category.ts29
-rw-r--r--server/initializers/migrations/0035-video-licence.ts30
-rw-r--r--server/initializers/migrations/0040-video-nsfw.ts29
-rw-r--r--server/initializers/migrations/0045-user-display-nsfw.ts13
-rw-r--r--server/initializers/migrations/0050-video-language.ts13
-rw-r--r--server/initializers/migrator.ts133
-rw-r--r--server/lib/friends.ts318
-rw-r--r--server/lib/jobs/handlers/index.ts8
-rw-r--r--server/lib/jobs/handlers/video-transcoder.ts22
-rw-r--r--server/lib/jobs/job-scheduler.ts117
-rw-r--r--server/lib/oauth-model.ts15
-rw-r--r--server/lib/request/abstract-request-scheduler.ts124
-rw-r--r--server/lib/request/request-scheduler.ts32
-rw-r--r--server/lib/request/request-video-event-scheduler.ts13
-rw-r--r--server/lib/request/request-video-qadu-scheduler.ts13
-rw-r--r--server/middlewares/secure.ts56
-rw-r--r--server/middlewares/validators/pods.ts44
-rw-r--r--server/middlewares/validators/users.ts52
-rw-r--r--server/middlewares/validators/videos.ts75
-rw-r--r--server/models/application/application-interface.ts10
-rw-r--r--server/models/application/application.ts13
-rw-r--r--server/models/job/job-interface.ts4
-rw-r--r--server/models/job/job.ts5
-rw-r--r--server/models/oauth/oauth-client-interface.ts9
-rw-r--r--server/models/oauth/oauth-client.ts9
-rw-r--r--server/models/oauth/oauth-token-interface.ts11
-rw-r--r--server/models/oauth/oauth-token.ts5
-rw-r--r--server/models/pod/pod-interface.ts28
-rw-r--r--server/models/pod/pod.ts115
-rw-r--r--server/models/request/abstract-request-interface.ts12
-rw-r--r--server/models/request/index.ts1
-rw-r--r--server/models/request/request-interface.ts16
-rw-r--r--server/models/request/request-to-pod-interface.ts8
-rw-r--r--server/models/request/request-to-pod.ts7
-rw-r--r--server/models/request/request-video-event-interface.ts22
-rw-r--r--server/models/request/request-video-event.ts27
-rw-r--r--server/models/request/request-video-qadu-interface.ts22
-rw-r--r--server/models/request/request-video-qadu.ts27
-rw-r--r--server/models/request/request.ts28
-rw-r--r--server/models/user/user-interface.ts26
-rw-r--r--server/models/user/user-video-rate-interface.ts4
-rw-r--r--server/models/user/user-video-rate.ts5
-rw-r--r--server/models/user/user.ts47
-rw-r--r--server/models/video/author-interface.ts9
-rw-r--r--server/models/video/author.ts23
-rw-r--r--server/models/video/tag-interface.ts4
-rw-r--r--server/models/video/tag.ts24
-rw-r--r--server/models/video/video-abuse-interface.ts5
-rw-r--r--server/models/video/video-abuse.ts11
-rw-r--r--server/models/video/video-blacklist-interface.ts24
-rw-r--r--server/models/video/video-blacklist.ts28
-rw-r--r--server/models/video/video-interface.ts70
-rw-r--r--server/models/video/video-tag.ts6
-rw-r--r--server/models/video/video.ts369
-rw-r--r--server/tests/api/friends-advanced.js19
79 files changed, 1926 insertions, 2445 deletions
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
24 return res.type('json').status(403).end() 24 return res.type('json').status(403).end()
25 } 25 }
26 26
27 db.OAuthClient.loadFirstClient(function (err, client) { 27 db.OAuthClient.loadFirstClient()
28 if (err) return next(err) 28 .then(client => {
29 if (!client) return next(new Error('No client available.')) 29 if (!client) throw new Error('No client available.')
30 30
31 const json: OAuthClientLocal = { 31 const json: OAuthClientLocal = {
32 client_id: client.clientId, 32 client_id: client.clientId,
33 client_secret: client.clientSecret 33 client_secret: client.clientSecret
34 } 34 }
35 res.json(json) 35 res.json(json)
36 }) 36 })
37 .catch(err => next(err))
37} 38}
38 39
39// --------------------------------------------------------------------------- 40// ---------------------------------------------------------------------------
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { waterfall } from 'async'
3 2
4import { database as db } from '../../initializers/database' 3import { database as db } from '../../initializers/database'
5import { CONFIG } from '../../initializers' 4import { CONFIG } from '../../initializers'
@@ -57,65 +56,39 @@ export {
57function addPods (req: express.Request, res: express.Response, next: express.NextFunction) { 56function addPods (req: express.Request, res: express.Response, next: express.NextFunction) {
58 const informations = req.body 57 const informations = req.body
59 58
60 waterfall<string, Error>([ 59 const pod = db.Pod.build(informations)
61 function addPod (callback) { 60 pod.save()
62 const pod = db.Pod.build(informations) 61 .then(podCreated => {
63 pod.save().asCallback(function (err, podCreated) { 62 return sendOwnedVideosToPod(podCreated.id)
64 // Be sure about the number of parameters for the callback 63 })
65 return callback(err, podCreated) 64 .then(() => {
66 }) 65 return getMyPublicCert()
67 }, 66 })
68 67 .then(cert => {
69 function sendMyVideos (podCreated: PodInstance, callback) { 68 return res.json({ cert: cert, email: CONFIG.ADMIN.EMAIL })
70 sendOwnedVideosToPod(podCreated.id) 69 })
71 70 .catch(err => next(err))
72 callback(null)
73 },
74
75 function fetchMyCertificate (callback) {
76 getMyPublicCert(function (err, cert) {
77 if (err) {
78 logger.error('Cannot read cert file.')
79 return callback(err)
80 }
81
82 return callback(null, cert)
83 })
84 }
85 ], function (err, cert) {
86 if (err) return next(err)
87
88 return res.json({ cert: cert, email: CONFIG.ADMIN.EMAIL })
89 })
90} 71}
91 72
92function listPods (req: express.Request, res: express.Response, next: express.NextFunction) { 73function listPods (req: express.Request, res: express.Response, next: express.NextFunction) {
93 db.Pod.list(function (err, podsList) { 74 db.Pod.list()
94 if (err) return next(err) 75 .then(podsList => res.json(getFormatedObjects(podsList, podsList.length)))
95 76 .catch(err => next(err))
96 res.json(getFormatedObjects(podsList, podsList.length))
97 })
98} 77}
99 78
100function makeFriendsController (req: express.Request, res: express.Response, next: express.NextFunction) { 79function makeFriendsController (req: express.Request, res: express.Response, next: express.NextFunction) {
101 const hosts = req.body.hosts as string[] 80 const hosts = req.body.hosts as string[]
102 81
103 makeFriends(hosts, function (err) { 82 makeFriends(hosts)
104 if (err) { 83 .then(() => logger.info('Made friends!'))
105 logger.error('Could not make friends.', { error: err }) 84 .catch(err => logger.error('Could not make friends.', { error: err }))
106 return
107 }
108
109 logger.info('Made friends!')
110 })
111 85
86 // Don't wait the process that could be long
112 res.type('json').status(204).end() 87 res.type('json').status(204).end()
113} 88}
114 89
115function quitFriendsController (req: express.Request, res: express.Response, next: express.NextFunction) { 90function quitFriendsController (req: express.Request, res: express.Response, next: express.NextFunction) {
116 quitFriends(function (err) { 91 quitFriends()
117 if (err) return next(err) 92 .then(() => res.type('json').status(204).end())
118 93 .catch(err => next(err))
119 res.type('json').status(204).end()
120 })
121} 94}
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as waterfall from 'async/waterfall'
3 2
4import { database as db } from '../../../initializers/database' 3import { database as db } from '../../../initializers/database'
5import { checkSignature, signatureValidator } from '../../../middlewares' 4import { checkSignature, signatureValidator } from '../../../middlewares'
@@ -24,17 +23,10 @@ export {
24function removePods (req: express.Request, res: express.Response, next: express.NextFunction) { 23function removePods (req: express.Request, res: express.Response, next: express.NextFunction) {
25 const host = req.body.signature.host 24 const host = req.body.signature.host
26 25
27 waterfall([ 26 db.Pod.loadByHost(host)
28 function loadPod (callback) { 27 .then(pod => {
29 db.Pod.loadByHost(host, callback) 28 return pod.destroy()
30 }, 29 })
31 30 .then(() => res.type('json').status(204).end())
32 function deletePod (pod, callback) { 31 .catch(err => next(err))
33 pod.destroy().asCallback(callback)
34 }
35 ], function (err) {
36 if (err) return next(err)
37
38 return res.type('json').status(204).end()
39 })
40} 32}
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as Sequelize from 'sequelize' 2import * as Promise from 'bluebird'
3import { eachSeries, waterfall } from 'async'
4 3
5import { database as db } from '../../../initializers/database' 4import { database as db } from '../../../initializers/database'
6import { 5import {
@@ -16,20 +15,14 @@ import {
16 remoteQaduVideosValidator, 15 remoteQaduVideosValidator,
17 remoteEventsVideosValidator 16 remoteEventsVideosValidator
18} from '../../../middlewares' 17} from '../../../middlewares'
19import { 18import { logger, retryTransactionWrapper } from '../../../helpers'
20 logger,
21 commitTransaction,
22 retryTransactionWrapper,
23 rollbackTransaction,
24 startSerializableTransaction
25} from '../../../helpers'
26import { quickAndDirtyUpdatesVideoToFriends } from '../../../lib' 19import { quickAndDirtyUpdatesVideoToFriends } from '../../../lib'
27import { PodInstance, VideoInstance } from '../../../models' 20import { PodInstance, VideoInstance } from '../../../models'
28 21
29const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] 22const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
30 23
31// Functions to call when processing a remote request 24// Functions to call when processing a remote request
32const functionsHash = {} 25const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {}
33functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper 26functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper
34functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper 27functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper
35functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo 28functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo
@@ -72,20 +65,19 @@ function remoteVideos (req: express.Request, res: express.Response, next: expres
72 65
73 // We need to process in the same order to keep consistency 66 // We need to process in the same order to keep consistency
74 // TODO: optimization 67 // TODO: optimization
75 eachSeries(requests, function (request: any, callbackEach) { 68 Promise.mapSeries(requests, (request: any) => {
76 const data = request.data 69 const data = request.data
77 70
78 // Get the function we need to call in order to process the request 71 // Get the function we need to call in order to process the request
79 const fun = functionsHash[request.type] 72 const fun = functionsHash[request.type]
80 if (fun === undefined) { 73 if (fun === undefined) {
81 logger.error('Unkown remote request type %s.', request.type) 74 logger.error('Unkown remote request type %s.', request.type)
82 return callbackEach(null) 75 return
83 } 76 }
84 77
85 fun.call(this, data, fromPod, callbackEach) 78 return fun.call(this, data, fromPod)
86 }, function (err) {
87 if (err) logger.error('Error managing remote videos.', { error: err })
88 }) 79 })
80 .catch(err => logger.error('Error managing remote videos.', { error: err }))
89 81
90 // We don't need to keep the other pod waiting 82 // We don't need to keep the other pod waiting
91 return res.type('json').status(204).end() 83 return res.type('json').status(204).end()
@@ -95,13 +87,12 @@ function remoteVideosQadu (req: express.Request, res: express.Response, next: ex
95 const requests = req.body.data 87 const requests = req.body.data
96 const fromPod = res.locals.secure.pod 88 const fromPod = res.locals.secure.pod
97 89
98 eachSeries(requests, function (request: any, callbackEach) { 90 Promise.mapSeries(requests, (request: any) => {
99 const videoData = request.data 91 const videoData = request.data
100 92
101 quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod, callbackEach) 93 return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod)
102 }, function (err) {
103 if (err) logger.error('Error managing remote videos.', { error: err })
104 }) 94 })
95 .catch(err => logger.error('Error managing remote videos.', { error: err }))
105 96
106 return res.type('json').status(204).end() 97 return res.type('json').status(204).end()
107} 98}
@@ -110,414 +101,303 @@ function remoteVideosEvents (req: express.Request, res: express.Response, next:
110 const requests = req.body.data 101 const requests = req.body.data
111 const fromPod = res.locals.secure.pod 102 const fromPod = res.locals.secure.pod
112 103
113 eachSeries(requests, function (request: any, callbackEach) { 104 Promise.mapSeries(requests, (request: any) => {
114 const eventData = request.data 105 const eventData = request.data
115 106
116 processVideosEventsRetryWrapper(eventData, fromPod, callbackEach) 107 return processVideosEventsRetryWrapper(eventData, fromPod)
117 }, function (err) {
118 if (err) logger.error('Error managing remote videos.', { error: err })
119 }) 108 })
109 .catch(err => logger.error('Error managing remote videos.', { error: err }))
120 110
121 return res.type('json').status(204).end() 111 return res.type('json').status(204).end()
122} 112}
123 113
124function processVideosEventsRetryWrapper (eventData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { 114function processVideosEventsRetryWrapper (eventData: any, fromPod: PodInstance) {
125 const options = { 115 const options = {
126 arguments: [ eventData, fromPod ], 116 arguments: [ eventData, fromPod ],
127 errorMessage: 'Cannot process videos events with many retries.' 117 errorMessage: 'Cannot process videos events with many retries.'
128 } 118 }
129 119
130 retryTransactionWrapper(processVideosEvents, options, finalCallback) 120 return retryTransactionWrapper(processVideosEvents, options)
131} 121}
132 122
133function processVideosEvents (eventData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { 123function processVideosEvents (eventData: any, fromPod: PodInstance) {
134 waterfall([
135 startSerializableTransaction,
136
137 function findVideo (t, callback) {
138 fetchOwnedVideo(eventData.remoteId, function (err, videoInstance) {
139 return callback(err, t, videoInstance)
140 })
141 },
142 124
143 function updateVideoIntoDB (t, videoInstance, callback) { 125 return db.sequelize.transaction(t => {
144 const options = { transaction: t } 126 return fetchOwnedVideo(eventData.remoteId)
127 .then(videoInstance => {
128 const options = { transaction: t }
145 129
146 let columnToUpdate 130 let columnToUpdate
147 let qaduType 131 let qaduType
148 132
149 switch (eventData.eventType) { 133 switch (eventData.eventType) {
150 case REQUEST_VIDEO_EVENT_TYPES.VIEWS: 134 case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
151 columnToUpdate = 'views' 135 columnToUpdate = 'views'
152 qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS 136 qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
153 break 137 break
154 138
155 case REQUEST_VIDEO_EVENT_TYPES.LIKES: 139 case REQUEST_VIDEO_EVENT_TYPES.LIKES:
156 columnToUpdate = 'likes' 140 columnToUpdate = 'likes'
157 qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES 141 qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
158 break 142 break
159 143
160 case REQUEST_VIDEO_EVENT_TYPES.DISLIKES: 144 case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
161 columnToUpdate = 'dislikes' 145 columnToUpdate = 'dislikes'
162 qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES 146 qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
163 break 147 break
164 148
165 default: 149 default:
166 return callback(new Error('Unknown video event type.')) 150 throw new Error('Unknown video event type.')
167 } 151 }
168 152
169 const query = {} 153 const query = {}
170 query[columnToUpdate] = eventData.count 154 query[columnToUpdate] = eventData.count
171 155
172 videoInstance.increment(query, options).asCallback(function (err) { 156 return videoInstance.increment(query, options).then(() => ({ videoInstance, qaduType }))
173 return callback(err, t, videoInstance, qaduType)
174 }) 157 })
175 }, 158 .then(({ videoInstance, qaduType }) => {
176 159 const qadusParams = [
177 function sendQaduToFriends (t, videoInstance, qaduType, callback) { 160 {
178 const qadusParams = [ 161 videoId: videoInstance.id,
179 { 162 type: qaduType
180 videoId: videoInstance.id, 163 }
181 type: qaduType 164 ]
182 } 165
183 ] 166 return quickAndDirtyUpdatesVideoToFriends(qadusParams, t)
184
185 quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
186 return callback(err, t)
187 }) 167 })
188 }, 168 })
189 169 .then(() => logger.info('Remote video event processed for video %s.', eventData.remoteId))
190 commitTransaction 170 .catch(err => {
191 171 logger.debug('Cannot process a video event.', { error: err })
192 ], function (err: Error, t: Sequelize.Transaction) { 172 throw err
193 if (err) {
194 logger.debug('Cannot process a video event.', { error: err })
195 return rollbackTransaction(err, t, finalCallback)
196 }
197
198 logger.info('Remote video event processed for video %s.', eventData.remoteId)
199 return finalCallback(null)
200 }) 173 })
201} 174}
202 175
203function quickAndDirtyUpdateVideoRetryWrapper (videoData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { 176function quickAndDirtyUpdateVideoRetryWrapper (videoData: any, fromPod: PodInstance) {
204 const options = { 177 const options = {
205 arguments: [ videoData, fromPod ], 178 arguments: [ videoData, fromPod ],
206 errorMessage: 'Cannot update quick and dirty the remote video with many retries.' 179 errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
207 } 180 }
208 181
209 retryTransactionWrapper(quickAndDirtyUpdateVideo, options, finalCallback) 182 return retryTransactionWrapper(quickAndDirtyUpdateVideo, options)
210} 183}
211 184
212function quickAndDirtyUpdateVideo (videoData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { 185function quickAndDirtyUpdateVideo (videoData: any, fromPod: PodInstance) {
213 let videoName 186 let videoName
214 187
215 waterfall([ 188 return db.sequelize.transaction(t => {
216 startSerializableTransaction, 189 return fetchRemoteVideo(fromPod.host, videoData.remoteId)
217 190 .then(videoInstance => {
218 function findVideo (t, callback) { 191 const options = { transaction: t }
219 fetchRemoteVideo(fromPod.host, videoData.remoteId, function (err, videoInstance) {
220 return callback(err, t, videoInstance)
221 })
222 },
223
224 function updateVideoIntoDB (t, videoInstance, callback) {
225 const options = { transaction: t }
226 192
227 videoName = videoInstance.name 193 videoName = videoInstance.name
228 194
229 if (videoData.views) { 195 if (videoData.views) {
230 videoInstance.set('views', videoData.views) 196 videoInstance.set('views', videoData.views)
231 } 197 }
232 198
233 if (videoData.likes) { 199 if (videoData.likes) {
234 videoInstance.set('likes', videoData.likes) 200 videoInstance.set('likes', videoData.likes)
235 } 201 }
236 202
237 if (videoData.dislikes) { 203 if (videoData.dislikes) {
238 videoInstance.set('dislikes', videoData.dislikes) 204 videoInstance.set('dislikes', videoData.dislikes)
239 } 205 }
240 206
241 videoInstance.save(options).asCallback(function (err) { 207 return videoInstance.save(options)
242 return callback(err, t)
243 }) 208 })
244 },
245
246 commitTransaction
247
248 ], function (err: Error, t: Sequelize.Transaction) {
249 if (err) {
250 logger.debug('Cannot quick and dirty update the remote video.', { error: err })
251 return rollbackTransaction(err, t, finalCallback)
252 }
253
254 logger.info('Remote video %s quick and dirty updated', videoName)
255 return finalCallback(null)
256 }) 209 })
210 .then(() => logger.info('Remote video %s quick and dirty updated', videoName))
211 .catch(err => logger.debug('Cannot quick and dirty update the remote video.', { error: err }))
257} 212}
258 213
259// Handle retries on fail 214// Handle retries on fail
260function addRemoteVideoRetryWrapper (videoToCreateData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { 215function addRemoteVideoRetryWrapper (videoToCreateData: any, fromPod: PodInstance) {
261 const options = { 216 const options = {
262 arguments: [ videoToCreateData, fromPod ], 217 arguments: [ videoToCreateData, fromPod ],
263 errorMessage: 'Cannot insert the remote video with many retries.' 218 errorMessage: 'Cannot insert the remote video with many retries.'
264 } 219 }
265 220
266 retryTransactionWrapper(addRemoteVideo, options, finalCallback) 221 return retryTransactionWrapper(addRemoteVideo, options)
267} 222}
268 223
269function addRemoteVideo (videoToCreateData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { 224function addRemoteVideo (videoToCreateData: any, fromPod: PodInstance) {
270 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId) 225 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
271 226
272 waterfall([ 227 return db.sequelize.transaction(t => {
273 228 return db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId)
274 startSerializableTransaction, 229 .then(video => {
275 230 if (video) throw new Error('RemoteId and host pair is not unique.')
276 function assertRemoteIdAndHostUnique (t, callback) {
277 db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId, function (err, video) {
278 if (err) return callback(err)
279 231
280 if (video) return callback(new Error('RemoteId and host pair is not unique.')) 232 return undefined
281
282 return callback(null, t)
283 }) 233 })
284 }, 234 .then(() => {
285 235 const name = videoToCreateData.author
286 function findOrCreateAuthor (t, callback) { 236 const podId = fromPod.id
287 const name = videoToCreateData.author 237 // This author is from another pod so we do not associate a user
288 const podId = fromPod.id 238 const userId = null
289 // This author is from another pod so we do not associate a user
290 const userId = null
291 239
292 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) { 240 return db.Author.findOrCreateAuthor(name, podId, userId, t)
293 return callback(err, t, authorInstance)
294 }) 241 })
295 }, 242 .then(author => {
243 const tags = videoToCreateData.tags
296 244
297 function findOrCreateTags (t, author, callback) { 245 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances }))
298 const tags = videoToCreateData.tags
299
300 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
301 return callback(err, t, author, tagInstances)
302 }) 246 })
303 }, 247 .then(({ author, tagInstances }) => {
304 248 const videoData = {
305 function createVideoObject (t, author, tagInstances, callback) { 249 name: videoToCreateData.name,
306 const videoData = { 250 remoteId: videoToCreateData.remoteId,
307 name: videoToCreateData.name, 251 extname: videoToCreateData.extname,
308 remoteId: videoToCreateData.remoteId, 252 infoHash: videoToCreateData.infoHash,
309 extname: videoToCreateData.extname, 253 category: videoToCreateData.category,
310 infoHash: videoToCreateData.infoHash, 254 licence: videoToCreateData.licence,
311 category: videoToCreateData.category, 255 language: videoToCreateData.language,
312 licence: videoToCreateData.licence, 256 nsfw: videoToCreateData.nsfw,
313 language: videoToCreateData.language, 257 description: videoToCreateData.description,
314 nsfw: videoToCreateData.nsfw, 258 authorId: author.id,
315 description: videoToCreateData.description, 259 duration: videoToCreateData.duration,
316 authorId: author.id, 260 createdAt: videoToCreateData.createdAt,
317 duration: videoToCreateData.duration, 261 // FIXME: updatedAt does not seems to be considered by Sequelize
318 createdAt: videoToCreateData.createdAt, 262 updatedAt: videoToCreateData.updatedAt,
319 // FIXME: updatedAt does not seems to be considered by Sequelize 263 views: videoToCreateData.views,
320 updatedAt: videoToCreateData.updatedAt, 264 likes: videoToCreateData.likes,
321 views: videoToCreateData.views, 265 dislikes: videoToCreateData.dislikes
322 likes: videoToCreateData.likes,
323 dislikes: videoToCreateData.dislikes
324 }
325
326 const video = db.Video.build(videoData)
327
328 return callback(null, t, tagInstances, video)
329 },
330
331 function generateThumbnail (t, tagInstances, video, callback) {
332 db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) {
333 if (err) {
334 logger.error('Cannot generate thumbnail from data.', { error: err })
335 return callback(err)
336 } 266 }
337 267
338 return callback(err, t, tagInstances, video) 268 const video = db.Video.build(videoData)
269 return { tagInstances, video }
339 }) 270 })
340 }, 271 .then(({ tagInstances, video }) => {
341 272 return db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData).then(() => ({ tagInstances, video }))
342 function insertVideoIntoDB (t, tagInstances, video, callback) {
343 const options = {
344 transaction: t
345 }
346
347 video.save(options).asCallback(function (err, videoCreated) {
348 return callback(err, t, tagInstances, videoCreated)
349 }) 273 })
350 }, 274 .then(({ tagInstances, video }) => {
351 275 const options = {
352 function associateTagsToVideo (t, tagInstances, video, callback) { 276 transaction: t
353 const options = { 277 }
354 transaction: t
355 }
356 278
357 video.setTags(tagInstances, options).asCallback(function (err) { 279 return video.save(options).then(videoCreated => ({ tagInstances, videoCreated }))
358 return callback(err, t)
359 }) 280 })
360 }, 281 .then(({ tagInstances, videoCreated }) => {
361 282 const options = {
362 commitTransaction 283 transaction: t
363 284 }
364 ], function (err: Error, t: Sequelize.Transaction) {
365 if (err) {
366 // This is just a debug because we will retry the insert
367 logger.debug('Cannot insert the remote video.', { error: err })
368 return rollbackTransaction(err, t, finalCallback)
369 }
370 285
371 logger.info('Remote video %s inserted.', videoToCreateData.name) 286 return videoCreated.setTags(tagInstances, options)
372 return finalCallback(null) 287 })
288 })
289 .then(() => logger.info('Remote video %s inserted.', videoToCreateData.name))
290 .catch(err => {
291 logger.debug('Cannot insert the remote video.', { error: err })
292 throw err
373 }) 293 })
374} 294}
375 295
376// Handle retries on fail 296// Handle retries on fail
377function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { 297function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: any, fromPod: PodInstance) {
378 const options = { 298 const options = {
379 arguments: [ videoAttributesToUpdate, fromPod ], 299 arguments: [ videoAttributesToUpdate, fromPod ],
380 errorMessage: 'Cannot update the remote video with many retries' 300 errorMessage: 'Cannot update the remote video with many retries'
381 } 301 }
382 302
383 retryTransactionWrapper(updateRemoteVideo, options, finalCallback) 303 return retryTransactionWrapper(updateRemoteVideo, options)
384} 304}
385 305
386function updateRemoteVideo (videoAttributesToUpdate: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { 306function updateRemoteVideo (videoAttributesToUpdate: any, fromPod: PodInstance) {
387 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId) 307 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
388 308
389 waterfall([ 309 return db.sequelize.transaction(t => {
310 return fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId)
311 .then(videoInstance => {
312 const tags = videoAttributesToUpdate.tags
390 313
391 startSerializableTransaction, 314 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoInstance, tagInstances }))
392
393 function findVideo (t, callback) {
394 fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
395 return callback(err, t, videoInstance)
396 }) 315 })
397 }, 316 .then(({ videoInstance, tagInstances }) => {
398 317 const options = { transaction: t }
399 function findOrCreateTags (t, videoInstance, callback) { 318
400 const tags = videoAttributesToUpdate.tags 319 videoInstance.set('name', videoAttributesToUpdate.name)
401 320 videoInstance.set('category', videoAttributesToUpdate.category)
402 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { 321 videoInstance.set('licence', videoAttributesToUpdate.licence)
403 return callback(err, t, videoInstance, tagInstances) 322 videoInstance.set('language', videoAttributesToUpdate.language)
404 }) 323 videoInstance.set('nsfw', videoAttributesToUpdate.nsfw)
405 }, 324 videoInstance.set('description', videoAttributesToUpdate.description)
406 325 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
407 function updateVideoIntoDB (t, videoInstance, tagInstances, callback) { 326 videoInstance.set('duration', videoAttributesToUpdate.duration)
408 const options = { transaction: t } 327 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
409 328 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
410 videoInstance.set('name', videoAttributesToUpdate.name) 329 videoInstance.set('extname', videoAttributesToUpdate.extname)
411 videoInstance.set('category', videoAttributesToUpdate.category) 330 videoInstance.set('views', videoAttributesToUpdate.views)
412 videoInstance.set('licence', videoAttributesToUpdate.licence) 331 videoInstance.set('likes', videoAttributesToUpdate.likes)
413 videoInstance.set('language', videoAttributesToUpdate.language) 332 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
414 videoInstance.set('nsfw', videoAttributesToUpdate.nsfw) 333
415 videoInstance.set('description', videoAttributesToUpdate.description) 334 return videoInstance.save(options).then(() => ({ videoInstance, tagInstances }))
416 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
417 videoInstance.set('duration', videoAttributesToUpdate.duration)
418 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
419 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
420 videoInstance.set('extname', videoAttributesToUpdate.extname)
421 videoInstance.set('views', videoAttributesToUpdate.views)
422 videoInstance.set('likes', videoAttributesToUpdate.likes)
423 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
424
425 videoInstance.save(options).asCallback(function (err) {
426 return callback(err, t, videoInstance, tagInstances)
427 }) 335 })
428 }, 336 .then(({ videoInstance, tagInstances }) => {
429 337 const options = { transaction: t }
430 function associateTagsToVideo (t, videoInstance, tagInstances, callback) {
431 const options = { transaction: t }
432 338
433 videoInstance.setTags(tagInstances, options).asCallback(function (err) { 339 return videoInstance.setTags(tagInstances, options)
434 return callback(err, t)
435 }) 340 })
436 }, 341 })
437 342 .then(() => logger.info('Remote video %s updated', videoAttributesToUpdate.name))
438 commitTransaction 343 .catch(err => {
439 344 // This is just a debug because we will retry the insert
440 ], function (err: Error, t: Sequelize.Transaction) { 345 logger.debug('Cannot update the remote video.', { error: err })
441 if (err) { 346 throw err
442 // This is just a debug because we will retry the insert
443 logger.debug('Cannot update the remote video.', { error: err })
444 return rollbackTransaction(err, t, finalCallback)
445 }
446
447 logger.info('Remote video %s updated', videoAttributesToUpdate.name)
448 return finalCallback(null)
449 }) 347 })
450} 348}
451 349
452function removeRemoteVideo (videoToRemoveData: any, fromPod: PodInstance, callback: (err: Error) => void) { 350function removeRemoteVideo (videoToRemoveData: any, fromPod: PodInstance) {
453 // We need the instance because we have to remove some other stuffs (thumbnail etc) 351 // We need the instance because we have to remove some other stuffs (thumbnail etc)
454 fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) { 352 return fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId)
455 // Do not return the error, continue the process 353 .then(video => {
456 if (err) return callback(null) 354 logger.debug('Removing remote video %s.', video.remoteId)
457 355 return video.destroy()
458 logger.debug('Removing remote video %s.', video.remoteId) 356 })
459 video.destroy().asCallback(function (err) { 357 .catch(err => {
460 // Do not return the error, continue the process 358 logger.debug('Could not fetch remote video.', { host: fromPod.host, remoteId: videoToRemoveData.remoteId, error: err })
461 if (err) {
462 logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err })
463 }
464
465 return callback(null)
466 }) 359 })
467 })
468} 360}
469 361
470function reportAbuseRemoteVideo (reportData: any, fromPod: PodInstance, callback: (err: Error) => void) { 362function reportAbuseRemoteVideo (reportData: any, fromPod: PodInstance) {
471 fetchOwnedVideo(reportData.videoRemoteId, function (err, video) { 363 return fetchOwnedVideo(reportData.videoRemoteId)
472 if (err || !video) { 364 .then(video => {
473 if (!err) err = new Error('video not found') 365 logger.debug('Reporting remote abuse for video %s.', video.id)
474
475 logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId })
476 // Do not return the error, continue the process
477 return callback(null)
478 }
479
480 logger.debug('Reporting remote abuse for video %s.', video.id)
481
482 const videoAbuseData = {
483 reporterUsername: reportData.reporterUsername,
484 reason: reportData.reportReason,
485 reporterPodId: fromPod.id,
486 videoId: video.id
487 }
488 366
489 db.VideoAbuse.create(videoAbuseData).asCallback(function (err) { 367 const videoAbuseData = {
490 if (err) { 368 reporterUsername: reportData.reporterUsername,
491 logger.error('Cannot create remote abuse video.', { error: err }) 369 reason: reportData.reportReason,
370 reporterPodId: fromPod.id,
371 videoId: video.id
492 } 372 }
493 373
494 return callback(null) 374 return db.VideoAbuse.create(videoAbuseData)
495 }) 375 })
496 }) 376 .catch(err => logger.error('Cannot create remote abuse video.', { error: err }))
497} 377}
498 378
499function fetchOwnedVideo (id: string, callback: (err: Error, video?: VideoInstance) => void) { 379function fetchOwnedVideo (id: string) {
500 db.Video.load(id, function (err, video) { 380 return db.Video.load(id)
501 if (err || !video) { 381 .then(video => {
502 if (!err) err = new Error('video not found') 382 if (!video) throw new Error('Video not found')
503 383
384 return video
385 })
386 .catch(err => {
504 logger.error('Cannot load owned video from id.', { error: err, id }) 387 logger.error('Cannot load owned video from id.', { error: err, id })
505 return callback(err) 388 throw err
506 } 389 })
507
508 return callback(null, video)
509 })
510} 390}
511 391
512function fetchRemoteVideo (podHost: string, remoteId: string, callback: (err: Error, video?: VideoInstance) => void) { 392function fetchRemoteVideo (podHost: string, remoteId: string) {
513 db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) { 393 return db.Video.loadByHostAndRemoteId(podHost, remoteId)
514 if (err || !video) { 394 .then(video => {
515 if (!err) err = new Error('video not found') 395 if (!video) throw new Error('Video not found')
516 396
397 return video
398 })
399 .catch(err => {
517 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId }) 400 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
518 return callback(err) 401 throw err
519 } 402 })
520
521 return callback(null, video)
522 })
523} 403}
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { parallel } from 'async' 2import * as Promise from 'bluebird'
3 3
4import { 4import {
5 AbstractRequestScheduler, 5 AbstractRequestScheduler,
@@ -27,33 +27,27 @@ export {
27// --------------------------------------------------------------------------- 27// ---------------------------------------------------------------------------
28 28
29function getRequestSchedulersStats (req: express.Request, res: express.Response, next: express.NextFunction) { 29function getRequestSchedulersStats (req: express.Request, res: express.Response, next: express.NextFunction) {
30 parallel({ 30 Promise.props({
31 requestScheduler: buildRequestSchedulerStats(getRequestScheduler()), 31 requestScheduler: buildRequestSchedulerStats(getRequestScheduler()),
32 requestVideoQaduScheduler: buildRequestSchedulerStats(getRequestVideoQaduScheduler()), 32 requestVideoQaduScheduler: buildRequestSchedulerStats(getRequestVideoQaduScheduler()),
33 requestVideoEventScheduler: buildRequestSchedulerStats(getRequestVideoEventScheduler()) 33 requestVideoEventScheduler: buildRequestSchedulerStats(getRequestVideoEventScheduler())
34 }, function (err, result) {
35 if (err) return next(err)
36
37 return res.json(result)
38 }) 34 })
35 .then(result => res.json(result))
36 .catch(err => next(err))
39} 37}
40 38
41// --------------------------------------------------------------------------- 39// ---------------------------------------------------------------------------
42 40
43function buildRequestSchedulerStats (requestScheduler: AbstractRequestScheduler) { 41function buildRequestSchedulerStats (requestScheduler: AbstractRequestScheduler<any>) {
44 return function (callback) { 42 return requestScheduler.remainingRequestsCount().then(count => {
45 requestScheduler.remainingRequestsCount(function (err, count) { 43 const result: RequestSchedulerStatsAttributes = {
46 if (err) return callback(err) 44 totalRequests: count,
47 45 requestsLimitPods: requestScheduler.limitPods,
48 const result: RequestSchedulerStatsAttributes = { 46 requestsLimitPerPod: requestScheduler.limitPerPod,
49 totalRequests: count, 47 remainingMilliSeconds: requestScheduler.remainingMilliSeconds(),
50 requestsLimitPods: requestScheduler.limitPods, 48 milliSecondsInterval: requestScheduler.requestInterval
51 requestsLimitPerPod: requestScheduler.limitPerPod, 49 }
52 remainingMilliSeconds: requestScheduler.remainingMilliSeconds(), 50
53 milliSecondsInterval: requestScheduler.requestInterval 51 return result
54 } 52 })
55
56 return callback(null, result)
57 })
58 }
59} 53}
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { waterfall } from 'async'
3 2
4import { database as db } from '../../initializers/database' 3import { database as db } from '../../initializers/database'
5import { CONFIG, USER_ROLES } from '../../initializers' 4import { USER_ROLES } from '../../initializers'
6import { logger, getFormatedObjects } from '../../helpers' 5import { logger, getFormatedObjects } from '../../helpers'
7import { 6import {
8 authenticate, 7 authenticate,
@@ -87,78 +86,61 @@ function createUser (req: express.Request, res: express.Response, next: express.
87 role: USER_ROLES.USER 86 role: USER_ROLES.USER
88 }) 87 })
89 88
90 user.save().asCallback(function (err) { 89 user.save()
91 if (err) return next(err) 90 .then(() => res.type('json').status(204).end())
92 91 .catch(err => next(err))
93 return res.type('json').status(204).end()
94 })
95} 92}
96 93
97function getUserInformation (req: express.Request, res: express.Response, next: express.NextFunction) { 94function getUserInformation (req: express.Request, res: express.Response, next: express.NextFunction) {
98 db.User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) { 95 db.User.loadByUsername(res.locals.oauth.token.user.username)
99 if (err) return next(err) 96 .then(user => res.json(user.toFormatedJSON()))
100 97 .catch(err => next(err))
101 return res.json(user.toFormatedJSON())
102 })
103} 98}
104 99
105function getUserVideoRating (req: express.Request, res: express.Response, next: express.NextFunction) { 100function getUserVideoRating (req: express.Request, res: express.Response, next: express.NextFunction) {
106 const videoId = '' + req.params.videoId 101 const videoId = '' + req.params.videoId
107 const userId = +res.locals.oauth.token.User.id 102 const userId = +res.locals.oauth.token.User.id
108 103
109 db.UserVideoRate.load(userId, videoId, null, function (err, ratingObj) { 104 db.UserVideoRate.load(userId, videoId, null)
110 if (err) return next(err) 105 .then(ratingObj => {
111 106 const rating = ratingObj ? ratingObj.type : 'none'
112 const rating = ratingObj ? ratingObj.type : 'none' 107 const json: FormatedUserVideoRate = {
113 108 videoId,
114 const json: FormatedUserVideoRate = { 109 rating
115 videoId, 110 }
116 rating 111 res.json(json)
117 } 112 })
118 res.json(json) 113 .catch(err => next(err))
119 })
120} 114}
121 115
122function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { 116function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) {
123 db.User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) { 117 db.User.listForApi(req.query.start, req.query.count, req.query.sort)
124 if (err) return next(err) 118 .then(resultList => {
125 119 res.json(getFormatedObjects(resultList.data, resultList.total))
126 res.json(getFormatedObjects(usersList, usersTotal)) 120 })
127 }) 121 .catch(err => next(err))
128} 122}
129 123
130function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) { 124function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) {
131 waterfall([ 125 db.User.loadById(req.params.id)
132 function loadUser (callback) { 126 .then(user => user.destroy())
133 db.User.loadById(req.params.id, callback) 127 .then(() => res.sendStatus(204))
134 }, 128 .catch(err => {
135
136 function deleteUser (user, callback) {
137 user.destroy().asCallback(callback)
138 }
139 ], function andFinally (err) {
140 if (err) {
141 logger.error('Errors when removed the user.', { error: err }) 129 logger.error('Errors when removed the user.', { error: err })
142 return next(err) 130 return next(err)
143 } 131 })
144
145 return res.sendStatus(204)
146 })
147} 132}
148 133
149function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { 134function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) {
150 db.User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) { 135 db.User.loadByUsername(res.locals.oauth.token.user.username)
151 if (err) return next(err) 136 .then(user => {
152 137 if (req.body.password) user.password = req.body.password
153 if (req.body.password) user.password = req.body.password 138 if (req.body.displayNSFW !== undefined) user.displayNSFW = req.body.displayNSFW
154 if (req.body.displayNSFW !== undefined) user.displayNSFW = req.body.displayNSFW
155 139
156 user.save().asCallback(function (err) { 140 return user.save()
157 if (err) return next(err)
158
159 return res.sendStatus(204)
160 }) 141 })
161 }) 142 .then(() => res.sendStatus(204))
143 .catch(err => next(err))
162} 144}
163 145
164function success (req: express.Request, res: express.Response, next: express.NextFunction) { 146function 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 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as Sequelize from 'sequelize'
3import { waterfall } from 'async'
4 2
5import { database as db } from '../../../initializers/database' 3import { database as db } from '../../../initializers/database'
6import * as friends from '../../../lib/friends' 4import * as friends from '../../../lib/friends'
7import { 5import {
8 logger, 6 logger,
9 getFormatedObjects, 7 getFormatedObjects,
10 retryTransactionWrapper, 8 retryTransactionWrapper
11 startSerializableTransaction,
12 commitTransaction,
13 rollbackTransaction
14} from '../../../helpers' 9} from '../../../helpers'
15import { 10import {
16 authenticate, 11 authenticate,
@@ -21,6 +16,7 @@ import {
21 setVideoAbusesSort, 16 setVideoAbusesSort,
22 setPagination 17 setPagination
23} from '../../../middlewares' 18} from '../../../middlewares'
19import { VideoInstance } from '../../../models'
24 20
25const abuseVideoRouter = express.Router() 21const abuseVideoRouter = express.Router()
26 22
@@ -48,11 +44,9 @@ export {
48// --------------------------------------------------------------------------- 44// ---------------------------------------------------------------------------
49 45
50function listVideoAbuses (req: express.Request, res: express.Response, next: express.NextFunction) { 46function listVideoAbuses (req: express.Request, res: express.Response, next: express.NextFunction) {
51 db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) { 47 db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort)
52 if (err) return next(err) 48 .then(result => res.json(getFormatedObjects(result.data, result.total)))
53 49 .catch(err => next(err))
54 res.json(getFormatedObjects(abusesList, abusesTotal))
55 })
56} 50}
57 51
58function reportVideoAbuseRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { 52function reportVideoAbuseRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
@@ -61,14 +55,12 @@ function reportVideoAbuseRetryWrapper (req: express.Request, res: express.Respon
61 errorMessage: 'Cannot report abuse to the video with many retries.' 55 errorMessage: 'Cannot report abuse to the video with many retries.'
62 } 56 }
63 57
64 retryTransactionWrapper(reportVideoAbuse, options, function (err) { 58 retryTransactionWrapper(reportVideoAbuse, options)
65 if (err) return next(err) 59 .then(() => res.type('json').status(204).end())
66 60 .catch(err => next(err))
67 return res.type('json').status(204).end()
68 })
69} 61}
70 62
71function reportVideoAbuse (req: express.Request, res: express.Response, finalCallback: (err: Error) => void) { 63function reportVideoAbuse (req: express.Request, res: express.Response) {
72 const videoInstance = res.locals.video 64 const videoInstance = res.locals.video
73 const reporterUsername = res.locals.oauth.token.User.username 65 const reporterUsername = res.locals.oauth.token.User.username
74 66
@@ -79,40 +71,26 @@ function reportVideoAbuse (req: express.Request, res: express.Response, finalCal
79 reporterPodId: null // This is our pod that reported this abuse 71 reporterPodId: null // This is our pod that reported this abuse
80 } 72 }
81 73
82 waterfall([ 74 return db.sequelize.transaction(t => {
83 75 return db.VideoAbuse.create(abuse, { transaction: t })
84 startSerializableTransaction, 76 .then(abuse => {
85 77 // We send the information to the destination pod
86 function createAbuse (t, callback) { 78 if (videoInstance.isOwned() === false) {
87 db.VideoAbuse.create(abuse).asCallback(function (err, abuse) { 79 const reportData = {
88 return callback(err, t, abuse) 80 reporterUsername,
89 }) 81 reportReason: abuse.reason,
90 }, 82 videoRemoteId: videoInstance.remoteId
91 83 }
92 function sendToFriendsIfNeeded (t, abuse, callback) { 84
93 // We send the information to the destination pod 85 return friends.reportAbuseVideoToFriend(reportData, videoInstance, t).then(() => videoInstance)
94 if (videoInstance.isOwned() === false) {
95 const reportData = {
96 reporterUsername,
97 reportReason: abuse.reason,
98 videoRemoteId: videoInstance.remoteId
99 } 86 }
100 87
101 friends.reportAbuseVideoToFriend(reportData, videoInstance) 88 return videoInstance
102 } 89 })
103 90 })
104 return callback(null, t) 91 .then((videoInstance: VideoInstance) => logger.info('Abuse report for video %s created.', videoInstance.name))
105 }, 92 .catch(err => {
106 93 logger.debug('Cannot update the video.', { error: err })
107 commitTransaction 94 throw err
108
109 ], function andFinally (err: Error, t: Sequelize.Transaction) {
110 if (err) {
111 logger.debug('Cannot update the video.', { error: err })
112 return rollbackTransaction(err, t, finalCallback)
113 }
114
115 logger.info('Abuse report for video %s created.', videoInstance.name)
116 return finalCallback(null)
117 }) 95 })
118} 96}
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:
32 videoId: videoInstance.id 32 videoId: videoInstance.id
33 } 33 }
34 34
35 db.BlacklistedVideo.create(toCreate).asCallback(function (err) { 35 db.BlacklistedVideo.create(toCreate)
36 if (err) { 36 .then(() => res.type('json').status(204).end())
37 .catch(err => {
37 logger.error('Errors when blacklisting video ', { error: err }) 38 logger.error('Errors when blacklisting video ', { error: err })
38 return next(err) 39 return next(err)
39 } 40 })
40
41 return res.type('json').status(204).end()
42 })
43} 41}
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as Sequelize from 'sequelize' 2import * as Promise from 'bluebird'
3import * as fs from 'fs'
4import * as multer from 'multer' 3import * as multer from 'multer'
5import * as path from 'path' 4import * as path from 'path'
6import { waterfall } from 'async'
7 5
8import { database as db } from '../../../initializers/database' 6import { database as db } from '../../../initializers/database'
9import { 7import {
@@ -35,13 +33,12 @@ import {
35} from '../../../middlewares' 33} from '../../../middlewares'
36import { 34import {
37 logger, 35 logger,
38 commitTransaction,
39 retryTransactionWrapper, 36 retryTransactionWrapper,
40 rollbackTransaction,
41 startSerializableTransaction,
42 generateRandomString, 37 generateRandomString,
43 getFormatedObjects 38 getFormatedObjects,
39 renamePromise
44} from '../../../helpers' 40} from '../../../helpers'
41import { TagInstance } from '../../../models'
45 42
46import { abuseVideoRouter } from './abuse' 43import { abuseVideoRouter } from './abuse'
47import { blacklistRouter } from './blacklist' 44import { blacklistRouter } from './blacklist'
@@ -60,10 +57,15 @@ const storage = multer.diskStorage({
60 if (file.mimetype === 'video/webm') extension = 'webm' 57 if (file.mimetype === 'video/webm') extension = 'webm'
61 else if (file.mimetype === 'video/mp4') extension = 'mp4' 58 else if (file.mimetype === 'video/mp4') extension = 'mp4'
62 else if (file.mimetype === 'video/ogg') extension = 'ogv' 59 else if (file.mimetype === 'video/ogg') extension = 'ogv'
63 generateRandomString(16, function (err, randomString) { 60 generateRandomString(16)
64 const fieldname = err ? undefined : randomString 61 .then(randomString => {
65 cb(null, fieldname + '.' + extension) 62 const filename = randomString
66 }) 63 cb(null, filename + '.' + extension)
64 })
65 .catch(err => {
66 logger.error('Cannot generate random string for file name.', { error: err })
67 throw err
68 })
67 } 69 }
68}) 70})
69 71
@@ -144,125 +146,97 @@ function addVideoRetryWrapper (req: express.Request, res: express.Response, next
144 errorMessage: 'Cannot insert the video with many retries.' 146 errorMessage: 'Cannot insert the video with many retries.'
145 } 147 }
146 148
147 retryTransactionWrapper(addVideo, options, function (err) { 149 retryTransactionWrapper(addVideo, options)
148 if (err) return next(err) 150 .then(() => {
149 151 // TODO : include Location of the new video -> 201
150 // TODO : include Location of the new video -> 201 152 res.type('json').status(204).end()
151 return res.type('json').status(204).end() 153 })
152 }) 154 .catch(err => next(err))
153} 155}
154 156
155function addVideo (req: express.Request, res: express.Response, videoFile: Express.Multer.File, finalCallback: (err: Error) => void) { 157function addVideo (req: express.Request, res: express.Response, videoFile: Express.Multer.File) {
156 const videoInfos = req.body 158 const videoInfos = req.body
157 159
158 waterfall([ 160 return db.sequelize.transaction(t => {
159 161 const user = res.locals.oauth.token.User
160 startSerializableTransaction,
161 162
162 function findOrCreateAuthor (t, callback) { 163 const name = user.username
163 const user = res.locals.oauth.token.User 164 // null because it is OUR pod
165 const podId = null
166 const userId = user.id
164 167
165 const name = user.username 168 return db.Author.findOrCreateAuthor(name, podId, userId, t)
166 // null because it is OUR pod 169 .then(author => {
167 const podId = null 170 const tags = videoInfos.tags
168 const userId = user.id 171 if (!tags) return { author, tagInstances: undefined }
169 172
170 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) { 173 return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances }))
171 return callback(err, t, authorInstance)
172 }) 174 })
173 }, 175 .then(({ author, tagInstances }) => {
174 176 const videoData = {
175 function findOrCreateTags (t, author, callback) { 177 name: videoInfos.name,
176 const tags = videoInfos.tags 178 remoteId: null,
177 179 extname: path.extname(videoFile.filename),
178 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { 180 category: videoInfos.category,
179 return callback(err, t, author, tagInstances) 181 licence: videoInfos.licence,
182 language: videoInfos.language,
183 nsfw: videoInfos.nsfw,
184 description: videoInfos.description,
185 duration: videoFile['duration'], // duration was added by a previous middleware
186 authorId: author.id
187 }
188
189 const video = db.Video.build(videoData)
190 return { author, tagInstances, video }
180 }) 191 })
181 }, 192 .then(({ author, tagInstances, video }) => {
182 193 const videoDir = CONFIG.STORAGE.VIDEOS_DIR
183 function createVideoObject (t, author, tagInstances, callback) { 194 const source = path.join(videoDir, videoFile.filename)
184 const videoData = { 195 const destination = path.join(videoDir, video.getVideoFilename())
185 name: videoInfos.name, 196
186 remoteId: null, 197 return renamePromise(source, destination)
187 extname: path.extname(videoFile.filename), 198 .then(() => {
188 category: videoInfos.category, 199 // This is important in case if there is another attempt in the retry process
189 licence: videoInfos.licence, 200 videoFile.filename = video.getVideoFilename()
190 language: videoInfos.language, 201 return { author, tagInstances, video }
191 nsfw: videoInfos.nsfw, 202 })
192 description: videoInfos.description,
193 duration: videoFile['duration'], // duration was added by a previous middleware
194 authorId: author.id
195 }
196
197 const video = db.Video.build(videoData)
198
199 return callback(null, t, author, tagInstances, video)
200 },
201
202 // Set the videoname the same as the id
203 function renameVideoFile (t, author, tagInstances, video, callback) {
204 const videoDir = CONFIG.STORAGE.VIDEOS_DIR
205 const source = path.join(videoDir, videoFile.filename)
206 const destination = path.join(videoDir, video.getVideoFilename())
207
208 fs.rename(source, destination, function (err) {
209 if (err) return callback(err)
210
211 // This is important in case if there is another attempt
212 videoFile.filename = video.getVideoFilename()
213 return callback(null, t, author, tagInstances, video)
214 }) 203 })
215 }, 204 .then(({ author, tagInstances, video }) => {
216 205 const options = { transaction: t }
217 function insertVideoIntoDB (t, author, tagInstances, video, callback) {
218 const options = { transaction: t }
219
220 // Add tags association
221 video.save(options).asCallback(function (err, videoCreated) {
222 if (err) return callback(err)
223 206
224 // Do not forget to add Author informations to the created video 207 return video.save(options)
225 videoCreated.Author = author 208 .then(videoCreated => {
209 // Do not forget to add Author informations to the created video
210 videoCreated.Author = author
226 211
227 return callback(err, t, tagInstances, videoCreated) 212 return { tagInstances, video: videoCreated }
213 })
228 }) 214 })
229 }, 215 .then(({ tagInstances, video }) => {
230 216 if (!tagInstances) return video
231 function associateTagsToVideo (t, tagInstances, video, callback) {
232 const options = { transaction: t }
233 217
234 video.setTags(tagInstances, options).asCallback(function (err) { 218 const options = { transaction: t }
235 video.Tags = tagInstances 219 return video.setTags(tagInstances, options)
236 220 .then(() => {
237 return callback(err, t, video) 221 video.Tags = tagInstances
222 return video
223 })
238 }) 224 })
239 }, 225 .then(video => {
240 226 // Let transcoding job send the video to friends because the videofile extension might change
241 function sendToFriends (t, video, callback) { 227 if (CONFIG.TRANSCODING.ENABLED === true) return undefined
242 // Let transcoding job send the video to friends because the videofile extension might change 228
243 if (CONFIG.TRANSCODING.ENABLED === true) return callback(null, t) 229 return video.toAddRemoteJSON()
244 230 .then(remoteVideo => {
245 video.toAddRemoteJSON(function (err, remoteVideo) { 231 // Now we'll add the video's meta data to our friends
246 if (err) return callback(err) 232 return addVideoToFriends(remoteVideo, t)
247 233 })
248 // Now we'll add the video's meta data to our friends
249 addVideoToFriends(remoteVideo, t, function (err) {
250 return callback(err, t)
251 })
252 }) 234 })
253 }, 235 })
254 236 .then(() => logger.info('Video with name %s created.', videoInfos.name))
255 commitTransaction 237 .catch((err: Error) => {
256 238 logger.debug('Cannot insert the video.', { error: err.stack })
257 ], function andFinally (err: Error, t: Sequelize.Transaction) { 239 throw err
258 if (err) {
259 // This is just a debug because we will retry the insert
260 logger.debug('Cannot insert the video.', { error: err })
261 return rollbackTransaction(err, t, finalCallback)
262 }
263
264 logger.info('Video with name %s created.', videoInfos.name)
265 return finalCallback(null)
266 }) 240 })
267} 241}
268 242
@@ -272,92 +246,75 @@ function updateVideoRetryWrapper (req: express.Request, res: express.Response, n
272 errorMessage: 'Cannot update the video with many retries.' 246 errorMessage: 'Cannot update the video with many retries.'
273 } 247 }
274 248
275 retryTransactionWrapper(updateVideo, options, function (err) { 249 retryTransactionWrapper(updateVideo, options)
276 if (err) return next(err) 250 .then(() => {
277 251 // TODO : include Location of the new video -> 201
278 // TODO : include Location of the new video -> 201 252 return res.type('json').status(204).end()
279 return res.type('json').status(204).end() 253 })
280 }) 254 .catch(err => next(err))
281} 255}
282 256
283function updateVideo (req: express.Request, res: express.Response, finalCallback: (err: Error) => void) { 257function updateVideo (req: express.Request, res: express.Response) {
284 const videoInstance = res.locals.video 258 const videoInstance = res.locals.video
285 const videoFieldsSave = videoInstance.toJSON() 259 const videoFieldsSave = videoInstance.toJSON()
286 const videoInfosToUpdate = req.body 260 const videoInfosToUpdate = req.body
287 261
288 waterfall([ 262 return db.sequelize.transaction(t => {
289 263 let tagsPromise: Promise<TagInstance[]>
290 startSerializableTransaction, 264 if (!videoInfosToUpdate.tags) {
291 265 tagsPromise = Promise.resolve(null)
292 function findOrCreateTags (t, callback) { 266 } else {
293 if (videoInfosToUpdate.tags) { 267 tagsPromise = db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t)
294 db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) { 268 }
295 return callback(err, t, tagInstances)
296 })
297 } else {
298 return callback(null, t, null)
299 }
300 },
301
302 function updateVideoIntoDB (t, tagInstances, callback) {
303 const options = {
304 transaction: t
305 }
306
307 if (videoInfosToUpdate.name !== undefined) videoInstance.set('name', videoInfosToUpdate.name)
308 if (videoInfosToUpdate.category !== undefined) videoInstance.set('category', videoInfosToUpdate.category)
309 if (videoInfosToUpdate.licence !== undefined) videoInstance.set('licence', videoInfosToUpdate.licence)
310 if (videoInfosToUpdate.language !== undefined) videoInstance.set('language', videoInfosToUpdate.language)
311 if (videoInfosToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfosToUpdate.nsfw)
312 if (videoInfosToUpdate.description !== undefined) videoInstance.set('description', videoInfosToUpdate.description)
313
314 videoInstance.save(options).asCallback(function (err) {
315 return callback(err, t, tagInstances)
316 })
317 },
318
319 function associateTagsToVideo (t, tagInstances, callback) {
320 if (tagInstances) {
321 const options = { transaction: t }
322
323 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
324 videoInstance.Tags = tagInstances
325 269
326 return callback(err, t) 270 return tagsPromise
327 }) 271 .then(tagInstances => {
328 } else { 272 const options = {
329 return callback(null, t) 273 transaction: t
330 } 274 }
331 },
332 275
333 function sendToFriends (t, callback) { 276 if (videoInfosToUpdate.name !== undefined) videoInstance.set('name', videoInfosToUpdate.name)
334 const json = videoInstance.toUpdateRemoteJSON() 277 if (videoInfosToUpdate.category !== undefined) videoInstance.set('category', videoInfosToUpdate.category)
278 if (videoInfosToUpdate.licence !== undefined) videoInstance.set('licence', videoInfosToUpdate.licence)
279 if (videoInfosToUpdate.language !== undefined) videoInstance.set('language', videoInfosToUpdate.language)
280 if (videoInfosToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfosToUpdate.nsfw)
281 if (videoInfosToUpdate.description !== undefined) videoInstance.set('description', videoInfosToUpdate.description)
335 282
336 // Now we'll update the video's meta data to our friends 283 return videoInstance.save(options).then(() => tagInstances)
337 updateVideoToFriends(json, t, function (err) {
338 return callback(err, t)
339 }) 284 })
340 }, 285 .then(tagInstances => {
341 286 if (!tagInstances) return
342 commitTransaction
343 287
344 ], function andFinally (err: Error, t: Sequelize.Transaction) { 288 const options = { transaction: t }
345 if (err) { 289 return videoInstance.setTags(tagInstances, options)
346 logger.debug('Cannot update the video.', { error: err }) 290 .then(() => {
291 videoInstance.Tags = tagInstances
347 292
348 // Force fields we want to update 293 return
349 // If the transaction is retried, sequelize will think the object has not changed 294 })
350 // So it will skip the SQL request, even if the last one was ROLLBACKed!
351 Object.keys(videoFieldsSave).forEach(function (key) {
352 const value = videoFieldsSave[key]
353 videoInstance.set(key, value)
354 }) 295 })
296 .then(() => {
297 const json = videoInstance.toUpdateRemoteJSON()
355 298
356 return rollbackTransaction(err, t, finalCallback) 299 // Now we'll update the video's meta data to our friends
357 } 300 return updateVideoToFriends(json, t)
358 301 })
302 })
303 .then(() => {
359 logger.info('Video with name %s updated.', videoInstance.name) 304 logger.info('Video with name %s updated.', videoInstance.name)
360 return finalCallback(null) 305 })
306 .catch(err => {
307 logger.debug('Cannot update the video.', { error: err })
308
309 // Force fields we want to update
310 // If the transaction is retried, sequelize will think the object has not changed
311 // So it will skip the SQL request, even if the last one was ROLLBACKed!
312 Object.keys(videoFieldsSave).forEach(function (key) {
313 const value = videoFieldsSave[key]
314 videoInstance.set(key, value)
315 })
316
317 throw err
361 }) 318 })
362} 319}
363 320
@@ -366,20 +323,17 @@ function getVideo (req: express.Request, res: express.Response, next: express.Ne
366 323
367 if (videoInstance.isOwned()) { 324 if (videoInstance.isOwned()) {
368 // The increment is done directly in the database, not using the instance value 325 // The increment is done directly in the database, not using the instance value
369 videoInstance.increment('views').asCallback(function (err) { 326 videoInstance.increment('views')
370 if (err) { 327 .then(() => {
371 logger.error('Cannot add view to video %d.', videoInstance.id) 328 // FIXME: make a real view system
372 return 329 // For example, only add a view when a user watch a video during 30s etc
373 } 330 const qaduParams = {
374 331 videoId: videoInstance.id,
375 // FIXME: make a real view system 332 type: REQUEST_VIDEO_QADU_TYPES.VIEWS
376 // For example, only add a view when a user watch a video during 30s etc 333 }
377 const qaduParams = { 334 return quickAndDirtyUpdateVideoToFriends(qaduParams)
378 videoId: videoInstance.id, 335 })
379 type: REQUEST_VIDEO_QADU_TYPES.VIEWS 336 .catch(err => logger.error('Cannot add view to video %d.', videoInstance.id, { error: err }))
380 }
381 quickAndDirtyUpdateVideoToFriends(qaduParams)
382 })
383 } else { 337 } else {
384 // Just send the event to our friends 338 // Just send the event to our friends
385 const eventParams = { 339 const eventParams = {
@@ -394,33 +348,24 @@ function getVideo (req: express.Request, res: express.Response, next: express.Ne
394} 348}
395 349
396function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 350function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
397 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) { 351 db.Video.listForApi(req.query.start, req.query.count, req.query.sort)
398 if (err) return next(err) 352 .then(result => res.json(getFormatedObjects(result.data, result.total)))
399 353 .catch(err => next(err))
400 res.json(getFormatedObjects(videosList, videosTotal))
401 })
402} 354}
403 355
404function removeVideo (req: express.Request, res: express.Response, next: express.NextFunction) { 356function removeVideo (req: express.Request, res: express.Response, next: express.NextFunction) {
405 const videoInstance = res.locals.video 357 const videoInstance = res.locals.video
406 358
407 videoInstance.destroy().asCallback(function (err) { 359 videoInstance.destroy()
408 if (err) { 360 .then(() => res.type('json').status(204).end())
361 .catch(err => {
409 logger.error('Errors when removed the video.', { error: err }) 362 logger.error('Errors when removed the video.', { error: err })
410 return next(err) 363 return next(err)
411 } 364 })
412
413 return res.type('json').status(204).end()
414 })
415} 365}
416 366
417function searchVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 367function searchVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
418 db.Video.searchAndPopulateAuthorAndPodAndTags( 368 db.Video.searchAndPopulateAuthorAndPodAndTags(req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort)
419 req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort, 369 .then(result => res.json(getFormatedObjects(result.data, result.total)))
420 function (err, videosList, videosTotal) { 370 .catch(err => next(err))
421 if (err) return next(err)
422
423 res.json(getFormatedObjects(videosList, videosTotal))
424 }
425 )
426} 371}
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as Sequelize from 'sequelize'
3import { waterfall } from 'async'
4 2
5import { database as db } from '../../../initializers/database' 3import { database as db } from '../../../initializers/database'
6import { 4import {
7 logger, 5 logger,
8 retryTransactionWrapper, 6 retryTransactionWrapper
9 startSerializableTransaction,
10 commitTransaction,
11 rollbackTransaction
12} from '../../../helpers' 7} from '../../../helpers'
13import { 8import {
14 VIDEO_RATE_TYPES, 9 VIDEO_RATE_TYPES,
@@ -46,137 +41,109 @@ function rateVideoRetryWrapper (req: express.Request, res: express.Response, nex
46 errorMessage: 'Cannot update the user video rate.' 41 errorMessage: 'Cannot update the user video rate.'
47 } 42 }
48 43
49 retryTransactionWrapper(rateVideo, options, function (err) { 44 retryTransactionWrapper(rateVideo, options)
50 if (err) return next(err) 45 .then(() => res.type('json').status(204).end())
51 46 .catch(err => next(err))
52 return res.type('json').status(204).end()
53 })
54} 47}
55 48
56function rateVideo (req: express.Request, res: express.Response, finalCallback: (err: Error) => void) { 49function rateVideo (req: express.Request, res: express.Response) {
57 const rateType = req.body.rating 50 const rateType = req.body.rating
58 const videoInstance = res.locals.video 51 const videoInstance = res.locals.video
59 const userInstance = res.locals.oauth.token.User 52 const userInstance = res.locals.oauth.token.User
60 53
61 waterfall([ 54 return db.sequelize.transaction(t => {
62 startSerializableTransaction, 55 return db.UserVideoRate.load(userInstance.id, videoInstance.id, t)
63 56 .then(previousRate => {
64 function findPreviousRate (t, callback) { 57 const options = { transaction: t }
65 db.UserVideoRate.load(userInstance.id, videoInstance.id, t, function (err, previousRate) {
66 return callback(err, t, previousRate)
67 })
68 },
69 58
70 function insertUserRateIntoDB (t, previousRate, callback) { 59 let likesToIncrement = 0
71 const options = { transaction: t } 60 let dislikesToIncrement = 0
72 61
73 let likesToIncrement = 0 62 if (rateType === VIDEO_RATE_TYPES.LIKE) likesToIncrement++
74 let dislikesToIncrement = 0 63 else if (rateType === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++
75 64
76 if (rateType === VIDEO_RATE_TYPES.LIKE) likesToIncrement++ 65 // There was a previous rate, update it
77 else if (rateType === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++ 66 if (previousRate) {
67 // We will remove the previous rate, so we will need to remove it from the video attribute
68 if (previousRate.type === VIDEO_RATE_TYPES.LIKE) likesToIncrement--
69 else if (previousRate.type === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement--
78 70
79 // There was a previous rate, update it 71 previousRate.type = rateType
80 if (previousRate) {
81 // We will remove the previous rate, so we will need to remove it from the video attribute
82 if (previousRate.type === VIDEO_RATE_TYPES.LIKE) likesToIncrement--
83 else if (previousRate.type === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement--
84 72
85 previousRate.type = rateType 73 return previousRate.save(options).then(() => ({ t, likesToIncrement, dislikesToIncrement }))
74 } else { // There was not a previous rate, insert a new one
75 const query = {
76 userId: userInstance.id,
77 videoId: videoInstance.id,
78 type: rateType
79 }
86 80
87 previousRate.save(options).asCallback(function (err) { 81 return db.UserVideoRate.create(query, options).then(() => ({ likesToIncrement, dislikesToIncrement }))
88 return callback(err, t, likesToIncrement, dislikesToIncrement)
89 })
90 } else { // There was not a previous rate, insert a new one
91 const query = {
92 userId: userInstance.id,
93 videoId: videoInstance.id,
94 type: rateType
95 } 82 }
96
97 db.UserVideoRate.create(query, options).asCallback(function (err) {
98 return callback(err, t, likesToIncrement, dislikesToIncrement)
99 })
100 }
101 },
102
103 function updateVideoAttributeDB (t, likesToIncrement, dislikesToIncrement, callback) {
104 const options = { transaction: t }
105 const incrementQuery = {
106 likes: likesToIncrement,
107 dislikes: dislikesToIncrement
108 }
109
110 // Even if we do not own the video we increment the attributes
111 // It is usefull for the user to have a feedback
112 videoInstance.increment(incrementQuery, options).asCallback(function (err) {
113 return callback(err, t, likesToIncrement, dislikesToIncrement)
114 })
115 },
116
117 function sendEventsToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) {
118 // No need for an event type, we own the video
119 if (videoInstance.isOwned()) return callback(null, t, likesToIncrement, dislikesToIncrement)
120
121 const eventsParams = []
122
123 if (likesToIncrement !== 0) {
124 eventsParams.push({
125 videoId: videoInstance.id,
126 type: REQUEST_VIDEO_EVENT_TYPES.LIKES,
127 count: likesToIncrement
128 })
129 }
130
131 if (dislikesToIncrement !== 0) {
132 eventsParams.push({
133 videoId: videoInstance.id,
134 type: REQUEST_VIDEO_EVENT_TYPES.DISLIKES,
135 count: dislikesToIncrement
136 })
137 }
138
139 addEventsToRemoteVideo(eventsParams, t, function (err) {
140 return callback(err, t, likesToIncrement, dislikesToIncrement)
141 }) 83 })
142 }, 84 .then(({ likesToIncrement, dislikesToIncrement }) => {
143 85 const options = { transaction: t }
144 function sendQaduToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) { 86 const incrementQuery = {
145 // We do not own the video, there is no need to send a quick and dirty update to friends 87 likes: likesToIncrement,
146 // Our rate was already sent by the addEvent function 88 dislikes: dislikesToIncrement
147 if (videoInstance.isOwned() === false) return callback(null, t) 89 }
148 90
149 const qadusParams = [] 91 // Even if we do not own the video we increment the attributes
150 92 // It is usefull for the user to have a feedback
151 if (likesToIncrement !== 0) { 93 return videoInstance.increment(incrementQuery, options).then(() => ({ likesToIncrement, dislikesToIncrement }))
152 qadusParams.push({
153 videoId: videoInstance.id,
154 type: REQUEST_VIDEO_QADU_TYPES.LIKES
155 })
156 }
157
158 if (dislikesToIncrement !== 0) {
159 qadusParams.push({
160 videoId: videoInstance.id,
161 type: REQUEST_VIDEO_QADU_TYPES.DISLIKES
162 })
163 }
164
165 quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
166 return callback(err, t)
167 }) 94 })
168 }, 95 .then(({ likesToIncrement, dislikesToIncrement }) => {
96 // No need for an event type, we own the video
97 if (videoInstance.isOwned()) return { likesToIncrement, dislikesToIncrement }
98
99 const eventsParams = []
100
101 if (likesToIncrement !== 0) {
102 eventsParams.push({
103 videoId: videoInstance.id,
104 type: REQUEST_VIDEO_EVENT_TYPES.LIKES,
105 count: likesToIncrement
106 })
107 }
108
109 if (dislikesToIncrement !== 0) {
110 eventsParams.push({
111 videoId: videoInstance.id,
112 type: REQUEST_VIDEO_EVENT_TYPES.DISLIKES,
113 count: dislikesToIncrement
114 })
115 }
169 116
170 commitTransaction 117 return addEventsToRemoteVideo(eventsParams, t).then(() => ({ likesToIncrement, dislikesToIncrement }))
118 })
119 .then(({ likesToIncrement, dislikesToIncrement }) => {
120 // We do not own the video, there is no need to send a quick and dirty update to friends
121 // Our rate was already sent by the addEvent function
122 if (videoInstance.isOwned() === false) return undefined
123
124 const qadusParams = []
125
126 if (likesToIncrement !== 0) {
127 qadusParams.push({
128 videoId: videoInstance.id,
129 type: REQUEST_VIDEO_QADU_TYPES.LIKES
130 })
131 }
171 132
172 ], function (err: Error, t: Sequelize.Transaction) { 133 if (dislikesToIncrement !== 0) {
173 if (err) { 134 qadusParams.push({
174 // This is just a debug because we will retry the insert 135 videoId: videoInstance.id,
175 logger.debug('Cannot add the user video rate.', { error: err }) 136 type: REQUEST_VIDEO_QADU_TYPES.DISLIKES
176 return rollbackTransaction(err, t, finalCallback) 137 })
177 } 138 }
178 139
179 logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username) 140 return quickAndDirtyUpdatesVideoToFriends(qadusParams, t)
180 return finalCallback(null) 141 })
142 })
143 .then(() => logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username))
144 .catch(err => {
145 // This is just a debug because we will retry the insert
146 logger.debug('Cannot add the user video rate.', { error: err })
147 throw err
181 }) 148 })
182} 149}
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 @@
1import { parallel } from 'async'
2import * as express from 'express' 1import * as express from 'express'
3import * as fs from 'fs'
4import { join } from 'path' 2import { join } from 'path'
5import * as validator from 'validator' 3import * as validator from 'validator'
4import * as Promise from 'bluebird'
6 5
7import { database as db } from '../initializers/database' 6import { database as db } from '../initializers/database'
8import { 7import {
@@ -11,7 +10,7 @@ import {
11 STATIC_PATHS, 10 STATIC_PATHS,
12 STATIC_MAX_AGE 11 STATIC_MAX_AGE
13} from '../initializers' 12} from '../initializers'
14import { root } from '../helpers' 13import { root, readFileBufferPromise } from '../helpers'
15import { VideoInstance } from '../models' 14import { VideoInstance } from '../models'
16 15
17const clientsRouter = express.Router() 16const clientsRouter = express.Router()
@@ -95,19 +94,15 @@ function generateWatchHtmlPage (req: express.Request, res: express.Response, nex
95 // Let Angular application handle errors 94 // Let Angular application handle errors
96 if (!validator.isUUID(videoId, 4)) return res.sendFile(indexPath) 95 if (!validator.isUUID(videoId, 4)) return res.sendFile(indexPath)
97 96
98 parallel({ 97 Promise.all([
99 file: function (callback) { 98 readFileBufferPromise(indexPath),
100 fs.readFile(indexPath, callback) 99 db.Video.loadAndPopulateAuthorAndPodAndTags(videoId)
101 }, 100 ])
101 .then(([ file, video ]) => {
102 file = file as Buffer
103 video = video as VideoInstance
102 104
103 video: function (callback) { 105 const html = file.toString()
104 db.Video.loadAndPopulateAuthorAndPodAndTags(videoId, callback)
105 }
106 }, function (err: Error, result: { file: Buffer, video: VideoInstance }) {
107 if (err) return next(err)
108
109 const html = result.file.toString()
110 const video = result.video
111 106
112 // Let Angular application handle errors 107 // Let Angular application handle errors
113 if (!video) return res.sendFile(indexPath) 108 if (!video) return res.sendFile(indexPath)
@@ -115,4 +110,5 @@ function generateWatchHtmlPage (req: express.Request, res: express.Response, nex
115 const htmlStringPageWithTags = addOpenGraphTags(html, video) 110 const htmlStringPageWithTags = addOpenGraphTags(html, video)
116 res.set('Content-Type', 'text/html; charset=UTF-8').send(htmlStringPageWithTags) 111 res.set('Content-Type', 'text/html; charset=UTF-8').send(htmlStringPageWithTags)
117 }) 112 })
113 .catch(err => next(err))
118} 114}
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 @@
4*/ 4*/
5 5
6import { join } from 'path' 6import { join } from 'path'
7import { pseudoRandomBytes } from 'crypto'
8import {
9 readdir,
10 readFile,
11 rename,
12 unlink,
13 writeFile,
14 access
15} from 'fs'
16import * as mkdirp from 'mkdirp'
17import * as bcrypt from 'bcrypt'
18import * as createTorrent from 'create-torrent'
19import * as openssl from 'openssl-wrapper'
20import * as Promise from 'bluebird'
7 21
8function isTestInstance () { 22function isTestInstance () {
9 return process.env.NODE_ENV === 'test' 23 return process.env.NODE_ENV === 'test'
@@ -14,9 +28,82 @@ function root () {
14 return join(__dirname, '..', '..', '..') 28 return join(__dirname, '..', '..', '..')
15} 29}
16 30
31function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
32 return function promisified (): Promise<A> {
33 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
34 func.apply(null, [ (err: any, res: A) => err ? reject(err) : resolve(res) ])
35 })
36 }
37}
38
39// Thanks to https://gist.github.com/kumasento/617daa7e46f13ecdd9b2
40function promisify1<T, A> (func: (arg: T, cb: (err: any, result: A) => void) => void): (arg: T) => Promise<A> {
41 return function promisified (arg: T): Promise<A> {
42 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
43 func.apply(null, [ arg, (err: any, res: A) => err ? reject(err) : resolve(res) ])
44 })
45 }
46}
47
48function promisify1WithVoid<T> (func: (arg: T, cb: (err: any) => void) => void): (arg: T) => Promise<void> {
49 return function promisified (arg: T): Promise<void> {
50 return new Promise<void>((resolve: () => void, reject: (err: any) => void) => {
51 func.apply(null, [ arg, (err: any) => err ? reject(err) : resolve() ])
52 })
53 }
54}
55
56function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise<A> {
57 return function promisified (arg1: T, arg2: U): Promise<A> {
58 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
59 func.apply(null, [ arg1, arg2, (err: any, res: A) => err ? reject(err) : resolve(res) ])
60 })
61 }
62}
63
64function promisify2WithVoid<T, U> (func: (arg1: T, arg2: U, cb: (err: any) => void) => void): (arg1: T, arg2: U) => Promise<void> {
65 return function promisified (arg1: T, arg2: U): Promise<void> {
66 return new Promise<void>((resolve: () => void, reject: (err: any) => void) => {
67 func.apply(null, [ arg1, arg2, (err: any) => err ? reject(err) : resolve() ])
68 })
69 }
70}
71
72const readFilePromise = promisify2<string, string, string>(readFile)
73const readFileBufferPromise = promisify1<string, Buffer>(readFile)
74const unlinkPromise = promisify1WithVoid<string>(unlink)
75const renamePromise = promisify2WithVoid<string, string>(rename)
76const writeFilePromise = promisify2<string, any, void>(writeFile)
77const readdirPromise = promisify1<string, string[]>(readdir)
78const mkdirpPromise = promisify1<string, string>(mkdirp)
79const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes)
80const accessPromise = promisify1WithVoid<string|Buffer>(access)
81const opensslExecPromise = promisify2WithVoid<string, any>(openssl.exec)
82const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare)
83const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt)
84const bcryptHashPromise = promisify2<any, string|number, string>(bcrypt.hash)
85const createTorrentPromise = promisify2<string, any, any>(createTorrent)
86
17// --------------------------------------------------------------------------- 87// ---------------------------------------------------------------------------
18 88
19export { 89export {
20 isTestInstance, 90 isTestInstance,
21 root 91 root,
92
93 promisify0,
94 promisify1,
95 readdirPromise,
96 readFilePromise,
97 readFileBufferPromise,
98 unlinkPromise,
99 renamePromise,
100 writeFilePromise,
101 mkdirpPromise,
102 pseudoRandomBytesPromise,
103 accessPromise,
104 opensslExecPromise,
105 bcryptComparePromise,
106 bcryptGenSaltPromise,
107 bcryptHashPromise,
108 createTorrentPromise
22} 109}
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 @@
1import * as Sequelize from 'sequelize'
2// TODO: import from ES6 when retry typing file will include errorFilter function 1// TODO: import from ES6 when retry typing file will include errorFilter function
3import * as retry from 'async/retry' 2import * as retry from 'async/retry'
3import * as Promise from 'bluebird'
4 4
5import { database as db } from '../initializers/database'
6import { logger } from './logger' 5import { logger } from './logger'
7 6
8function commitTransaction (t: Sequelize.Transaction, callback: (err: Error) => void) {
9 return t.commit().asCallback(callback)
10}
11
12function rollbackTransaction (err: Error, t: Sequelize.Transaction, callback: (err: Error) => void) {
13 // Try to rollback transaction
14 if (t) {
15 // Do not catch err, report the original one
16 t.rollback().asCallback(function () {
17 return callback(err)
18 })
19 } else {
20 return callback(err)
21 }
22}
23
24type RetryTransactionWrapperOptions = { errorMessage: string, arguments?: any[] } 7type RetryTransactionWrapperOptions = { errorMessage: string, arguments?: any[] }
25function retryTransactionWrapper (functionToRetry: Function, options: RetryTransactionWrapperOptions, finalCallback: Function) { 8function retryTransactionWrapper (functionToRetry: (... args) => Promise<any>, options: RetryTransactionWrapperOptions) {
26 const args = options.arguments ? options.arguments : [] 9 const args = options.arguments ? options.arguments : []
27 10
28 transactionRetryer( 11 return transactionRetryer(
29 function (callback) { 12 function (callback) {
30 return functionToRetry.apply(this, args.concat([ callback ])) 13 functionToRetry.apply(this, args)
31 }, 14 .then(result => callback(null, result))
32 function (err) { 15 .catch(err => callback(err))
33 if (err) {
34 logger.error(options.errorMessage, { error: err })
35 }
36
37 // Do not return the error, continue the process
38 return finalCallback(null)
39 } 16 }
40 ) 17 )
18 .catch(err => {
19 // Do not throw the error, continue the process
20 logger.error(options.errorMessage, { error: err })
21 })
41} 22}
42 23
43function transactionRetryer (func: Function, callback: (err: Error) => void) { 24function transactionRetryer (func: Function) {
44 retry({ 25 return new Promise((res, rej) => {
45 times: 5, 26 retry({
46 27 times: 5,
47 errorFilter: function (err) {
48 const willRetry = (err.name === 'SequelizeDatabaseError')
49 logger.debug('Maybe retrying the transaction function.', { willRetry })
50 return willRetry
51 }
52 }, func, callback)
53}
54 28
55function startSerializableTransaction (callback: (err: Error, t: Sequelize.Transaction) => void) { 29 errorFilter: function (err) {
56 db.sequelize.transaction(/* { isolationLevel: 'SERIALIZABLE' } */).asCallback(function (err, t) { 30 const willRetry = (err.name === 'SequelizeDatabaseError')
57 // We force to return only two parameters 31 logger.debug('Maybe retrying the transaction function.', { willRetry })
58 return callback(err, t) 32 return willRetry
33 }
34 }, func, function (err) {
35 err ? rej(err) : res()
36 })
59 }) 37 })
60} 38}
61 39
62// --------------------------------------------------------------------------- 40// ---------------------------------------------------------------------------
63 41
64export { 42export {
65 commitTransaction,
66 retryTransactionWrapper, 43 retryTransactionWrapper,
67 rollbackTransaction,
68 startSerializableTransaction,
69 transactionRetryer 44 transactionRetryer
70} 45}
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 @@
1import * as crypto from 'crypto' 1import * as crypto from 'crypto'
2import * as bcrypt from 'bcrypt'
3import * as fs from 'fs' 2import * as fs from 'fs'
4import * as openssl from 'openssl-wrapper'
5import { join } from 'path' 3import { join } from 'path'
6 4
7import { 5import {
@@ -12,6 +10,14 @@ import {
12 BCRYPT_SALT_SIZE, 10 BCRYPT_SALT_SIZE,
13 PUBLIC_CERT_NAME 11 PUBLIC_CERT_NAME
14} from '../initializers' 12} from '../initializers'
13import {
14 readFilePromise,
15 bcryptComparePromise,
16 bcryptGenSaltPromise,
17 bcryptHashPromise,
18 accessPromise,
19 opensslExecPromise
20} from './core-utils'
15import { logger } from './logger' 21import { logger } from './logger'
16 22
17function checkSignature (publicKey: string, data: string, hexSignature: string) { 23function checkSignature (publicKey: string, data: string, hexSignature: string) {
@@ -60,46 +66,32 @@ function sign (data: string|Object) {
60 return signature 66 return signature
61} 67}
62 68
63function comparePassword (plainPassword: string, hashPassword: string, callback: (err: Error, match?: boolean) => void) { 69function comparePassword (plainPassword: string, hashPassword: string) {
64 bcrypt.compare(plainPassword, hashPassword, function (err, isPasswordMatch) { 70 return bcryptComparePromise(plainPassword, hashPassword)
65 if (err) return callback(err)
66
67 return callback(null, isPasswordMatch)
68 })
69} 71}
70 72
71function createCertsIfNotExist (callback: (err: Error) => void) { 73function createCertsIfNotExist () {
72 certsExist(function (err, exist) { 74 return certsExist().then(exist => {
73 if (err) return callback(err)
74
75 if (exist === true) { 75 if (exist === true) {
76 return callback(null) 76 return undefined
77 } 77 }
78 78
79 createCerts(function (err) { 79 return createCerts()
80 return callback(err)
81 })
82 }) 80 })
83} 81}
84 82
85function cryptPassword (password: string, callback: (err: Error, hash?: string) => void) { 83function cryptPassword (password: string) {
86 bcrypt.genSalt(BCRYPT_SALT_SIZE, function (err, salt) { 84 return bcryptGenSaltPromise(BCRYPT_SALT_SIZE).then(salt => bcryptHashPromise(password, salt))
87 if (err) return callback(err)
88
89 bcrypt.hash(password, salt, function (err, hash) {
90 return callback(err, hash)
91 })
92 })
93} 85}
94 86
95function getMyPrivateCert (callback: (err: Error, privateCert: string) => void) { 87function getMyPrivateCert () {
96 const certPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME) 88 const certPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME)
97 fs.readFile(certPath, 'utf8', callback) 89 return readFilePromise(certPath, 'utf8')
98} 90}
99 91
100function getMyPublicCert (callback: (err: Error, publicCert: string) => void) { 92function getMyPublicCert () {
101 const certPath = join(CONFIG.STORAGE.CERT_DIR, PUBLIC_CERT_NAME) 93 const certPath = join(CONFIG.STORAGE.CERT_DIR, PUBLIC_CERT_NAME)
102 fs.readFile(certPath, 'utf8', callback) 94 return readFilePromise(certPath, 'utf8')
103} 95}
104 96
105// --------------------------------------------------------------------------- 97// ---------------------------------------------------------------------------
@@ -116,23 +108,21 @@ export {
116 108
117// --------------------------------------------------------------------------- 109// ---------------------------------------------------------------------------
118 110
119function certsExist (callback: (err: Error, certsExist: boolean) => void) { 111function certsExist () {
120 const certPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME) 112 const certPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME)
121 fs.access(certPath, function (err) {
122 // If there is an error the certificates do not exist
123 const exists = !err
124 return callback(null, exists)
125 })
126}
127 113
128function createCerts (callback: (err: Error) => void) { 114 // If there is an error the certificates do not exist
129 certsExist(function (err, exist) { 115 return accessPromise(certPath)
130 if (err) return callback(err) 116 .then(() => true)
117 .catch(() => false)
118}
131 119
120function createCerts () {
121 return certsExist().then(exist => {
132 if (exist === true) { 122 if (exist === true) {
133 const errorMessage = 'Certs already exist.' 123 const errorMessage = 'Certs already exist.'
134 logger.warning(errorMessage) 124 logger.warning(errorMessage)
135 return callback(new Error(errorMessage)) 125 throw new Error(errorMessage)
136 } 126 }
137 127
138 logger.info('Generating a RSA key...') 128 logger.info('Generating a RSA key...')
@@ -142,30 +132,27 @@ function createCerts (callback: (err: Error) => void) {
142 'out': privateCertPath, 132 'out': privateCertPath,
143 '2048': false 133 '2048': false
144 } 134 }
145 openssl.exec('genrsa', genRsaOptions, function (err) { 135 return opensslExecPromise('genrsa', genRsaOptions)
146 if (err) { 136 .then(() => {
147 logger.error('Cannot create private key on this pod.') 137 logger.info('RSA key generated.')
148 return callback(err) 138 logger.info('Managing public key...')
149 } 139
150 140 const publicCertPath = join(CONFIG.STORAGE.CERT_DIR, 'peertube.pub')
151 logger.info('RSA key generated.') 141 const rsaOptions = {
152 logger.info('Managing public key...') 142 'in': privateCertPath,
153 143 'pubout': true,
154 const publicCertPath = join(CONFIG.STORAGE.CERT_DIR, 'peertube.pub') 144 'out': publicCertPath
155 const rsaOptions = {
156 'in': privateCertPath,
157 'pubout': true,
158 'out': publicCertPath
159 }
160 openssl.exec('rsa', rsaOptions, function (err) {
161 if (err) {
162 logger.error('Cannot create public key on this pod.')
163 return callback(err)
164 } 145 }
165 146 return opensslExecPromise('rsa', rsaOptions)
166 logger.info('Public key managed.') 147 .then(() => logger.info('Public key managed.'))
167 return callback(null) 148 .catch(err => {
149 logger.error('Cannot create public key on this pod.')
150 throw err
151 })
152 })
153 .catch(err => {
154 logger.error('Cannot create private key on this pod.')
155 throw err
168 }) 156 })
169 })
170 }) 157 })
171} 158}
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 @@
1import * as replay from 'request-replay' 1import * as replay from 'request-replay'
2import * as request from 'request' 2import * as request from 'request'
3import * as Promise from 'bluebird'
3 4
4import { 5import {
5 RETRY_REQUESTS, 6 RETRY_REQUESTS,
@@ -14,16 +15,18 @@ type MakeRetryRequestParams = {
14 method: 'GET'|'POST', 15 method: 'GET'|'POST',
15 json: Object 16 json: Object
16} 17}
17function makeRetryRequest (params: MakeRetryRequestParams, callback: request.RequestCallback) { 18function makeRetryRequest (params: MakeRetryRequestParams) {
18 replay( 19 return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => {
19 request(params, callback), 20 replay(
20 { 21 request(params, (err, response, body) => err ? rej(err) : res({ response, body })),
21 retries: RETRY_REQUESTS, 22 {
22 factor: 3, 23 retries: RETRY_REQUESTS,
23 maxTimeout: Infinity, 24 factor: 3,
24 errorCodes: [ 'EADDRINFO', 'ETIMEDOUT', 'ECONNRESET', 'ESOCKETTIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED' ] 25 maxTimeout: Infinity,
25 } 26 errorCodes: [ 'EADDRINFO', 'ETIMEDOUT', 'ECONNRESET', 'ESOCKETTIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED' ]
26 ) 27 }
28 )
29 })
27} 30}
28 31
29type MakeSecureRequestParams = { 32type MakeSecureRequestParams = {
@@ -33,41 +36,43 @@ type MakeSecureRequestParams = {
33 sign: boolean 36 sign: boolean
34 data?: Object 37 data?: Object
35} 38}
36function makeSecureRequest (params: MakeSecureRequestParams, callback: request.RequestCallback) { 39function makeSecureRequest (params: MakeSecureRequestParams) {
37 const requestParams = { 40 return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => {
38 url: REMOTE_SCHEME.HTTP + '://' + params.toPod.host + params.path, 41 const requestParams = {
39 json: {} 42 url: REMOTE_SCHEME.HTTP + '://' + params.toPod.host + params.path,
40 } 43 json: {}
44 }
41 45
42 if (params.method !== 'POST') { 46 if (params.method !== 'POST') {
43 return callback(new Error('Cannot make a secure request with a non POST method.'), null, null) 47 return rej(new Error('Cannot make a secure request with a non POST method.'))
44 } 48 }
45 49
46 // Add signature if it is specified in the params 50 // Add signature if it is specified in the params
47 if (params.sign === true) { 51 if (params.sign === true) {
48 const host = CONFIG.WEBSERVER.HOST 52 const host = CONFIG.WEBSERVER.HOST
49 53
50 let dataToSign 54 let dataToSign
51 if (params.data) { 55 if (params.data) {
52 dataToSign = params.data 56 dataToSign = params.data
53 } else { 57 } else {
54 // We do not have data to sign so we just take our host 58 // We do not have data to sign so we just take our host
55 // It is not ideal but the connection should be in HTTPS 59 // It is not ideal but the connection should be in HTTPS
56 dataToSign = host 60 dataToSign = host
57 } 61 }
58 62
59 requestParams.json['signature'] = { 63 requestParams.json['signature'] = {
60 host, // Which host we pretend to be 64 host, // Which host we pretend to be
61 signature: sign(dataToSign) 65 signature: sign(dataToSign)
66 }
62 } 67 }
63 }
64 68
65 // If there are data informations 69 // If there are data informations
66 if (params.data) { 70 if (params.data) {
67 requestParams.json['data'] = params.data 71 requestParams.json['data'] = params.data
68 } 72 }
69 73
70 request.post(requestParams, callback) 74 request.post(requestParams, (err, response, body) => err ? rej(err) : res({ response, body }))
75 })
71} 76}
72 77
73// --------------------------------------------------------------------------- 78// ---------------------------------------------------------------------------
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 @@
1import * as express from 'express' 1import * as express from 'express'
2 2
3import { pseudoRandomBytes } from 'crypto' 3import { pseudoRandomBytesPromise } from './core-utils'
4 4import { ResultList } from '../../shared'
5import { logger } from './logger'
6 5
7function badRequest (req: express.Request, res: express.Response, next: express.NextFunction) { 6function badRequest (req: express.Request, res: express.Response, next: express.NextFunction) {
8 res.type('json').status(400).end() 7 res.type('json').status(400).end()
9} 8}
10 9
11function generateRandomString (size: number, callback: (err: Error, randomString?: string) => void) { 10function generateRandomString (size: number) {
12 pseudoRandomBytes(size, function (err, raw) { 11 return pseudoRandomBytesPromise(size).then(raw => raw.toString('hex'))
13 if (err) return callback(err)
14
15 callback(null, raw.toString('hex'))
16 })
17}
18
19function createEmptyCallback () {
20 return function (err) {
21 if (err) logger.error('Error in empty callback.', { error: err })
22 }
23} 12}
24 13
25interface FormatableToJSON { 14interface FormatableToJSON {
@@ -33,17 +22,18 @@ function getFormatedObjects<U, T extends FormatableToJSON> (objects: T[], object
33 formatedObjects.push(object.toFormatedJSON()) 22 formatedObjects.push(object.toFormatedJSON())
34 }) 23 })
35 24
36 return { 25 const res: ResultList<U> = {
37 total: objectsTotal, 26 total: objectsTotal,
38 data: formatedObjects 27 data: formatedObjects
39 } 28 }
29
30 return res
40} 31}
41 32
42// --------------------------------------------------------------------------- 33// ---------------------------------------------------------------------------
43 34
44export { 35export {
45 badRequest, 36 badRequest,
46 createEmptyCallback,
47 generateRandomString, 37 generateRandomString,
48 getFormatedObjects 38 getFormatedObjects
49} 39}
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'
2 2
3import { database as db } from './database' 3import { database as db } from './database'
4import { CONFIG } from './constants' 4import { CONFIG } from './constants'
5import { promisify0 } from '../helpers/core-utils'
5 6
6// Some checks on configuration files 7// Some checks on configuration files
7function checkConfig () { 8function checkConfig () {
@@ -35,41 +36,36 @@ function checkMissedConfig () {
35} 36}
36 37
37// Check the available codecs 38// Check the available codecs
38function checkFFmpeg (callback: (err: Error) => void) { 39function checkFFmpeg () {
39 const Ffmpeg = require('fluent-ffmpeg') 40 const Ffmpeg = require('fluent-ffmpeg')
40 41 const getAvailableCodecsPromise = promisify0(Ffmpeg.getAvailableCodecs)
41 Ffmpeg.getAvailableCodecs(function (err, codecs) { 42
42 if (err) return callback(err) 43 getAvailableCodecsPromise()
43 if (CONFIG.TRANSCODING.ENABLED === false) return callback(null) 44 .then(codecs => {
44 45 if (CONFIG.TRANSCODING.ENABLED === false) return undefined
45 const canEncode = [ 'libx264' ] 46
46 canEncode.forEach(function (codec) { 47 const canEncode = [ 'libx264' ]
47 if (codecs[codec] === undefined) { 48 canEncode.forEach(function (codec) {
48 return callback(new Error('Unknown codec ' + codec + ' in FFmpeg.')) 49 if (codecs[codec] === undefined) {
49 } 50 throw new Error('Unknown codec ' + codec + ' in FFmpeg.')
50 51 }
51 if (codecs[codec].canEncode !== true) { 52
52 return callback(new Error('Unavailable encode codec ' + codec + ' in FFmpeg')) 53 if (codecs[codec].canEncode !== true) {
53 } 54 throw new Error('Unavailable encode codec ' + codec + ' in FFmpeg')
55 }
56 })
54 }) 57 })
55
56 return callback(null)
57 })
58} 58}
59 59
60function clientsExist (callback: (err: Error, clientsExist?: boolean) => void) { 60function clientsExist () {
61 db.OAuthClient.countTotal(function (err, totalClients) { 61 return db.OAuthClient.countTotal().then(totalClients => {
62 if (err) return callback(err) 62 return totalClients !== 0
63
64 return callback(null, totalClients !== 0)
65 }) 63 })
66} 64}
67 65
68function usersExist (callback: (err: Error, usersExist?: boolean) => void) { 66function usersExist () {
69 db.User.countTotal(function (err, totalUsers) { 67 return db.User.countTotal().then(totalUsers => {
70 if (err) return callback(err) 68 return totalUsers !== 0
71
72 return callback(null, totalUsers !== 0)
73 }) 69 })
74} 70}
75 71
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 @@
1import * as fs from 'fs'
2import { join } from 'path' 1import { join } from 'path'
2import { flattenDepth } from 'lodash'
3import * as Sequelize from 'sequelize' 3import * as Sequelize from 'sequelize'
4import { each } from 'async' 4import * as Promise from 'bluebird'
5 5
6import { CONFIG } from './constants' 6import { CONFIG } from './constants'
7// Do not use barrel, we need to load database first 7// Do not use barrel, we need to load database first
8import { logger } from '../helpers/logger' 8import { logger } from '../helpers/logger'
9import { isTestInstance } from '../helpers/core-utils' 9import { isTestInstance, readdirPromise } from '../helpers/core-utils'
10import { 10import {
11 ApplicationModel, 11 ApplicationModel,
12 AuthorModel, 12 AuthorModel,
@@ -33,7 +33,7 @@ const password = CONFIG.DATABASE.PASSWORD
33 33
34const database: { 34const database: {
35 sequelize?: Sequelize.Sequelize, 35 sequelize?: Sequelize.Sequelize,
36 init?: (silent: any, callback: any) => void, 36 init?: (silent: boolean) => Promise<void>,
37 37
38 Application?: ApplicationModel, 38 Application?: ApplicationModel,
39 Author?: AuthorModel, 39 Author?: AuthorModel,
@@ -72,19 +72,17 @@ const sequelize = new Sequelize(dbname, username, password, {
72 72
73database.sequelize = sequelize 73database.sequelize = sequelize
74 74
75database.init = function (silent: boolean, callback: (err: Error) => void) { 75database.init = function (silent: boolean) {
76 const modelDirectory = join(__dirname, '..', 'models') 76 const modelDirectory = join(__dirname, '..', 'models')
77 77
78 getModelFiles(modelDirectory, function (err, filePaths) { 78 return getModelFiles(modelDirectory).then(filePaths => {
79 if (err) throw err 79 filePaths.forEach(filePath => {
80
81 filePaths.forEach(function (filePath) {
82 const model = sequelize.import(filePath) 80 const model = sequelize.import(filePath)
83 81
84 database[model['name']] = model 82 database[model['name']] = model
85 }) 83 })
86 84
87 Object.keys(database).forEach(function (modelName) { 85 Object.keys(database).forEach(modelName => {
88 if ('associate' in database[modelName]) { 86 if ('associate' in database[modelName]) {
89 database[modelName].associate(database) 87 database[modelName].associate(database)
90 } 88 }
@@ -92,7 +90,7 @@ database.init = function (silent: boolean, callback: (err: Error) => void) {
92 90
93 if (!silent) logger.info('Database %s is ready.', dbname) 91 if (!silent) logger.info('Database %s is ready.', dbname)
94 92
95 return callback(null) 93 return undefined
96 }) 94 })
97} 95}
98 96
@@ -104,49 +102,50 @@ export {
104 102
105// --------------------------------------------------------------------------- 103// ---------------------------------------------------------------------------
106 104
107function getModelFiles (modelDirectory: string, callback: (err: Error, filePaths: string[]) => void) { 105function getModelFiles (modelDirectory: string) {
108 fs.readdir(modelDirectory, function (err, files) { 106 return readdirPromise(modelDirectory)
109 if (err) throw err 107 .then(files => {
110 108 const directories: string[] = files.filter(function (directory) {
111 const directories = files.filter(function (directory) { 109 // Find directories
112 // Find directories 110 if (
113 if ( 111 directory.endsWith('.js.map') ||
114 directory.endsWith('.js.map') || 112 directory === 'index.js' || directory === 'index.ts' ||
115 directory === 'index.js' || directory === 'index.ts' || 113 directory === 'utils.js' || directory === 'utils.ts'
116 directory === 'utils.js' || directory === 'utils.ts' 114 ) return false
117 ) return false 115
116 return true
117 })
118 118
119 return true 119 return directories
120 }) 120 })
121 121 .then(directories => {
122 let modelFilePaths: string[] = [] 122 const tasks = []
123 123
124 // For each directory we read it and append model in the modelFilePaths array 124 // For each directory we read it and append model in the modelFilePaths array
125 each(directories, function (directory: string, eachCallback: ErrorCallback<Error>) { 125 directories.forEach(directory => {
126 const modelDirectoryPath = join(modelDirectory, directory) 126 const modelDirectoryPath = join(modelDirectory, directory)
127 127
128 fs.readdir(modelDirectoryPath, function (err, files) { 128 const promise = readdirPromise(modelDirectoryPath).then(files => {
129 if (err) return eachCallback(err) 129 const filteredFiles = files.filter(file => {
130 130 if (
131 const filteredFiles = files.filter(file => { 131 file === 'index.js' || file === 'index.ts' ||
132 if ( 132 file === 'utils.js' || file === 'utils.ts' ||
133 file === 'index.js' || file === 'index.ts' || 133 file.endsWith('-interface.js') || file.endsWith('-interface.ts') ||
134 file === 'utils.js' || file === 'utils.ts' || 134 file.endsWith('.js.map')
135 file.endsWith('-interface.js') || file.endsWith('-interface.ts') || 135 ) return false
136 file.endsWith('.js.map') 136
137 ) return false 137 return true
138 138 }).map(file => join(modelDirectoryPath, file))
139 return true 139
140 }).map(file => { 140 return filteredFiles
141 return join(modelDirectoryPath, file)
142 }) 141 })
143 142
144 modelFilePaths = modelFilePaths.concat(filteredFiles) 143 tasks.push(promise)
145
146 return eachCallback(null)
147 }) 144 })
148 }, function (err: Error) { 145
149 return callback(err, modelFilePaths) 146 return Promise.all(tasks)
147 })
148 .then((filteredFiles: string[][]) => {
149 return flattenDepth<string>(filteredFiles, 1)
150 }) 150 })
151 })
152} 151}
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 @@
1import { join } from 'path' 1import { join } from 'path'
2import * as config from 'config' 2import * as config from 'config'
3import { each, series } from 'async'
4import * as mkdirp from 'mkdirp'
5import * as passwordGenerator from 'password-generator' 3import * as passwordGenerator from 'password-generator'
4import * as Promise from 'bluebird'
6 5
7import { database as db } from './database' 6import { database as db } from './database'
8import { USER_ROLES, CONFIG, LAST_MIGRATION_VERSION } from './constants' 7import { USER_ROLES, CONFIG, LAST_MIGRATION_VERSION } from './constants'
9import { clientsExist, usersExist } from './checker' 8import { clientsExist, usersExist } from './checker'
10import { logger, createCertsIfNotExist, root } from '../helpers' 9import { logger, createCertsIfNotExist, root, mkdirpPromise } from '../helpers'
11 10
12function installApplication (callback: (err: Error) => void) { 11function installApplication () {
13 series([ 12 return db.sequelize.sync()
14 function createDatabase (callbackAsync) { 13 .then(() => createDirectoriesIfNotExist())
15 db.sequelize.sync().asCallback(callbackAsync) 14 .then(() => createCertsIfNotExist())
16 // db.sequelize.sync({ force: true }).asCallback(callbackAsync) 15 .then(() => createOAuthClientIfNotExist())
17 }, 16 .then(() => createOAuthAdminIfNotExist())
18
19 function createDirectories (callbackAsync) {
20 createDirectoriesIfNotExist(callbackAsync)
21 },
22
23 function createCertificates (callbackAsync) {
24 createCertsIfNotExist(callbackAsync)
25 },
26
27 function createOAuthClient (callbackAsync) {
28 createOAuthClientIfNotExist(callbackAsync)
29 },
30
31 function createOAuthUser (callbackAsync) {
32 createOAuthAdminIfNotExist(callbackAsync)
33 }
34 ], callback)
35} 17}
36 18
37// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
@@ -42,21 +24,22 @@ export {
42 24
43// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
44 26
45function createDirectoriesIfNotExist (callback: (err: Error) => void) { 27function createDirectoriesIfNotExist () {
46 const storages = config.get('storage') 28 const storages = config.get('storage')
47 29
48 each(Object.keys(storages), function (key, callbackEach) { 30 const tasks = []
31 Object.keys(storages).forEach(key => {
49 const dir = storages[key] 32 const dir = storages[key]
50 mkdirp(join(root(), dir), callbackEach) 33 tasks.push(mkdirpPromise(join(root(), dir)))
51 }, callback) 34 })
52}
53 35
54function createOAuthClientIfNotExist (callback: (err: Error) => void) { 36 return Promise.all(tasks)
55 clientsExist(function (err, exist) { 37}
56 if (err) return callback(err)
57 38
39function createOAuthClientIfNotExist () {
40 return clientsExist().then(exist => {
58 // Nothing to do, clients already exist 41 // Nothing to do, clients already exist
59 if (exist === true) return callback(null) 42 if (exist === true) return undefined
60 43
61 logger.info('Creating a default OAuth Client.') 44 logger.info('Creating a default OAuth Client.')
62 45
@@ -69,23 +52,19 @@ function createOAuthClientIfNotExist (callback: (err: Error) => void) {
69 redirectUris: null 52 redirectUris: null
70 }) 53 })
71 54
72 client.save().asCallback(function (err, createdClient) { 55 return client.save().then(createdClient => {
73 if (err) return callback(err)
74
75 logger.info('Client id: ' + createdClient.clientId) 56 logger.info('Client id: ' + createdClient.clientId)
76 logger.info('Client secret: ' + createdClient.clientSecret) 57 logger.info('Client secret: ' + createdClient.clientSecret)
77 58
78 return callback(null) 59 return undefined
79 }) 60 })
80 }) 61 })
81} 62}
82 63
83function createOAuthAdminIfNotExist (callback: (err: Error) => void) { 64function createOAuthAdminIfNotExist () {
84 usersExist(function (err, exist) { 65 return usersExist().then(exist => {
85 if (err) return callback(err)
86
87 // Nothing to do, users already exist 66 // Nothing to do, users already exist
88 if (exist === true) return callback(null) 67 if (exist === true) return undefined
89 68
90 logger.info('Creating the administrator.') 69 logger.info('Creating the administrator.')
91 70
@@ -116,14 +95,12 @@ function createOAuthAdminIfNotExist (callback: (err: Error) => void) {
116 role 95 role
117 } 96 }
118 97
119 db.User.create(userData, createOptions).asCallback(function (err, createdUser) { 98 return db.User.create(userData, createOptions).then(createdUser => {
120 if (err) return callback(err)
121
122 logger.info('Username: ' + username) 99 logger.info('Username: ' + username)
123 logger.info('User password: ' + password) 100 logger.info('User password: ' + password)
124 101
125 logger.info('Creating Application table.') 102 logger.info('Creating Application table.')
126 db.Application.create({ migrationVersion: LAST_MIGRATION_VERSION }).asCallback(callback) 103 return db.Application.create({ migrationVersion: LAST_MIGRATION_VERSION })
127 }) 104 })
128 }) 105 })
129} 106}
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 @@
1import { waterfall } from 'async' 1import * as Sequelize from 'sequelize'
2 2import * as Promise from 'bluebird'
3// utils = { transaction, queryInterface, sequelize, Sequelize } 3
4function up (utils, finalCallback) { 4function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize
8}): Promise<void> {
5 const q = utils.queryInterface 9 const q = utils.queryInterface
6 const Sequelize = utils.Sequelize
7 10
8 const data = { 11 const data = {
9 type: Sequelize.STRING(400), 12 type: Sequelize.STRING(400),
@@ -11,27 +14,16 @@ function up (utils, finalCallback) {
11 defaultValue: '' 14 defaultValue: ''
12 } 15 }
13 16
14 waterfall([ 17 return q.addColumn('Pods', 'email', data)
15 18 .then(() => {
16 function addEmailColumn (callback) {
17 q.addColumn('Pods', 'email', data, { transaction: utils.transaction }).asCallback(function (err) {
18 return callback(err)
19 })
20 },
21
22 function updateWithFakeEmails (callback) {
23 const query = 'UPDATE "Pods" SET "email" = \'dummy@example.com\'' 19 const query = 'UPDATE "Pods" SET "email" = \'dummy@example.com\''
24 utils.sequelize.query(query, { transaction: utils.transaction }).asCallback(function (err) { 20 return utils.sequelize.query(query, { transaction: utils.transaction })
25 return callback(err) 21 })
26 }) 22 .then(() => {
27 },
28
29 function nullOnDefault (callback) {
30 data.defaultValue = null 23 data.defaultValue = null
31 24
32 q.changeColumn('Pods', 'email', data, { transaction: utils.transaction }).asCallback(callback) 25 return q.changeColumn('Pods', 'email', data)
33 } 26 })
34 ], finalCallback)
35} 27}
36 28
37function down (options, callback) { 29function 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 @@
1import { waterfall } from 'async' 1import * as Sequelize from 'sequelize'
2 2import * as Promise from 'bluebird'
3// utils = { transaction, queryInterface, sequelize, Sequelize } 3
4function up (utils, finalCallback) { 4function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize
8}): Promise<void> {
5 const q = utils.queryInterface 9 const q = utils.queryInterface
6 const Sequelize = utils.Sequelize
7 10
8 const data = { 11 const data = {
9 type: Sequelize.STRING(400), 12 type: Sequelize.STRING(400),
10 allowNull: false, 13 allowNull: false,
11 defaultValue: '' 14 defaultValue: ''
12 } 15 }
13 16 return q.addColumn('Users', 'email', data)
14 waterfall([ 17 .then(() => {
15
16 function addEmailColumn (callback) {
17 q.addColumn('Users', 'email', data, { transaction: utils.transaction }).asCallback(function (err) {
18 return callback(err)
19 })
20 },
21
22 function updateWithFakeEmails (callback) {
23 const query = 'UPDATE "Users" SET "email" = CONCAT("username", \'@example.com\')' 18 const query = 'UPDATE "Users" SET "email" = CONCAT("username", \'@example.com\')'
24 utils.sequelize.query(query, { transaction: utils.transaction }).asCallback(function (err) { 19 return utils.sequelize.query(query, { transaction: utils.transaction })
25 return callback(err) 20 })
26 }) 21 .then(() => {
27 },
28
29 function nullOnDefault (callback) {
30 data.defaultValue = null 22 data.defaultValue = null
31 23
32 q.changeColumn('Users', 'email', data, { transaction: utils.transaction }).asCallback(callback) 24 return q.changeColumn('Users', 'email', data)
33 } 25 })
34 ], finalCallback)
35} 26}
36 27
37function down (options, callback) { 28function 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 @@
1// utils = { transaction, queryInterface, sequelize, Sequelize } 1import * as Sequelize from 'sequelize'
2function up (utils, finalCallback) { 2import * as Promise from 'bluebird'
3
4function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize
8}): Promise<void> {
3 const q = utils.queryInterface 9 const q = utils.queryInterface
4 const Sequelize = utils.Sequelize
5 10
6 const data = { 11 const data = {
7 type: Sequelize.INTEGER, 12 type: Sequelize.INTEGER,
@@ -9,7 +14,7 @@ function up (utils, finalCallback) {
9 defaultValue: 0 14 defaultValue: 0
10 } 15 }
11 16
12 q.addColumn('Videos', 'views', data, { transaction: utils.transaction }).asCallback(finalCallback) 17 return q.addColumn('Videos', 'views', data)
13} 18}
14 19
15function down (options, callback) { 20function 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 @@
1// utils = { transaction, queryInterface, sequelize, Sequelize } 1import * as Sequelize from 'sequelize'
2function up (utils, finalCallback) { 2import * as Promise from 'bluebird'
3
4function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize
8}): Promise<void> {
3 const q = utils.queryInterface 9 const q = utils.queryInterface
4 const Sequelize = utils.Sequelize
5 10
6 const data = { 11 const data = {
7 type: Sequelize.INTEGER, 12 type: Sequelize.INTEGER,
@@ -9,7 +14,7 @@ function up (utils, finalCallback) {
9 defaultValue: 0 14 defaultValue: 0
10 } 15 }
11 16
12 q.addColumn('Videos', 'likes', data, { transaction: utils.transaction }).asCallback(finalCallback) 17 return q.addColumn('Videos', 'likes', data)
13} 18}
14 19
15function down (options, callback) { 20function 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 @@
1// utils = { transaction, queryInterface, sequelize, Sequelize } 1import * as Sequelize from 'sequelize'
2function up (utils, finalCallback) { 2import * as Promise from 'bluebird'
3
4function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize
8}): Promise<void> {
3 const q = utils.queryInterface 9 const q = utils.queryInterface
4 const Sequelize = utils.Sequelize
5 10
6 const data = { 11 const data = {
7 type: Sequelize.INTEGER, 12 type: Sequelize.INTEGER,
@@ -9,7 +14,7 @@ function up (utils, finalCallback) {
9 defaultValue: 0 14 defaultValue: 0
10 } 15 }
11 16
12 q.addColumn('Videos', 'dislikes', data, { transaction: utils.transaction }).asCallback(finalCallback) 17 return q.addColumn('Videos', 'dislikes', data)
13} 18}
14 19
15function down (options, callback) { 20function 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 @@
1import { waterfall } from 'async' 1import * as Sequelize from 'sequelize'
2 2import * as Promise from 'bluebird'
3// utils = { transaction, queryInterface, sequelize, Sequelize } 3
4function up (utils, finalCallback) { 4function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize
8}): Promise<void> {
5 const q = utils.queryInterface 9 const q = utils.queryInterface
6 const Sequelize = utils.Sequelize
7 10
8 const data = { 11 const data = {
9 type: Sequelize.INTEGER, 12 type: Sequelize.INTEGER,
@@ -11,20 +14,12 @@ function up (utils, finalCallback) {
11 defaultValue: 0 14 defaultValue: 0
12 } 15 }
13 16
14 waterfall([ 17 return q.addColumn('Videos', 'category', data)
15 18 .then(() => {
16 function addCategoryColumn (callback) {
17 q.addColumn('Videos', 'category', data, { transaction: utils.transaction }).asCallback(function (err) {
18 return callback(err)
19 })
20 },
21
22 function nullOnDefault (callback) {
23 data.defaultValue = null 19 data.defaultValue = null
24 20
25 q.changeColumn('Videos', 'category', data, { transaction: utils.transaction }).asCallback(callback) 21 return q.changeColumn('Videos', 'category', data)
26 } 22 })
27 ], finalCallback)
28} 23}
29 24
30function down (options, callback) { 25function 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 @@
1import { waterfall } from 'async' 1import * as Sequelize from 'sequelize'
2 2import * as Promise from 'bluebird'
3// utils = { transaction, queryInterface, sequelize, Sequelize } 3
4function up (utils, finalCallback) { 4function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize
8}): Promise<void> {
5 const q = utils.queryInterface 9 const q = utils.queryInterface
6 const Sequelize = utils.Sequelize
7 10
8 const data = { 11 const data = {
9 type: Sequelize.INTEGER, 12 type: Sequelize.INTEGER,
@@ -11,20 +14,11 @@ function up (utils, finalCallback) {
11 defaultValue: 0 14 defaultValue: 0
12 } 15 }
13 16
14 waterfall([ 17 return q.addColumn('Videos', 'licence', data)
15 18 .then(() => {
16 function addLicenceColumn (callback) {
17 q.addColumn('Videos', 'licence', data, { transaction: utils.transaction }).asCallback(function (err) {
18 return callback(err)
19 })
20 },
21
22 function nullOnDefault (callback) {
23 data.defaultValue = null 19 data.defaultValue = null
24 20 return q.changeColumn('Videos', 'licence', data)
25 q.changeColumn('Videos', 'licence', data, { transaction: utils.transaction }).asCallback(callback) 21 })
26 }
27 ], finalCallback)
28} 22}
29 23
30function down (options, callback) { 24function 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 @@
1import { waterfall } from 'async' 1import * as Sequelize from 'sequelize'
2 2import * as Promise from 'bluebird'
3// utils = { transaction, queryInterface, sequelize, Sequelize } 3
4function up (utils, finalCallback) { 4function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize
8}): Promise<void> {
5 const q = utils.queryInterface 9 const q = utils.queryInterface
6 const Sequelize = utils.Sequelize
7 10
8 const data = { 11 const data = {
9 type: Sequelize.BOOLEAN, 12 type: Sequelize.BOOLEAN,
@@ -11,20 +14,12 @@ function up (utils, finalCallback) {
11 defaultValue: false 14 defaultValue: false
12 } 15 }
13 16
14 waterfall([ 17 return q.addColumn('Videos', 'nsfw', data)
15 18 .then(() => {
16 function addNSFWColumn (callback) {
17 q.addColumn('Videos', 'nsfw', data, { transaction: utils.transaction }).asCallback(function (err) {
18 return callback(err)
19 })
20 },
21
22 function nullOnDefault (callback) {
23 data.defaultValue = null 19 data.defaultValue = null
24 20
25 q.changeColumn('Videos', 'nsfw', data, { transaction: utils.transaction }).asCallback(callback) 21 return q.changeColumn('Videos', 'nsfw', data)
26 } 22 })
27 ], finalCallback)
28} 23}
29 24
30function down (options, callback) { 25function 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 @@
1// utils = { transaction, queryInterface, sequelize, Sequelize } 1import * as Sequelize from 'sequelize'
2function up (utils, finalCallback) { 2import * as Promise from 'bluebird'
3
4function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize
8}): Promise<void> {
3 const q = utils.queryInterface 9 const q = utils.queryInterface
4 const Sequelize = utils.Sequelize
5 10
6 const data = { 11 const data = {
7 type: Sequelize.BOOLEAN, 12 type: Sequelize.BOOLEAN,
@@ -9,7 +14,7 @@ function up (utils, finalCallback) {
9 defaultValue: false 14 defaultValue: false
10 } 15 }
11 16
12 q.addColumn('Users', 'displayNSFW', data, { transaction: utils.transaction }).asCallback(finalCallback) 17 return q.addColumn('Users', 'displayNSFW', data)
13} 18}
14 19
15function down (options, callback) { 20function 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 @@
1// utils = { transaction, queryInterface, sequelize, Sequelize } 1import * as Sequelize from 'sequelize'
2function up (utils, finalCallback) { 2import * as Promise from 'bluebird'
3
4function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize
8}): Promise<void> {
3 const q = utils.queryInterface 9 const q = utils.queryInterface
4 const Sequelize = utils.Sequelize
5 10
6 const data = { 11 const data = {
7 type: Sequelize.INTEGER, 12 type: Sequelize.INTEGER,
@@ -9,7 +14,7 @@ function up (utils, finalCallback) {
9 defaultValue: null 14 defaultValue: null
10 } 15 }
11 16
12 q.addColumn('Videos', 'language', data, { transaction: utils.transaction }).asCallback(finalCallback) 17 return q.addColumn('Videos', 'language', data)
13} 18}
14 19
15function down (options, callback) { 20function 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 @@
1import { waterfall, eachSeries } from 'async'
2import * as fs from 'fs'
3import * as path from 'path' 1import * as path from 'path'
4import * as Sequelize from 'sequelize' 2import * as Promise from 'bluebird'
5 3
6import { database as db } from './database' 4import { database as db } from './database'
7import { LAST_MIGRATION_VERSION } from './constants' 5import { LAST_MIGRATION_VERSION } from './constants'
8import { logger } from '../helpers' 6import { logger, readdirPromise } from '../helpers'
9 7
10function migrate (finalCallback: (err: Error) => void) { 8function migrate () {
11 waterfall([ 9 const p = db.sequelize.getQueryInterface().showAllTables()
12 10 .then(tables => {
13 function checkApplicationTableExists (callback) { 11 // No tables, we don't need to migrate anything
14 db.sequelize.getQueryInterface().showAllTables().asCallback(function (err, tables) { 12 // The installer will do that
15 if (err) return callback(err) 13 if (tables.length === 0) throw null
16 14 })
17 // No tables, we don't need to migrate anything 15 .then(() => {
18 // The installer will do that 16 return db.Application.loadMigrationVersion()
19 if (tables.length === 0) return finalCallback(null) 17 })
20 18 .then(actualVersion => {
21 return callback(null)
22 })
23 },
24
25 function loadMigrationVersion (callback) {
26 db.Application.loadMigrationVersion(callback)
27 },
28
29 function createMigrationRowIfNotExists (actualVersion, callback) {
30 if (actualVersion === null) { 19 if (actualVersion === null) {
31 db.Application.create({ 20 return db.Application.create({ migrationVersion: 0 }).then(() => 0)
32 migrationVersion: 0
33 }, function (err) {
34 return callback(err, 0)
35 })
36 } 21 }
37 22
38 return callback(null, actualVersion) 23 return actualVersion
39 }, 24 })
40 25 .then(actualVersion => {
41 function abortMigrationIfNotNeeded (actualVersion, callback) { 26 // No need migrations, abort
42 // No need migrations 27 if (actualVersion >= LAST_MIGRATION_VERSION) throw null
43 if (actualVersion >= LAST_MIGRATION_VERSION) return finalCallback(null)
44
45 return callback(null, actualVersion)
46 },
47 28
48 function getMigrations (actualVersion, callback) { 29 return actualVersion
30 })
31 .then(actualVersion => {
49 // If there are a new migration scripts 32 // If there are a new migration scripts
50 logger.info('Begin migrations.') 33 logger.info('Begin migrations.')
51 34
52 getMigrationScripts(function (err, migrationScripts) { 35 return getMigrationScripts().then(migrationScripts => ({ actualVersion, migrationScripts }))
53 return callback(err, actualVersion, migrationScripts) 36 })
37 .then(({ actualVersion, migrationScripts }) => {
38 return Promise.mapSeries(migrationScripts, entity => {
39 return executeMigration(actualVersion, entity)
54 }) 40 })
55 }, 41 })
42 .then(() => {
43 logger.info('Migrations finished. New migration version schema: %s', LAST_MIGRATION_VERSION)
44 })
45 .catch(err => {
46 if (err === null) return undefined
56 47
57 function doMigrations (actualVersion, migrationScripts, callback) { 48 throw err
58 eachSeries(migrationScripts, function (entity: any, callbackEach) { 49 })
59 executeMigration(actualVersion, entity, callbackEach)
60 }, function (err) {
61 if (err) return callback(err)
62 50
63 logger.info('Migrations finished. New migration version schema: %s', LAST_MIGRATION_VERSION) 51 return p
64 return callback(null)
65 })
66 }
67 ], finalCallback)
68} 52}
69 53
70// --------------------------------------------------------------------------- 54// ---------------------------------------------------------------------------
@@ -75,12 +59,12 @@ export {
75 59
76// --------------------------------------------------------------------------- 60// ---------------------------------------------------------------------------
77 61
78type GetMigrationScriptsCallback = (err: Error, filesToMigrate?: { version: string, script: string }[]) => void 62function getMigrationScripts () {
79function getMigrationScripts (callback: GetMigrationScriptsCallback) { 63 return readdirPromise(path.join(__dirname, 'migrations')).then(files => {
80 fs.readdir(path.join(__dirname, 'migrations'), function (err, files) { 64 const filesToMigrate: {
81 if (err) return callback(err) 65 version: string,
82 66 script: string
83 const filesToMigrate = [] 67 }[] = []
84 68
85 files.forEach(function (file) { 69 files.forEach(function (file) {
86 // Filename is something like 'version-blabla.js' 70 // Filename is something like 'version-blabla.js'
@@ -91,15 +75,15 @@ function getMigrationScripts (callback: GetMigrationScriptsCallback) {
91 }) 75 })
92 }) 76 })
93 77
94 return callback(err, filesToMigrate) 78 return filesToMigrate
95 }) 79 })
96} 80}
97 81
98function executeMigration (actualVersion: number, entity: { version: string, script: string }, callback: (err: Error) => void) { 82function executeMigration (actualVersion: number, entity: { version: string, script: string }) {
99 const versionScript = parseInt(entity.version, 10) 83 const versionScript = parseInt(entity.version, 10)
100 84
101 // Do not execute old migration scripts 85 // Do not execute old migration scripts
102 if (versionScript <= actualVersion) return callback(null) 86 if (versionScript <= actualVersion) return undefined
103 87
104 // Load the migration module and run it 88 // Load the migration module and run it
105 const migrationScriptName = entity.script 89 const migrationScriptName = entity.script
@@ -107,30 +91,17 @@ function executeMigration (actualVersion: number, entity: { version: string, scr
107 91
108 const migrationScript = require(path.join(__dirname, 'migrations', migrationScriptName)) 92 const migrationScript = require(path.join(__dirname, 'migrations', migrationScriptName))
109 93
110 db.sequelize.transaction().asCallback(function (err, t) { 94 return db.sequelize.transaction(t => {
111 if (err) return callback(err)
112
113 const options = { 95 const options = {
114 transaction: t, 96 transaction: t,
115 queryInterface: db.sequelize.getQueryInterface(), 97 queryInterface: db.sequelize.getQueryInterface(),
116 sequelize: db.sequelize, 98 sequelize: db.sequelize
117 Sequelize: Sequelize
118 } 99 }
119 migrationScript.up(options, function (err) {
120 if (err) {
121 t.rollback()
122 return callback(err)
123 }
124
125 // Update the new migration version
126 db.Application.updateMigrationVersion(versionScript, t, function (err) {
127 if (err) {
128 t.rollback()
129 return callback(err)
130 }
131 100
132 t.commit().asCallback(callback) 101 migrationScript.up(options)
102 .then(() => {
103 // Update the new migration version
104 db.Application.updateMigrationVersion(versionScript, t)
133 }) 105 })
134 })
135 }) 106 })
136} 107}
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 @@
1import { each, eachLimit, eachSeries, series, waterfall } from 'async'
2import * as request from 'request' 1import * as request from 'request'
3import * as Sequelize from 'sequelize' 2import * as Sequelize from 'sequelize'
3import * as Promise from 'bluebird'
4 4
5import { database as db } from '../initializers/database' 5import { database as db } from '../initializers/database'
6import { 6import {
@@ -15,8 +15,7 @@ import {
15 logger, 15 logger,
16 getMyPublicCert, 16 getMyPublicCert,
17 makeSecureRequest, 17 makeSecureRequest,
18 makeRetryRequest, 18 makeRetryRequest
19 createEmptyCallback
20} from '../helpers' 19} from '../helpers'
21import { 20import {
22 RequestScheduler, 21 RequestScheduler,
@@ -53,24 +52,24 @@ function activateSchedulers () {
53 requestVideoEventScheduler.activate() 52 requestVideoEventScheduler.activate()
54} 53}
55 54
56function addVideoToFriends (videoData: Object, transaction: Sequelize.Transaction, callback: (err: Error) => void) { 55function addVideoToFriends (videoData: Object, transaction: Sequelize.Transaction) {
57 const options = { 56 const options = {
58 type: ENDPOINT_ACTIONS.ADD, 57 type: ENDPOINT_ACTIONS.ADD,
59 endpoint: REQUEST_ENDPOINTS.VIDEOS, 58 endpoint: REQUEST_ENDPOINTS.VIDEOS,
60 data: videoData, 59 data: videoData,
61 transaction 60 transaction
62 } 61 }
63 createRequest(options, callback) 62 return createRequest(options)
64} 63}
65 64
66function updateVideoToFriends (videoData: Object, transaction: Sequelize.Transaction, callback: (err: Error) => void) { 65function updateVideoToFriends (videoData: Object, transaction: Sequelize.Transaction) {
67 const options = { 66 const options = {
68 type: ENDPOINT_ACTIONS.UPDATE, 67 type: ENDPOINT_ACTIONS.UPDATE,
69 endpoint: REQUEST_ENDPOINTS.VIDEOS, 68 endpoint: REQUEST_ENDPOINTS.VIDEOS,
70 data: videoData, 69 data: videoData,
71 transaction 70 transaction
72 } 71 }
73 createRequest(options, callback) 72 return createRequest(options)
74} 73}
75 74
76function removeVideoToFriends (videoParams: Object) { 75function removeVideoToFriends (videoParams: Object) {
@@ -80,121 +79,93 @@ function removeVideoToFriends (videoParams: Object) {
80 data: videoParams, 79 data: videoParams,
81 transaction: null 80 transaction: null
82 } 81 }
83 createRequest(options) 82 return createRequest(options)
84} 83}
85 84
86function reportAbuseVideoToFriend (reportData: Object, video: VideoInstance) { 85function reportAbuseVideoToFriend (reportData: Object, video: VideoInstance, transaction: Sequelize.Transaction) {
87 const options = { 86 const options = {
88 type: ENDPOINT_ACTIONS.REPORT_ABUSE, 87 type: ENDPOINT_ACTIONS.REPORT_ABUSE,
89 endpoint: REQUEST_ENDPOINTS.VIDEOS, 88 endpoint: REQUEST_ENDPOINTS.VIDEOS,
90 data: reportData, 89 data: reportData,
91 toIds: [ video.Author.podId ], 90 toIds: [ video.Author.podId ],
92 transaction: null 91 transaction
93 } 92 }
94 createRequest(options) 93 return createRequest(options)
95} 94}
96 95
97function quickAndDirtyUpdateVideoToFriends (qaduParam: QaduParam, transaction?: Sequelize.Transaction, callback?: (err: Error) => void) { 96function quickAndDirtyUpdateVideoToFriends (qaduParam: QaduParam, transaction?: Sequelize.Transaction) {
98 const options = { 97 const options = {
99 videoId: qaduParam.videoId, 98 videoId: qaduParam.videoId,
100 type: qaduParam.type, 99 type: qaduParam.type,
101 transaction 100 transaction
102 } 101 }
103 return createVideoQaduRequest(options, callback) 102 return createVideoQaduRequest(options)
104} 103}
105 104
106function quickAndDirtyUpdatesVideoToFriends ( 105function quickAndDirtyUpdatesVideoToFriends (qadusParams: QaduParam[], transaction: Sequelize.Transaction) {
107 qadusParams: QaduParam[],
108 transaction: Sequelize.Transaction,
109 finalCallback: (err: Error) => void
110) {
111 const tasks = [] 106 const tasks = []
112 107
113 qadusParams.forEach(function (qaduParams) { 108 qadusParams.forEach(function (qaduParams) {
114 const fun = function (callback) { 109 tasks.push(quickAndDirtyUpdateVideoToFriends(qaduParams, transaction))
115 quickAndDirtyUpdateVideoToFriends(qaduParams, transaction, callback)
116 }
117
118 tasks.push(fun)
119 }) 110 })
120 111
121 series(tasks, finalCallback) 112 return Promise.all(tasks)
122} 113}
123 114
124function addEventToRemoteVideo (eventParam: EventParam, transaction?: Sequelize.Transaction, callback?: (err: Error) => void) { 115function addEventToRemoteVideo (eventParam: EventParam, transaction?: Sequelize.Transaction) {
125 const options = { 116 const options = {
126 videoId: eventParam.videoId, 117 videoId: eventParam.videoId,
127 type: eventParam.type, 118 type: eventParam.type,
128 transaction 119 transaction
129 } 120 }
130 createVideoEventRequest(options, callback) 121 return createVideoEventRequest(options)
131} 122}
132 123
133function addEventsToRemoteVideo (eventsParams: EventParam[], transaction: Sequelize.Transaction, finalCallback: (err: Error) => void) { 124function addEventsToRemoteVideo (eventsParams: EventParam[], transaction: Sequelize.Transaction) {
134 const tasks = [] 125 const tasks = []
135 126
136 eventsParams.forEach(function (eventParams) { 127 eventsParams.forEach(function (eventParams) {
137 const fun = function (callback) { 128 tasks.push(addEventToRemoteVideo(eventParams, transaction))
138 addEventToRemoteVideo(eventParams, transaction, callback)
139 }
140
141 tasks.push(fun)
142 }) 129 })
143 130
144 series(tasks, finalCallback) 131 return Promise.all(tasks)
145} 132}
146 133
147function hasFriends (callback: (err: Error, hasFriends?: boolean) => void) { 134function hasFriends () {
148 db.Pod.countAll(function (err, count) { 135 return db.Pod.countAll().then(count => count !== 0)
149 if (err) return callback(err)
150
151 const hasFriends = (count !== 0)
152 callback(null, hasFriends)
153 })
154} 136}
155 137
156function makeFriends (hosts: string[], callback: (err: Error) => void) { 138function makeFriends (hosts: string[]) {
157 const podsScore = {} 139 const podsScore = {}
158 140
159 logger.info('Make friends!') 141 logger.info('Make friends!')
160 getMyPublicCert(function (err, cert) { 142 return getMyPublicCert()
161 if (err) { 143 .then(cert => {
162 logger.error('Cannot read public cert.') 144 return Promise.mapSeries(hosts, host => {
163 return callback(err) 145 return computeForeignPodsList(host, podsScore)
164 } 146 }).then(() => cert)
165 147 })
166 eachSeries(hosts, function (host, callbackEach) { 148 .then(cert => {
167 computeForeignPodsList(host, podsScore, callbackEach)
168 }, function (err: Error) {
169 if (err) return callback(err)
170
171 logger.debug('Pods scores computed.', { podsScore: podsScore }) 149 logger.debug('Pods scores computed.', { podsScore: podsScore })
172 const podsList = computeWinningPods(hosts, podsScore) 150 const podsList = computeWinningPods(hosts, podsScore)
173 logger.debug('Pods that we keep.', { podsToKeep: podsList }) 151 logger.debug('Pods that we keep.', { podsToKeep: podsList })
174 152
175 makeRequestsToWinningPods(cert, podsList, callback) 153 return makeRequestsToWinningPods(cert, podsList)
176 }) 154 })
177 })
178} 155}
179 156
180function quitFriends (callback: (err: Error) => void) { 157function quitFriends () {
181 // Stop pool requests 158 // Stop pool requests
182 requestScheduler.deactivate() 159 requestScheduler.deactivate()
183 160
184 waterfall([ 161 return requestScheduler.flush()
185 function flushRequests (callbackAsync) { 162 .then(() => {
186 requestScheduler.flush(err => callbackAsync(err)) 163 return requestVideoQaduScheduler.flush()
187 }, 164 })
188 165 .then(() => {
189 function flushVideoQaduRequests (callbackAsync) { 166 return db.Pod.list()
190 requestVideoQaduScheduler.flush(err => callbackAsync(err)) 167 })
191 }, 168 .then(pods => {
192
193 function getPodsList (callbackAsync) {
194 return db.Pod.list(callbackAsync)
195 },
196
197 function announceIQuitMyFriends (pods, callbackAsync) {
198 const requestParams = { 169 const requestParams = {
199 method: 'POST' as 'POST', 170 method: 'POST' as 'POST',
200 path: '/api/' + API_VERSION + '/remote/pods/remove', 171 path: '/api/' + API_VERSION + '/remote/pods/remove',
@@ -205,61 +176,57 @@ function quitFriends (callback: (err: Error) => void) {
205 // Announce we quit them 176 // Announce we quit them
206 // We don't care if the request fails 177 // We don't care if the request fails
207 // The other pod will exclude us automatically after a while 178 // The other pod will exclude us automatically after a while
208 eachLimit(pods, REQUESTS_IN_PARALLEL, function (pod, callbackEach) { 179 return Promise.map(pods, pod => {
209 requestParams.toPod = pod 180 requestParams.toPod = pod
210 makeSecureRequest(requestParams, callbackEach) 181 return makeSecureRequest(requestParams)
211 }, function (err) { 182 }, { concurrency: REQUESTS_IN_PARALLEL })
212 if (err) { 183 .then(() => pods)
213 logger.error('Some errors while quitting friends.', { err: err }) 184 .catch(err => {
214 // Don't stop the process 185 logger.error('Some errors while quitting friends.', { err: err })
215 } 186 // Don't stop the process
216
217 return callbackAsync(null, pods)
218 }) 187 })
219 }, 188 })
220 189 .then(pods => {
221 function removePodsFromDB (pods, callbackAsync) { 190 const tasks = []
222 each(pods, function (pod: any, callbackEach) { 191 pods.forEach(pod => tasks.push(pod.destroy()))
223 pod.destroy().asCallback(callbackEach)
224 }, callbackAsync)
225 }
226 ], function (err: Error) {
227 // Don't forget to re activate the scheduler, even if there was an error
228 requestScheduler.activate()
229
230 if (err) return callback(err)
231 192
232 logger.info('Removed all remote videos.') 193 return Promise.all(pods)
233 return callback(null) 194 })
234 }) 195 .then(() => {
196 logger.info('Removed all remote videos.')
197 // Don't forget to re activate the scheduler, even if there was an error
198 return requestScheduler.activate()
199 })
200 .finally(() => requestScheduler.activate())
235} 201}
236 202
237function sendOwnedVideosToPod (podId: number) { 203function sendOwnedVideosToPod (podId: number) {
238 db.Video.listOwnedAndPopulateAuthorAndTags(function (err, videosList) { 204 db.Video.listOwnedAndPopulateAuthorAndTags()
239 if (err) { 205 .then(videosList => {
240 logger.error('Cannot get the list of videos we own.') 206 const tasks = []
241 return 207 videosList.forEach(video => {
242 } 208 const promise = video.toAddRemoteJSON()
243 209 .then(remoteVideo => {
244 videosList.forEach(function (video) { 210 const options = {
245 video.toAddRemoteJSON(function (err, remoteVideo) { 211 type: 'add',
246 if (err) { 212 endpoint: REQUEST_ENDPOINTS.VIDEOS,
247 logger.error('Cannot convert video to remote.', { error: err }) 213 data: remoteVideo,
248 // Don't break the process 214 toIds: [ podId ],
249 return 215 transaction: null
250 } 216 }
251 217 return createRequest(options)
252 const options = { 218 })
253 type: 'add', 219 .catch(err => {
254 endpoint: REQUEST_ENDPOINTS.VIDEOS, 220 logger.error('Cannot convert video to remote.', { error: err })
255 data: remoteVideo, 221 // Don't break the process
256 toIds: [ podId ], 222 return undefined
257 transaction: null 223 })
258 } 224
259 createRequest(options) 225 tasks.push(promise)
260 }) 226 })
227
228 return Promise.all(tasks)
261 }) 229 })
262 })
263} 230}
264 231
265function getRequestScheduler () { 232function getRequestScheduler () {
@@ -297,23 +264,22 @@ export {
297 264
298// --------------------------------------------------------------------------- 265// ---------------------------------------------------------------------------
299 266
300function computeForeignPodsList (host: string, podsScore: { [ host: string ]: number }, callback: (err: Error) => void) { 267function computeForeignPodsList (host: string, podsScore: { [ host: string ]: number }) {
301 getForeignPodsList(host, function (err, res) { 268 // TODO: type res
302 if (err) return callback(err) 269 return getForeignPodsList(host).then((res: any) => {
303
304 const foreignPodsList = res.data 270 const foreignPodsList = res.data
305 271
306 // Let's give 1 point to the pod we ask the friends list 272 // Let's give 1 point to the pod we ask the friends list
307 foreignPodsList.push({ host }) 273 foreignPodsList.push({ host })
308 274
309 foreignPodsList.forEach(function (foreignPod) { 275 foreignPodsList.forEach(foreignPod => {
310 const foreignPodHost = foreignPod.host 276 const foreignPodHost = foreignPod.host
311 277
312 if (podsScore[foreignPodHost]) podsScore[foreignPodHost]++ 278 if (podsScore[foreignPodHost]) podsScore[foreignPodHost]++
313 else podsScore[foreignPodHost] = 1 279 else podsScore[foreignPodHost] = 1
314 }) 280 })
315 281
316 return callback(null) 282 return undefined
317 }) 283 })
318} 284}
319 285
@@ -323,7 +289,7 @@ function computeWinningPods (hosts: string[], podsScore: { [ host: string ]: num
323 const podsList = [] 289 const podsList = []
324 const baseScore = hosts.length / 2 290 const baseScore = hosts.length / 2
325 291
326 Object.keys(podsScore).forEach(function (podHost) { 292 Object.keys(podsScore).forEach(podHost => {
327 // If the pod is not me and with a good score we add it 293 // If the pod is not me and with a good score we add it
328 if (isMe(podHost) === false && podsScore[podHost] > baseScore) { 294 if (isMe(podHost) === false && podsScore[podHost] > baseScore) {
329 podsList.push({ host: podHost }) 295 podsList.push({ host: podHost })
@@ -333,28 +299,30 @@ function computeWinningPods (hosts: string[], podsScore: { [ host: string ]: num
333 return podsList 299 return podsList
334} 300}
335 301
336function getForeignPodsList (host: string, callback: (err: Error, foreignPodsList?: any) => void) { 302function getForeignPodsList (host: string) {
337 const path = '/api/' + API_VERSION + '/pods' 303 return new Promise((res, rej) => {
304 const path = '/api/' + API_VERSION + '/pods'
338 305
339 request.get(REMOTE_SCHEME.HTTP + '://' + host + path, function (err, response, body) { 306 request.get(REMOTE_SCHEME.HTTP + '://' + host + path, function (err, response, body) {
340 if (err) return callback(err) 307 if (err) return rej(err)
341 308
342 try { 309 try {
343 const json = JSON.parse(body) 310 const json = JSON.parse(body)
344 return callback(null, json) 311 return res(json)
345 } catch (err) { 312 } catch (err) {
346 return callback(err) 313 return rej(err)
347 } 314 }
315 })
348 }) 316 })
349} 317}
350 318
351function makeRequestsToWinningPods (cert: string, podsList: PodInstance[], callback: (err: Error) => void) { 319function makeRequestsToWinningPods (cert: string, podsList: PodInstance[]) {
352 // Stop pool requests 320 // Stop pool requests
353 requestScheduler.deactivate() 321 requestScheduler.deactivate()
354 // Flush pool requests 322 // Flush pool requests
355 requestScheduler.forceSend() 323 requestScheduler.forceSend()
356 324
357 eachLimit(podsList, REQUESTS_IN_PARALLEL, function (pod: PodInstance, callbackEach) { 325 return Promise.map(podsList, pod => {
358 const params = { 326 const params = {
359 url: REMOTE_SCHEME.HTTP + '://' + pod.host + '/api/' + API_VERSION + '/pods/', 327 url: REMOTE_SCHEME.HTTP + '://' + pod.host + '/api/' + API_VERSION + '/pods/',
360 method: 'POST' as 'POST', 328 method: 'POST' as 'POST',
@@ -365,38 +333,35 @@ function makeRequestsToWinningPods (cert: string, podsList: PodInstance[], callb
365 } 333 }
366 } 334 }
367 335
368 makeRetryRequest(params, function (err, res, body: { cert: string, email: string }) { 336 return makeRetryRequest(params)
369 if (err) { 337 .then(({ response, body }) => {
370 logger.error('Error with adding %s pod.', pod.host, { error: err }) 338 body = body as { cert: string, email: string }
339
340 if (response.statusCode === 200) {
341 const podObj = db.Pod.build({ host: pod.host, publicKey: body.cert, email: body.email })
342 return podObj.save()
343 .then(podCreated => {
344
345 // Add our videos to the request scheduler
346 sendOwnedVideosToPod(podCreated.id)
347 })
348 .catch(err => {
349 logger.error('Cannot add friend %s pod.', pod.host, { error: err })
350 })
351 } else {
352 logger.error('Status not 200 for %s pod.', pod.host)
353 }
354 })
355 .catch(err => {
356 logger.error('Error with adding %s pod.', pod.host, { error: err.stack })
371 // Don't break the process 357 // Don't break the process
372 return callbackEach() 358 })
373 } 359 }, { concurrency: REQUESTS_IN_PARALLEL })
374 360 .then(() => logger.debug('makeRequestsToWinningPods finished.'))
375 if (res.statusCode === 200) { 361 .finally(() => {
376 const podObj = db.Pod.build({ host: pod.host, publicKey: body.cert, email: body.email })
377 podObj.save().asCallback(function (err, podCreated) {
378 if (err) {
379 logger.error('Cannot add friend %s pod.', pod.host, { error: err })
380 return callbackEach()
381 }
382
383 // Add our videos to the request scheduler
384 sendOwnedVideosToPod(podCreated.id)
385
386 return callbackEach()
387 })
388 } else {
389 logger.error('Status not 200 for %s pod.', pod.host)
390 return callbackEach()
391 }
392 })
393 }, function endRequests () {
394 // Final callback, we've ended all the requests 362 // Final callback, we've ended all the requests
395 // Now we made new friends, we can re activate the pool of requests 363 // Now we made new friends, we can re activate the pool of requests
396 requestScheduler.activate() 364 requestScheduler.activate()
397
398 logger.debug('makeRequestsToWinningPods finished.')
399 return callback(null)
400 }) 365 })
401} 366}
402 367
@@ -408,33 +373,22 @@ type CreateRequestOptions = {
408 toIds?: number[] 373 toIds?: number[]
409 transaction: Sequelize.Transaction 374 transaction: Sequelize.Transaction
410} 375}
411function createRequest (options: CreateRequestOptions, callback?: (err: Error) => void) { 376function createRequest (options: CreateRequestOptions) {
412 if (!callback) callback = function () { /* empty */ } 377 if (options.toIds !== undefined) return requestScheduler.createRequest(options as RequestSchedulerOptions)
413
414 if (options.toIds !== undefined) return requestScheduler.createRequest(options as RequestSchedulerOptions, callback)
415 378
416 // If the "toIds" pods is not specified, we send the request to all our friends 379 // If the "toIds" pods is not specified, we send the request to all our friends
417 db.Pod.listAllIds(options.transaction, function (err, podIds) { 380 return db.Pod.listAllIds(options.transaction).then(podIds => {
418 if (err) {
419 logger.error('Cannot get pod ids', { error: err })
420 return
421 }
422
423 const newOptions = Object.assign(options, { toIds: podIds }) 381 const newOptions = Object.assign(options, { toIds: podIds })
424 return requestScheduler.createRequest(newOptions, callback) 382 return requestScheduler.createRequest(newOptions)
425 }) 383 })
426} 384}
427 385
428function createVideoQaduRequest (options: RequestVideoQaduSchedulerOptions, callback: (err: Error) => void) { 386function createVideoQaduRequest (options: RequestVideoQaduSchedulerOptions) {
429 if (!callback) callback = createEmptyCallback() 387 return requestVideoQaduScheduler.createRequest(options)
430
431 requestVideoQaduScheduler.createRequest(options, callback)
432} 388}
433 389
434function createVideoEventRequest (options: RequestVideoEventSchedulerOptions, callback: (err: Error) => void) { 390function createVideoEventRequest (options: RequestVideoEventSchedulerOptions) {
435 if (!callback) callback = createEmptyCallback() 391 return requestVideoEventScheduler.createRequest(options)
436
437 requestVideoEventScheduler.createRequest(options, callback)
438} 392}
439 393
440function isMe (host: string) { 394function 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 @@
1import * as videoTranscoder from './video-transcoder' 1import * as videoTranscoder from './video-transcoder'
2 2
3import { VideoInstance } from '../../../models'
4
5export interface JobHandler<T> { 3export interface JobHandler<T> {
6 process (data: object, callback: (err: Error, videoInstance?: T) => void) 4 process (data: object): T
7 onError (err: Error, jobId: number, video: T, callback: (err: Error) => void) 5 onError (err: Error, jobId: number)
8 onSuccess (data: any, jobId: number, video: T, callback: (err: Error) => void) 6 onSuccess (jobId: number, jobResult: T)
9} 7}
10 8
11const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = { 9const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = {
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'
3import { addVideoToFriends } from '../../../lib' 3import { addVideoToFriends } from '../../../lib'
4import { VideoInstance } from '../../../models' 4import { VideoInstance } from '../../../models'
5 5
6function process (data: { id: string }, callback: (err: Error, videoInstance?: VideoInstance) => void) { 6function process (data: { id: string }) {
7 db.Video.loadAndPopulateAuthorAndPodAndTags(data.id, function (err, video) { 7 return db.Video.loadAndPopulateAuthorAndPodAndTags(data.id).then(video => {
8 if (err) return callback(err) 8 return video.transcodeVideofile().then(() => video)
9
10 video.transcodeVideofile(function (err) {
11 return callback(err, video)
12 })
13 }) 9 })
14} 10}
15 11
16function onError (err: Error, jobId: number, video: VideoInstance, callback: (err: Error) => void) { 12function onError (err: Error, jobId: number) {
17 logger.error('Error when transcoding video file in job %d.', jobId, { error: err }) 13 logger.error('Error when transcoding video file in job %d.', jobId, { error: err })
18 return callback(null) 14 return Promise.resolve()
19} 15}
20 16
21function onSuccess (data: any, jobId: number, video: VideoInstance, callback: (err: Error) => void) { 17function onSuccess (jobId: number, video: VideoInstance) {
22 logger.info('Job %d is a success.', jobId) 18 logger.info('Job %d is a success.', jobId)
23 19
24 video.toAddRemoteJSON(function (err, remoteVideo) { 20 video.toAddRemoteJSON().then(remoteVideo => {
25 if (err) return callback(err)
26
27 // Now we'll add the video's meta data to our friends 21 // Now we'll add the video's meta data to our friends
28 addVideoToFriends(remoteVideo, null, callback) 22 return addVideoToFriends(remoteVideo, null)
29 }) 23 })
30} 24}
31 25
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 {
32 32
33 // Finish processing jobs from a previous start 33 // Finish processing jobs from a previous start
34 const state = JOB_STATES.PROCESSING 34 const state = JOB_STATES.PROCESSING
35 db.Job.listWithLimit(limit, state, (err, jobs) => { 35 db.Job.listWithLimit(limit, state)
36 this.enqueueJobs(err, jobsQueue, jobs) 36 .then(jobs => {
37 37 this.enqueueJobs(jobsQueue, jobs)
38 forever( 38
39 next => { 39 forever(
40 if (jobsQueue.length() !== 0) { 40 next => {
41 // Finish processing the queue first 41 if (jobsQueue.length() !== 0) {
42 return setTimeout(next, JOBS_FETCHING_INTERVAL) 42 // Finish processing the queue first
43 } 43 return setTimeout(next, JOBS_FETCHING_INTERVAL)
44
45 const state = JOB_STATES.PENDING
46 db.Job.listWithLimit(limit, state, (err, jobs) => {
47 if (err) {
48 logger.error('Cannot list pending jobs.', { error: err })
49 } else {
50 jobs.forEach(job => {
51 jobsQueue.push(job)
52 })
53 } 44 }
54 45
55 // Optimization: we could use "drain" from queue object 46 const state = JOB_STATES.PENDING
56 return setTimeout(next, JOBS_FETCHING_INTERVAL) 47 db.Job.listWithLimit(limit, state)
57 }) 48 .then(jobs => {
58 }, 49 this.enqueueJobs(jobsQueue, jobs)
59 50
60 err => { logger.error('Error in job scheduler queue.', { error: err }) } 51 // Optimization: we could use "drain" from queue object
61 ) 52 return setTimeout(next, JOBS_FETCHING_INTERVAL)
62 }) 53 })
54 .catch(err => logger.error('Cannot list pending jobs.', { error: err }))
55 },
56
57 err => logger.error('Error in job scheduler queue.', { error: err })
58 )
59 })
60 .catch(err => logger.error('Cannot list pending jobs.', { error: err }))
63 } 61 }
64 62
65 createJob (transaction: Sequelize.Transaction, handlerName: string, handlerInputData: object, callback: (err: Error) => void) { 63 createJob (transaction: Sequelize.Transaction, handlerName: string, handlerInputData: object) {
66 const createQuery = { 64 const createQuery = {
67 state: JOB_STATES.PENDING, 65 state: JOB_STATES.PENDING,
68 handlerName, 66 handlerName,
@@ -70,67 +68,62 @@ class JobScheduler {
70 } 68 }
71 const options = { transaction } 69 const options = { transaction }
72 70
73 db.Job.create(createQuery, options).asCallback(callback) 71 return db.Job.create(createQuery, options)
74 } 72 }
75 73
76 private enqueueJobs (err: Error, jobsQueue: AsyncQueue<JobInstance>, jobs: JobInstance[]) { 74 private enqueueJobs (jobsQueue: AsyncQueue<JobInstance>, jobs: JobInstance[]) {
77 if (err) { 75 jobs.forEach(job => jobsQueue.push(job))
78 logger.error('Cannot list pending jobs.', { error: err })
79 } else {
80 jobs.forEach(job => {
81 jobsQueue.push(job)
82 })
83 }
84 } 76 }
85 77
86 private processJob (job: JobInstance, callback: (err: Error) => void) { 78 private processJob (job: JobInstance, callback: (err: Error) => void) {
87 const jobHandler = jobHandlers[job.handlerName] 79 const jobHandler = jobHandlers[job.handlerName]
80 if (jobHandler === undefined) {
81 logger.error('Unknown job handler for job %s.', job.handlerName)
82 return callback(null)
83 }
88 84
89 logger.info('Processing job %d with handler %s.', job.id, job.handlerName) 85 logger.info('Processing job %d with handler %s.', job.id, job.handlerName)
90 86
91 job.state = JOB_STATES.PROCESSING 87 job.state = JOB_STATES.PROCESSING
92 job.save().asCallback(err => { 88 return job.save()
93 if (err) return this.cannotSaveJobError(err, callback) 89 .then(() => {
94 90 return jobHandler.process(job.handlerInputData)
95 if (jobHandler === undefined) { 91 })
96 logger.error('Unknown job handler for job %s.', job.handlerName) 92 .then(
97 return callback(null) 93 result => {
98 } 94 return this.onJobSuccess(jobHandler, job, result)
95 },
99 96
100 return jobHandler.process(job.handlerInputData, (err, result) => { 97 err => {
101 if (err) {
102 logger.error('Error in job handler %s.', job.handlerName, { error: err }) 98 logger.error('Error in job handler %s.', job.handlerName, { error: err })
103 return this.onJobError(jobHandler, job, result, callback) 99 return this.onJobError(jobHandler, job, err)
104 } 100 }
105 101 )
106 return this.onJobSuccess(jobHandler, job, result, callback) 102 .then(() => callback(null))
103 .catch(err => {
104 this.cannotSaveJobError(err)
105 return callback(err)
107 }) 106 })
108 })
109 } 107 }
110 108
111 private onJobError (jobHandler: JobHandler<any>, job: JobInstance, jobResult: any, callback: (err: Error) => void) { 109 private onJobError (jobHandler: JobHandler<any>, job: JobInstance, err: Error) {
112 job.state = JOB_STATES.ERROR 110 job.state = JOB_STATES.ERROR
113 111
114 job.save().asCallback(err => { 112 return job.save()
115 if (err) return this.cannotSaveJobError(err, callback) 113 .then(() => jobHandler.onError(err, job.id))
116 114 .catch(err => this.cannotSaveJobError(err))
117 return jobHandler.onError(err, job.id, jobResult, callback)
118 })
119 } 115 }
120 116
121 private onJobSuccess (jobHandler: JobHandler<any>, job: JobInstance, jobResult: any, callback: (err: Error) => void) { 117 private onJobSuccess (jobHandler: JobHandler<any>, job: JobInstance, jobResult: any) {
122 job.state = JOB_STATES.SUCCESS 118 job.state = JOB_STATES.SUCCESS
123 119
124 job.save().asCallback(err => { 120 return job.save()
125 if (err) return this.cannotSaveJobError(err, callback) 121 .then(() => jobHandler.onSuccess(job.id, jobResult))
126 122 .catch(err => this.cannotSaveJobError(err))
127 return jobHandler.onSuccess(err, job.id, jobResult, callback)
128 })
129 } 123 }
130 124
131 private cannotSaveJobError (err: Error, callback: (err: Error) => void) { 125 private cannotSaveJobError (err: Error) {
132 logger.error('Cannot save new job state.', { error: err }) 126 logger.error('Cannot save new job state.', { error: err })
133 return callback(err)
134 } 127 }
135} 128}
136 129
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) {
30 return db.User.getByUsername(username).then(function (user) { 30 return db.User.getByUsername(username).then(function (user) {
31 if (!user) return null 31 if (!user) return null
32 32
33 // We need to return a promise 33 return user.isPasswordMatch(password).then(passwordMatch => {
34 return new Promise(function (resolve, reject) { 34 if (passwordMatch === false) return null
35 return user.isPasswordMatch(password, function (err, isPasswordMatch) {
36 if (err) return reject(err)
37 35
38 if (isPasswordMatch === true) { 36 return user
39 return resolve(user)
40 }
41
42 return resolve(null)
43 })
44 }) 37 })
45 }) 38 })
46} 39}
@@ -80,8 +73,6 @@ function saveToken (token: TokenInfo, client: OAuthClientInstance, user: UserIns
80 tokenCreated.user = user 73 tokenCreated.user = user
81 74
82 return tokenCreated 75 return tokenCreated
83 }).catch(function (err) {
84 throw err
85 }) 76 })
86} 77}
87 78
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 @@
1import * as eachLimit from 'async/eachLimit' 1import { isEmpty } from 'lodash'
2import * as Promise from 'bluebird'
2 3
3import { database as db } from '../../initializers/database' 4import { database as db } from '../../initializers/database'
4import { logger, makeSecureRequest } from '../../helpers' 5import { logger, makeSecureRequest } from '../../helpers'
5import { PodInstance } from '../../models' 6import { AbstractRequestClass, AbstractRequestToPodClass, PodInstance } from '../../models'
6import { 7import {
7 API_VERSION, 8 API_VERSION,
8 REQUESTS_IN_PARALLEL, 9 REQUESTS_IN_PARALLEL,
9 REQUESTS_INTERVAL 10 REQUESTS_INTERVAL
10} from '../../initializers' 11} from '../../initializers'
11 12
12abstract class AbstractRequestScheduler { 13abstract class AbstractRequestScheduler <T> {
13 requestInterval: number 14 requestInterval: number
14 limitPods: number 15 limitPods: number
15 limitPerPod: number 16 limitPerPod: number
@@ -24,9 +25,9 @@ abstract class AbstractRequestScheduler {
24 this.requestInterval = REQUESTS_INTERVAL 25 this.requestInterval = REQUESTS_INTERVAL
25 } 26 }
26 27
27 abstract getRequestModel () 28 abstract getRequestModel (): AbstractRequestClass<T>
28 abstract getRequestToPodModel () 29 abstract getRequestToPodModel (): AbstractRequestToPodClass
29 abstract buildRequestObjects (requests: any) 30 abstract buildRequestObjects (requestsGrouped: T): {}
30 31
31 activate () { 32 activate () {
32 logger.info('Requests scheduler activated.') 33 logger.info('Requests scheduler activated.')
@@ -55,20 +56,18 @@ abstract class AbstractRequestScheduler {
55 return REQUESTS_INTERVAL - (Date.now() - this.lastRequestTimestamp) 56 return REQUESTS_INTERVAL - (Date.now() - this.lastRequestTimestamp)
56 } 57 }
57 58
58 remainingRequestsCount (callback: (err: Error, total: number) => void) { 59 remainingRequestsCount () {
59 return this.getRequestModel().countTotalRequests(callback) 60 return this.getRequestModel().countTotalRequests()
60 } 61 }
61 62
62 flush (callback: (err: Error) => void) { 63 flush () {
63 this.getRequestModel().removeAll(callback) 64 return this.getRequestModel().removeAll()
64 } 65 }
65 66
66 // --------------------------------------------------------------------------- 67 // ---------------------------------------------------------------------------
67 68
68 // Make a requests to friends of a certain type 69 // Make a requests to friends of a certain type
69 protected makeRequest (toPod: PodInstance, requestEndpoint: string, requestsToMake: Object, callback) { 70 protected makeRequest (toPod: PodInstance, requestEndpoint: string, requestsToMake: Object) {
70 if (!callback) callback = function () { /* empty */ }
71
72 const params = { 71 const params = {
73 toPod: toPod, 72 toPod: toPod,
74 sign: true, // Prove our identity 73 sign: true, // Prove our identity
@@ -79,65 +78,64 @@ abstract class AbstractRequestScheduler {
79 78
80 // Make multiple retry requests to all of pods 79 // Make multiple retry requests to all of pods
81 // The function fire some useful callbacks 80 // The function fire some useful callbacks
82 makeSecureRequest(params, (err, res) => { 81 return makeSecureRequest(params)
83 if (err || (res.statusCode !== 200 && res.statusCode !== 201 && res.statusCode !== 204)) { 82 .then(({ response, body }) => {
84 err = err ? err.message : 'Status code not 20x : ' + res.statusCode 83 if (response.statusCode !== 200 && response.statusCode !== 201 && response.statusCode !== 204) {
84 throw new Error('Status code not 20x : ' + response.statusCode)
85 }
86 })
87 .catch(err => {
85 logger.error('Error sending secure request to %s pod.', toPod.host, { error: err }) 88 logger.error('Error sending secure request to %s pod.', toPod.host, { error: err })
86 89
87 return callback(err) 90 throw err
88 } 91 })
89
90 return callback(null)
91 })
92 } 92 }
93 93
94 // Make all the requests of the scheduler 94 // Make all the requests of the scheduler
95 protected makeRequests () { 95 protected makeRequests () {
96 this.getRequestModel().listWithLimitAndRandom(this.limitPods, this.limitPerPod, (err, requests) => { 96 return this.getRequestModel().listWithLimitAndRandom(this.limitPods, this.limitPerPod)
97 if (err) { 97 .then((requestsGrouped: T) => {
98 logger.error('Cannot get the list of "%s".', this.description, { err: err }) 98 // We want to group requests by destinations pod and endpoint
99 return // Abort 99 const requestsToMake = this.buildRequestObjects(requestsGrouped)
100 } 100
101 101 // If there are no requests, abort
102 // If there are no requests, abort 102 if (isEmpty(requestsToMake) === true) {
103 if (requests.length === 0) { 103 logger.info('No "%s" to make.', this.description)
104 logger.info('No "%s" to make.', this.description) 104 return { goodPods: [], badPods: [] }
105 return 105 }
106 } 106
107 107 logger.info('Making "%s" to friends.', this.description)
108 // We want to group requests by destinations pod and endpoint 108
109 const requestsToMakeGrouped = this.buildRequestObjects(requests) 109 const goodPods = []
110 110 const badPods = []
111 logger.info('Making "%s" to friends.', this.description) 111
112 112 return Promise.map(Object.keys(requestsToMake), hashKey => {
113 const goodPods = [] 113 const requestToMake = requestsToMake[hashKey]
114 const badPods = [] 114 const toPod: PodInstance = requestToMake.toPod
115 115
116 eachLimit(Object.keys(requestsToMakeGrouped), REQUESTS_IN_PARALLEL, (hashKey, callbackEach) => { 116 return this.makeRequest(toPod, requestToMake.endpoint, requestToMake.datas)
117 const requestToMake = requestsToMakeGrouped[hashKey] 117 .then(() => {
118 const toPod = requestToMake.toPod 118 logger.debug('Removing requests for pod %s.', requestToMake.toPod.id, { requestsIds: requestToMake.ids })
119 119 goodPods.push(requestToMake.toPod.id)
120 this.makeRequest(toPod, requestToMake.endpoint, requestToMake.datas, (err) => { 120
121 if (err) { 121 this.afterRequestHook()
122 badPods.push(requestToMake.toPod.id) 122
123 return callbackEach() 123 // Remove the pod id of these request ids
124 } 124 return this.getRequestToPodModel().removeByRequestIdsAndPod(requestToMake.ids, requestToMake.toPod.id)
125 125 })
126 logger.debug('Removing requests for pod %s.', requestToMake.toPod.id, { requestsIds: requestToMake.ids }) 126 .catch(err => {
127 goodPods.push(requestToMake.toPod.id) 127 badPods.push(requestToMake.toPod.id)
128 128 logger.info('Cannot make request to %s.', toPod.host, { error: err })
129 // Remove the pod id of these request ids 129 })
130 this.getRequestToPodModel().removeByRequestIdsAndPod(requestToMake.ids, requestToMake.toPod.id, callbackEach) 130 }, { concurrency: REQUESTS_IN_PARALLEL }).then(() => ({ goodPods, badPods }))
131 })
132 .then(({ goodPods, badPods }) => {
133 this.afterRequestsHook()
131 134
132 this.afterRequestHook()
133 })
134 }, () => {
135 // All the requests were made, we update the pods score 135 // All the requests were made, we update the pods score
136 db.Pod.updatePodsScore(goodPods, badPods) 136 return db.Pod.updatePodsScore(goodPods, badPods)
137
138 this.afterRequestsHook()
139 }) 137 })
140 }) 138 .catch(err => logger.error('Cannot get the list of "%s".', this.description, { error: err.stack }))
141 } 139 }
142 140
143 protected afterRequestHook () { 141 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'
3import { database as db } from '../../initializers/database' 3import { database as db } from '../../initializers/database'
4import { AbstractRequestScheduler } from './abstract-request-scheduler' 4import { AbstractRequestScheduler } from './abstract-request-scheduler'
5import { logger } from '../../helpers' 5import { logger } from '../../helpers'
6import { 6import { REQUESTS_LIMIT_PODS, REQUESTS_LIMIT_PER_POD } from '../../initializers'
7 REQUESTS_LIMIT_PODS, 7import { RequestsGrouped } from '../../models'
8 REQUESTS_LIMIT_PER_POD
9} from '../../initializers'
10import { RequestEndpoint } from '../../../shared' 8import { RequestEndpoint } from '../../../shared'
11 9
12export type RequestSchedulerOptions = { 10export type RequestSchedulerOptions = {
@@ -17,7 +15,7 @@ export type RequestSchedulerOptions = {
17 transaction: Sequelize.Transaction 15 transaction: Sequelize.Transaction
18} 16}
19 17
20class RequestScheduler extends AbstractRequestScheduler { 18class RequestScheduler extends AbstractRequestScheduler<RequestsGrouped> {
21 constructor () { 19 constructor () {
22 super() 20 super()
23 21
@@ -36,11 +34,11 @@ class RequestScheduler extends AbstractRequestScheduler {
36 return db.RequestToPod 34 return db.RequestToPod
37 } 35 }
38 36
39 buildRequestObjects (requests: { [ toPodId: number ]: any }) { 37 buildRequestObjects (requestsGrouped: RequestsGrouped) {
40 const requestsToMakeGrouped = {} 38 const requestsToMakeGrouped = {}
41 39
42 Object.keys(requests).forEach(toPodId => { 40 Object.keys(requestsGrouped).forEach(toPodId => {
43 requests[toPodId].forEach(data => { 41 requestsGrouped[toPodId].forEach(data => {
44 const request = data.request 42 const request = data.request
45 const pod = data.pod 43 const pod = data.pod
46 const hashKey = toPodId + request.endpoint 44 const hashKey = toPodId + request.endpoint
@@ -62,12 +60,12 @@ class RequestScheduler extends AbstractRequestScheduler {
62 return requestsToMakeGrouped 60 return requestsToMakeGrouped
63 } 61 }
64 62
65 createRequest ({ type, endpoint, data, toIds, transaction }: RequestSchedulerOptions, callback: (err: Error) => void) { 63 createRequest ({ type, endpoint, data, toIds, transaction }: RequestSchedulerOptions) {
66 // TODO: check the setPods works 64 // TODO: check the setPods works
67 const podIds = [] 65 const podIds = []
68 66
69 // If there are no destination pods abort 67 // If there are no destination pods abort
70 if (toIds.length === 0) return callback(null) 68 if (toIds.length === 0) return undefined
71 69
72 toIds.forEach(toPod => { 70 toIds.forEach(toPod => {
73 podIds.push(toPod) 71 podIds.push(toPod)
@@ -85,20 +83,18 @@ class RequestScheduler extends AbstractRequestScheduler {
85 transaction 83 transaction
86 } 84 }
87 85
88 return db.Request.create(createQuery, dbRequestOptions).asCallback((err, request) => { 86 return db.Request.create(createQuery, dbRequestOptions)
89 if (err) return callback(err) 87 .then(request => {
90 88 return request.setPods(podIds, dbRequestOptions)
91 return request.setPods(podIds, dbRequestOptions).asCallback(callback) 89 })
92 })
93 } 90 }
94 91
95 // --------------------------------------------------------------------------- 92 // ---------------------------------------------------------------------------
96 93
97 afterRequestsHook () { 94 afterRequestsHook () {
98 // Flush requests with no pod 95 // Flush requests with no pod
99 this.getRequestModel().removeWithEmptyTo(err => { 96 this.getRequestModel().removeWithEmptyTo()
100 if (err) logger.error('Error when removing requests with no pods.', { error: err }) 97 .catch(err => logger.error('Error when removing requests with no pods.', { error: err }))
101 })
102 } 98 }
103} 99}
104 100
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 {
7 REQUESTS_VIDEO_EVENT_LIMIT_PER_POD, 7 REQUESTS_VIDEO_EVENT_LIMIT_PER_POD,
8 REQUEST_VIDEO_EVENT_ENDPOINT 8 REQUEST_VIDEO_EVENT_ENDPOINT
9} from '../../initializers' 9} from '../../initializers'
10import { RequestsVideoEventGrouped } from '../../models'
10import { RequestVideoEventType } from '../../../shared' 11import { RequestVideoEventType } from '../../../shared'
11 12
12export type RequestVideoEventSchedulerOptions = { 13export type RequestVideoEventSchedulerOptions = {
@@ -16,7 +17,7 @@ export type RequestVideoEventSchedulerOptions = {
16 transaction?: Sequelize.Transaction 17 transaction?: Sequelize.Transaction
17} 18}
18 19
19class RequestVideoEventScheduler extends AbstractRequestScheduler { 20class RequestVideoEventScheduler extends AbstractRequestScheduler<RequestsVideoEventGrouped> {
20 constructor () { 21 constructor () {
21 super() 22 super()
22 23
@@ -35,7 +36,7 @@ class RequestVideoEventScheduler extends AbstractRequestScheduler {
35 return db.RequestVideoEvent 36 return db.RequestVideoEvent
36 } 37 }
37 38
38 buildRequestObjects (eventsToProcess: { [ toPodId: number ]: any }[]) { 39 buildRequestObjects (eventRequests: RequestsVideoEventGrouped) {
39 const requestsToMakeGrouped = {} 40 const requestsToMakeGrouped = {}
40 41
41 /* Example: 42 /* Example:
@@ -50,8 +51,8 @@ class RequestVideoEventScheduler extends AbstractRequestScheduler {
50 51
51 // We group video events per video and per pod 52 // We group video events per video and per pod
52 // We add the counts of the same event types 53 // We add the counts of the same event types
53 Object.keys(eventsToProcess).forEach(toPodId => { 54 Object.keys(eventRequests).forEach(toPodId => {
54 eventsToProcess[toPodId].forEach(eventToProcess => { 55 eventRequests[toPodId].forEach(eventToProcess => {
55 if (!eventsPerVideoPerPod[toPodId]) eventsPerVideoPerPod[toPodId] = {} 56 if (!eventsPerVideoPerPod[toPodId]) eventsPerVideoPerPod[toPodId] = {}
56 57
57 if (!requestsToMakeGrouped[toPodId]) { 58 if (!requestsToMakeGrouped[toPodId]) {
@@ -97,7 +98,7 @@ class RequestVideoEventScheduler extends AbstractRequestScheduler {
97 return requestsToMakeGrouped 98 return requestsToMakeGrouped
98 } 99 }
99 100
100 createRequest ({ type, videoId, count, transaction }: RequestVideoEventSchedulerOptions, callback: (err: Error) => void) { 101 createRequest ({ type, videoId, count, transaction }: RequestVideoEventSchedulerOptions) {
101 if (count === undefined) count = 1 102 if (count === undefined) count = 1
102 103
103 const dbRequestOptions: Sequelize.CreateOptions = {} 104 const dbRequestOptions: Sequelize.CreateOptions = {}
@@ -109,7 +110,7 @@ class RequestVideoEventScheduler extends AbstractRequestScheduler {
109 videoId 110 videoId
110 } 111 }
111 112
112 return db.RequestVideoEvent.create(createQuery, dbRequestOptions).asCallback(callback) 113 return db.RequestVideoEvent.create(createQuery, dbRequestOptions)
113 } 114 }
114} 115}
115 116
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 {
9 REQUEST_VIDEO_QADU_ENDPOINT, 9 REQUEST_VIDEO_QADU_ENDPOINT,
10 REQUEST_VIDEO_QADU_TYPES 10 REQUEST_VIDEO_QADU_TYPES
11} from '../../initializers' 11} from '../../initializers'
12import { RequestsVideoQaduGrouped } from '../../models'
12import { RequestVideoQaduType } from '../../../shared' 13import { RequestVideoQaduType } from '../../../shared'
13 14
14export type RequestVideoQaduSchedulerOptions = { 15export type RequestVideoQaduSchedulerOptions = {
@@ -17,7 +18,7 @@ export type RequestVideoQaduSchedulerOptions = {
17 transaction?: Sequelize.Transaction 18 transaction?: Sequelize.Transaction
18} 19}
19 20
20class RequestVideoQaduScheduler extends AbstractRequestScheduler { 21class RequestVideoQaduScheduler extends AbstractRequestScheduler<RequestsVideoQaduGrouped> {
21 constructor () { 22 constructor () {
22 super() 23 super()
23 24
@@ -36,7 +37,7 @@ class RequestVideoQaduScheduler extends AbstractRequestScheduler {
36 return db.RequestVideoQadu 37 return db.RequestVideoQadu
37 } 38 }
38 39
39 buildRequestObjects (requests: { [ toPodId: number ]: any }[]) { 40 buildRequestObjects (requests: RequestsVideoQaduGrouped) {
40 const requestsToMakeGrouped = {} 41 const requestsToMakeGrouped = {}
41 42
42 Object.keys(requests).forEach(toPodId => { 43 Object.keys(requests).forEach(toPodId => {
@@ -105,20 +106,18 @@ class RequestVideoQaduScheduler extends AbstractRequestScheduler {
105 return requestsToMakeGrouped 106 return requestsToMakeGrouped
106 } 107 }
107 108
108 createRequest ({ type, videoId, transaction }: RequestVideoQaduSchedulerOptions, callback: (err: Error) => void) { 109 createRequest ({ type, videoId, transaction }: RequestVideoQaduSchedulerOptions) {
109 const dbRequestOptions: Sequelize.BulkCreateOptions = {} 110 const dbRequestOptions: Sequelize.BulkCreateOptions = {}
110 if (transaction) dbRequestOptions.transaction = transaction 111 if (transaction) dbRequestOptions.transaction = transaction
111 112
112 // Send the update to all our friends 113 // Send the update to all our friends
113 db.Pod.listAllIds(transaction, function (err, podIds) { 114 return db.Pod.listAllIds(transaction).then(podIds => {
114 if (err) return callback(err)
115
116 const queries = [] 115 const queries = []
117 podIds.forEach(podId => { 116 podIds.forEach(podId => {
118 queries.push({ type, videoId, podId }) 117 queries.push({ type, videoId, podId })
119 }) 118 })
120 119
121 return db.RequestVideoQadu.bulkCreate(queries, dbRequestOptions).asCallback(callback) 120 return db.RequestVideoQadu.bulkCreate(queries, dbRequestOptions)
122 }) 121 })
123 } 122 }
124} 123}
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 {
9 9
10function checkSignature (req: express.Request, res: express.Response, next: express.NextFunction) { 10function checkSignature (req: express.Request, res: express.Response, next: express.NextFunction) {
11 const host = req.body.signature.host 11 const host = req.body.signature.host
12 db.Pod.loadByHost(host, function (err, pod) { 12 db.Pod.loadByHost(host)
13 if (err) { 13 .then(pod => {
14 logger.error('Cannot get signed host in body.', { error: err }) 14 if (pod === null) {
15 return res.sendStatus(500) 15 logger.error('Unknown pod %s.', host)
16 } 16 return res.sendStatus(403)
17 }
17 18
18 if (pod === null) { 19 logger.debug('Checking signature from %s.', host)
19 logger.error('Unknown pod %s.', host)
20 return res.sendStatus(403)
21 }
22 20
23 logger.debug('Checking signature from %s.', host) 21 let signatureShouldBe
22 // If there is data in the body the sender used it for its signature
23 // If there is no data we just use its host as signature
24 if (req.body.data) {
25 signatureShouldBe = req.body.data
26 } else {
27 signatureShouldBe = host
28 }
24 29
25 let signatureShouldBe 30 const signatureOk = peertubeCryptoCheckSignature(pod.publicKey, signatureShouldBe, req.body.signature.signature)
26 // If there is data in the body the sender used it for its signature
27 // If there is no data we just use its host as signature
28 if (req.body.data) {
29 signatureShouldBe = req.body.data
30 } else {
31 signatureShouldBe = host
32 }
33 31
34 const signatureOk = peertubeCryptoCheckSignature(pod.publicKey, signatureShouldBe, req.body.signature.signature) 32 if (signatureOk === true) {
33 res.locals.secure = {
34 pod
35 }
35 36
36 if (signatureOk === true) { 37 return next()
37 res.locals.secure = {
38 pod
39 } 38 }
40 39
41 return next() 40 logger.error('Signature is not okay in body for %s.', req.body.signature.host)
42 } 41 return res.sendStatus(403)
43 42 })
44 logger.error('Signature is not okay in body for %s.', req.body.signature.host) 43 .catch(err => {
45 return res.sendStatus(403) 44 logger.error('Cannot get signed host in body.', { error: err })
46 }) 45 return res.sendStatus(500)
46 })
47} 47}
48 48
49// --------------------------------------------------------------------------- 49// ---------------------------------------------------------------------------
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
19 logger.debug('Checking makeFriends parameters', { parameters: req.body }) 19 logger.debug('Checking makeFriends parameters', { parameters: req.body })
20 20
21 checkErrors(req, res, function () { 21 checkErrors(req, res, function () {
22 hasFriends(function (err, heHasFriends) { 22 hasFriends()
23 if (err) { 23 .then(heHasFriends => {
24 if (heHasFriends === true) {
25 // We need to quit our friends before make new ones
26 return res.sendStatus(409)
27 }
28
29 return next()
30 })
31 .catch(err => {
24 logger.error('Cannot know if we have friends.', { error: err }) 32 logger.error('Cannot know if we have friends.', { error: err })
25 res.sendStatus(500) 33 res.sendStatus(500)
26 } 34 })
27
28 if (heHasFriends === true) {
29 // We need to quit our friends before make new ones
30 return res.sendStatus(409)
31 }
32
33 return next()
34 })
35 }) 35 })
36} 36}
37 37
@@ -42,19 +42,19 @@ function podsAddValidator (req: express.Request, res: express.Response, next: ex
42 logger.debug('Checking podsAdd parameters', { parameters: req.body }) 42 logger.debug('Checking podsAdd parameters', { parameters: req.body })
43 43
44 checkErrors(req, res, function () { 44 checkErrors(req, res, function () {
45 db.Pod.loadByHost(req.body.host, function (err, pod) { 45 db.Pod.loadByHost(req.body.host)
46 if (err) { 46 .then(pod => {
47 // Pod with this host already exists
48 if (pod) {
49 return res.sendStatus(409)
50 }
51
52 return next()
53 })
54 .catch(err => {
47 logger.error('Cannot load pod by host.', { error: err }) 55 logger.error('Cannot load pod by host.', { error: err })
48 res.sendStatus(500) 56 res.sendStatus(500)
49 } 57 })
50
51 // Pod with this host already exists
52 if (pod) {
53 return res.sendStatus(409)
54 }
55
56 return next()
57 })
58 }) 58 })
59} 59}
60 60
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
13 logger.debug('Checking usersAdd parameters', { parameters: req.body }) 13 logger.debug('Checking usersAdd parameters', { parameters: req.body })
14 14
15 checkErrors(req, res, function () { 15 checkErrors(req, res, function () {
16 db.User.loadByUsernameOrEmail(req.body.username, req.body.email, function (err, user) { 16 db.User.loadByUsernameOrEmail(req.body.username, req.body.email)
17 if (err) { 17 .then(user => {
18 if (user) return res.status(409).send('User already exists.')
19
20 next()
21 })
22 .catch(err => {
18 logger.error('Error in usersAdd request validator.', { error: err }) 23 logger.error('Error in usersAdd request validator.', { error: err })
19 return res.sendStatus(500) 24 return res.sendStatus(500)
20 } 25 })
21
22 if (user) return res.status(409).send('User already exists.')
23
24 next()
25 })
26 }) 26 })
27} 27}
28 28
@@ -32,18 +32,18 @@ function usersRemoveValidator (req: express.Request, res: express.Response, next
32 logger.debug('Checking usersRemove parameters', { parameters: req.params }) 32 logger.debug('Checking usersRemove parameters', { parameters: req.params })
33 33
34 checkErrors(req, res, function () { 34 checkErrors(req, res, function () {
35 db.User.loadById(req.params.id, function (err, user) { 35 db.User.loadById(req.params.id)
36 if (err) { 36 .then(user => {
37 logger.error('Error in usersRemove request validator.', { error: err }) 37 if (!user) return res.status(404).send('User not found')
38 return res.sendStatus(500)
39 }
40
41 if (!user) return res.status(404).send('User not found')
42 38
43 if (user.username === 'root') return res.status(400).send('Cannot remove the root user') 39 if (user.username === 'root') return res.status(400).send('Cannot remove the root user')
44 40
45 next() 41 next()
46 }) 42 })
43 .catch(err => {
44 logger.error('Error in usersRemove request validator.', { error: err })
45 return res.sendStatus(500)
46 })
47 }) 47 })
48} 48}
49 49
@@ -64,16 +64,16 @@ function usersVideoRatingValidator (req: express.Request, res: express.Response,
64 logger.debug('Checking usersVideoRating parameters', { parameters: req.params }) 64 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
65 65
66 checkErrors(req, res, function () { 66 checkErrors(req, res, function () {
67 db.Video.load(req.params.videoId, function (err, video) { 67 db.Video.load(req.params.videoId)
68 if (err) { 68 .then(video => {
69 if (!video) return res.status(404).send('Video not found')
70
71 next()
72 })
73 .catch(err => {
69 logger.error('Error in user request validator.', { error: err }) 74 logger.error('Error in user request validator.', { error: err })
70 return res.sendStatus(500) 75 return res.sendStatus(500)
71 } 76 })
72
73 if (!video) return res.status(404).send('Video not found')
74
75 next()
76 })
77 }) 77 })
78} 78}
79 79
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 @@
1import 'express-validator' 1import 'express-validator'
2import * as multer from 'multer'
3import * as express from 'express' 2import * as express from 'express'
4 3
5import { database as db } from '../../initializers/database' 4import { database as db } from '../../initializers/database'
@@ -24,18 +23,19 @@ function videosAddValidator (req: express.Request, res: express.Response, next:
24 checkErrors(req, res, function () { 23 checkErrors(req, res, function () {
25 const videoFile = req.files.videofile[0] 24 const videoFile = req.files.videofile[0]
26 25
27 db.Video.getDurationFromFile(videoFile.path, function (err, duration) { 26 db.Video.getDurationFromFile(videoFile.path)
28 if (err) { 27 .then(duration => {
29 return res.status(400).send('Cannot retrieve metadata of the file.') 28 if (!isVideoDurationValid('' + duration)) {
30 } 29 return res.status(400).send('Duration of the video file is too big (max: ' + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).')
31 30 }
32 if (!isVideoDurationValid(duration)) {
33 return res.status(400).send('Duration of the video file is too big (max: ' + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).')
34 }
35 31
36 videoFile['duration'] = duration 32 videoFile['duration'] = duration
37 next() 33 next()
38 }) 34 })
35 .catch(err => {
36 logger.error('Error in getting duration from file.', { error: err })
37 res.status(400).send('Cannot retrieve metadata of the file.')
38 })
39 }) 39 })
40} 40}
41 41
@@ -157,43 +157,42 @@ export {
157// --------------------------------------------------------------------------- 157// ---------------------------------------------------------------------------
158 158
159function checkVideoExists (id: string, res: express.Response, callback: () => void) { 159function checkVideoExists (id: string, res: express.Response, callback: () => void) {
160 db.Video.loadAndPopulateAuthorAndPodAndTags(id, function (err, video) { 160 db.Video.loadAndPopulateAuthorAndPodAndTags(id).then(video => {
161 if (err) {
162 logger.error('Error in video request validator.', { error: err })
163 return res.sendStatus(500)
164 }
165
166 if (!video) return res.status(404).send('Video not found') 161 if (!video) return res.status(404).send('Video not found')
167 162
168 res.locals.video = video 163 res.locals.video = video
169 callback() 164 callback()
170 }) 165 })
166 .catch(err => {
167 logger.error('Error in video request validator.', { error: err })
168 return res.sendStatus(500)
169 })
171} 170}
172 171
173function checkUserCanDeleteVideo (userId: number, res: express.Response, callback: () => void) { 172function checkUserCanDeleteVideo (userId: number, res: express.Response, callback: () => void) {
174 // Retrieve the user who did the request 173 // Retrieve the user who did the request
175 db.User.loadById(userId, function (err, user) { 174 db.User.loadById(userId)
176 if (err) { 175 .then(user => {
177 logger.error('Error in video request validator.', { error: err }) 176 // Check if the user can delete the video
178 return res.sendStatus(500) 177 // The user can delete it if s/he is an admin
179 } 178 // Or if s/he is the video's author
180 179 if (user.isAdmin() === false) {
181 // Check if the user can delete the video 180 if (res.locals.video.isOwned() === false) {
182 // The user can delete it if s/he is an admin 181 return res.status(403).send('Cannot remove video of another pod')
183 // Or if s/he is the video's author 182 }
184 if (user.isAdmin() === false) { 183
185 if (res.locals.video.isOwned() === false) { 184 if (res.locals.video.Author.userId !== res.locals.oauth.token.User.id) {
186 return res.status(403).send('Cannot remove video of another pod') 185 return res.status(403).send('Cannot remove video of another user')
187 } 186 }
188
189 if (res.locals.video.Author.userId !== res.locals.oauth.token.User.id) {
190 return res.status(403).send('Cannot remove video of another user')
191 } 187 }
192 }
193 188
194 // If we reach this comment, we can delete the video 189 // If we reach this comment, we can delete the video
195 callback() 190 callback()
196 }) 191 })
192 .catch(err => {
193 logger.error('Error in video request validator.', { error: err })
194 return res.sendStatus(500)
195 })
197} 196}
198 197
199function checkVideoIsBlacklistable (req: express.Request, res: express.Response, callback: () => void) { 198function 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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
2 3
3export namespace ApplicationMethods { 4export namespace ApplicationMethods {
4 export type LoadMigrationVersionCallback = (err: Error, version: number) => void 5 export type LoadMigrationVersion = () => Promise<number>
5 export type LoadMigrationVersion = (callback: LoadMigrationVersionCallback) => void
6 6
7 export type UpdateMigrationVersionCallback = (err: Error, applicationInstance: ApplicationAttributes) => void 7 export type UpdateMigrationVersion = (
8 export type UpdateMigrationVersion = (newVersion: number, transaction: Sequelize.Transaction, callback: UpdateMigrationVersionCallback) => void 8 newVersion: number,
9 transaction: Sequelize.Transaction
10 ) => Promise<[ number, ApplicationInstance[] ]>
9} 11}
10 12
11export interface ApplicationClass { 13export 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'
2 2
3import { addMethodsToModel } from '../utils' 3import { addMethodsToModel } from '../utils'
4import { 4import {
5 ApplicationClass,
6 ApplicationAttributes, 5 ApplicationAttributes,
7 ApplicationInstance, 6 ApplicationInstance,
8 7
@@ -35,23 +34,19 @@ export default function defineApplication (sequelize: Sequelize.Sequelize, DataT
35 34
36// --------------------------------------------------------------------------- 35// ---------------------------------------------------------------------------
37 36
38loadMigrationVersion = function (callback: ApplicationMethods.LoadMigrationVersionCallback) { 37loadMigrationVersion = function () {
39 const query = { 38 const query = {
40 attributes: [ 'migrationVersion' ] 39 attributes: [ 'migrationVersion' ]
41 } 40 }
42 41
43 return Application.findOne(query).asCallback(function (err, data) { 42 return Application.findOne(query).then(data => data ? data.migrationVersion : null)
44 const version = data ? data.migrationVersion : null
45
46 return callback(err, version)
47 })
48} 43}
49 44
50updateMigrationVersion = function (newVersion: number, transaction: Sequelize.Transaction, callback: ApplicationMethods.UpdateMigrationVersionCallback) { 45updateMigrationVersion = function (newVersion: number, transaction: Sequelize.Transaction) {
51 const options: Sequelize.UpdateOptions = { 46 const options: Sequelize.UpdateOptions = {
52 where: {}, 47 where: {},
53 transaction: transaction 48 transaction: transaction
54 } 49 }
55 50
56 return Application.update({ migrationVersion: newVersion }, options).asCallback(callback) 51 return Application.update({ migrationVersion: newVersion }, options)
57} 52}
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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
2 3
3import { JobState } from '../../../shared/models/job.model' 4import { JobState } from '../../../shared/models/job.model'
4 5
5export namespace JobMethods { 6export namespace JobMethods {
6 export type ListWithLimitCallback = (err: Error, jobInstances: JobInstance[]) => void 7 export type ListWithLimit = (limit: number, state: JobState) => Promise<JobInstance[]>
7 export type ListWithLimit = (limit: number, state: JobState, callback: ListWithLimitCallback) => void
8} 8}
9 9
10export interface JobClass { 10export 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'
5 5
6import { addMethodsToModel } from '../utils' 6import { addMethodsToModel } from '../utils'
7import { 7import {
8 JobClass,
9 JobInstance, 8 JobInstance,
10 JobAttributes, 9 JobAttributes,
11 10
@@ -49,7 +48,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
49 48
50// --------------------------------------------------------------------------- 49// ---------------------------------------------------------------------------
51 50
52listWithLimit = function (limit: number, state: JobState, callback: JobMethods.ListWithLimitCallback) { 51listWithLimit = function (limit: number, state: JobState) {
53 const query = { 52 const query = {
54 order: [ 53 order: [
55 [ 'id', 'ASC' ] 54 [ 'id', 'ASC' ]
@@ -60,5 +59,5 @@ listWithLimit = function (limit: number, state: JobState, callback: JobMethods.L
60 } 59 }
61 } 60 }
62 61
63 return Job.findAll(query).asCallback(callback) 62 return Job.findAll(query)
64} 63}
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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
2 3
3export namespace OAuthClientMethods { 4export namespace OAuthClientMethods {
4 export type CountTotalCallback = (err: Error, total: number) => void 5 export type CountTotal = () => Promise<number>
5 export type CountTotal = (callback: CountTotalCallback) => void
6 6
7 export type LoadFirstClientCallback = (err: Error, client: OAuthClientInstance) => void 7 export type LoadFirstClient = () => Promise<OAuthClientInstance>
8 export type LoadFirstClient = (callback: LoadFirstClientCallback) => void
9 8
10 export type GetByIdAndSecret = (clientId, clientSecret) => void 9 export type GetByIdAndSecret = (clientId: string, clientSecret: string) => Promise<OAuthClientInstance>
11} 10}
12 11
13export interface OAuthClientClass { 12export 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'
2 2
3import { addMethodsToModel } from '../utils' 3import { addMethodsToModel } from '../utils'
4import { 4import {
5 OAuthClientClass,
6 OAuthClientInstance, 5 OAuthClientInstance,
7 OAuthClientAttributes, 6 OAuthClientAttributes,
8 7
@@ -67,12 +66,12 @@ function associate (models) {
67 }) 66 })
68} 67}
69 68
70countTotal = function (callback: OAuthClientMethods.CountTotalCallback) { 69countTotal = function () {
71 return OAuthClient.count().asCallback(callback) 70 return OAuthClient.count()
72} 71}
73 72
74loadFirstClient = function (callback: OAuthClientMethods.LoadFirstClientCallback) { 73loadFirstClient = function () {
75 return OAuthClient.findOne().asCallback(callback) 74 return OAuthClient.findOne()
76} 75}
77 76
78getByIdAndSecret = function (clientId: string, clientSecret: string) { 77getByIdAndSecret = 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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Bluebird from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4import { UserModel } from '../user' 4import { UserModel } from '../user'
5 5
@@ -15,12 +15,11 @@ export type OAuthTokenInfo = {
15} 15}
16 16
17export namespace OAuthTokenMethods { 17export namespace OAuthTokenMethods {
18 export type GetByRefreshTokenAndPopulateClient = (refreshToken: string) => Bluebird<OAuthTokenInfo> 18 export type GetByRefreshTokenAndPopulateClient = (refreshToken: string) => Promise<OAuthTokenInfo>
19 export type GetByTokenAndPopulateUser = (bearerToken: string) => Bluebird<OAuthTokenInstance> 19 export type GetByTokenAndPopulateUser = (bearerToken: string) => Promise<OAuthTokenInstance>
20 export type GetByRefreshTokenAndPopulateUser = (refreshToken: string) => Bluebird<OAuthTokenInstance> 20 export type GetByRefreshTokenAndPopulateUser = (refreshToken: string) => Promise<OAuthTokenInstance>
21 21
22 export type RemoveByUserIdCallback = (err: Error) => void 22 export type RemoveByUserId = (userId) => Promise<number>
23 export type RemoveByUserId = (userId, callback) => void
24} 23}
25 24
26export interface OAuthTokenClass { 25export 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'
4 4
5import { addMethodsToModel } from '../utils' 5import { addMethodsToModel } from '../utils'
6import { 6import {
7 OAuthTokenClass,
8 OAuthTokenInstance, 7 OAuthTokenInstance,
9 OAuthTokenAttributes, 8 OAuthTokenAttributes,
10 9
@@ -149,12 +148,12 @@ getByRefreshTokenAndPopulateUser = function (refreshToken: string) {
149 }) 148 })
150} 149}
151 150
152removeByUserId = function (userId, callback) { 151removeByUserId = function (userId: number) {
153 const query = { 152 const query = {
154 where: { 153 where: {
155 userId: userId 154 userId: userId
156 } 155 }
157 } 156 }
158 157
159 return OAuthToken.destroy(query).asCallback(callback) 158 return OAuthToken.destroy(query)
160} 159}
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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
2 3
3// Don't use barrel, import just what we need 4// Don't use barrel, import just what we need
4import { Pod as FormatedPod } from '../../../shared/models/pod.model' 5import { Pod as FormatedPod } from '../../../shared/models/pod.model'
@@ -6,32 +7,23 @@ import { Pod as FormatedPod } from '../../../shared/models/pod.model'
6export namespace PodMethods { 7export namespace PodMethods {
7 export type ToFormatedJSON = (this: PodInstance) => FormatedPod 8 export type ToFormatedJSON = (this: PodInstance) => FormatedPod
8 9
9 export type CountAllCallback = (err: Error, total: number) => void 10 export type CountAll = () => Promise<number>
10 export type CountAll = (callback) => void
11 11
12 export type IncrementScoresCallback = (err: Error) => void 12 export type IncrementScores = (ids: number[], value: number) => Promise<[ number, PodInstance[] ]>
13 export type IncrementScores = (ids: number[], value: number, callback?: IncrementScoresCallback) => void
14 13
15 export type ListCallback = (err: Error, podInstances?: PodInstance[]) => void 14 export type List = () => Promise<PodInstance[]>
16 export type List = (callback: ListCallback) => void
17 15
18 export type ListAllIdsCallback = (err: Error, ids?: number[]) => void 16 export type ListAllIds = (transaction: Sequelize.Transaction) => Promise<number[]>
19 export type ListAllIds = (transaction: Sequelize.Transaction, callback: ListAllIdsCallback) => void
20 17
21 export type ListRandomPodIdsWithRequestCallback = (err: Error, podInstanceIds?: number[]) => void 18 export type ListRandomPodIdsWithRequest = (limit: number, tableWithPods: string, tableWithPodsJoins: string) => Promise<number[]>
22 export type ListRandomPodIdsWithRequest = (limit: number, tableWithPods: string, tableWithPodsJoins: string, callback: ListRandomPodIdsWithRequestCallback) => void
23 19
24 export type ListBadPodsCallback = (err: Error, podInstances?: PodInstance[]) => void 20 export type ListBadPods = () => Promise<PodInstance[]>
25 export type ListBadPods = (callback: ListBadPodsCallback) => void
26 21
27 export type LoadCallback = (err: Error, podInstance: PodInstance) => void 22 export type Load = (id: number) => Promise<PodInstance>
28 export type Load = (id: number, callback: LoadCallback) => void
29 23
30 export type LoadByHostCallback = (err: Error, podInstance: PodInstance) => void 24 export type LoadByHost = (host: string) => Promise<PodInstance>
31 export type LoadByHost = (host: string, callback: LoadByHostCallback) => void
32 25
33 export type RemoveAllCallback = (err: Error) => void 26 export type RemoveAll = () => Promise<number>
34 export type RemoveAll = (callback: RemoveAllCallback) => void
35 27
36 export type UpdatePodsScore = (goodPods: number[], badPods: number[]) => void 28 export type UpdatePodsScore = (goodPods: number[], badPods: number[]) => void
37} 29}
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 @@
1import { each, waterfall } from 'async'
2import { map } from 'lodash' 1import { map } from 'lodash'
3import * as Sequelize from 'sequelize' 2import * as Sequelize from 'sequelize'
4 3
@@ -7,7 +6,6 @@ import { logger, isHostValid } from '../../helpers'
7 6
8import { addMethodsToModel } from '../utils' 7import { addMethodsToModel } from '../utils'
9import { 8import {
10 PodClass,
11 PodInstance, 9 PodInstance,
12 PodAttributes, 10 PodAttributes,
13 11
@@ -118,13 +116,11 @@ function associate (models) {
118 }) 116 })
119} 117}
120 118
121countAll = function (callback: PodMethods.CountAllCallback) { 119countAll = function () {
122 return Pod.count().asCallback(callback) 120 return Pod.count()
123} 121}
124 122
125incrementScores = function (ids: number[], value: number, callback?: PodMethods.IncrementScoresCallback) { 123incrementScores = function (ids: number[], value: number) {
126 if (!callback) callback = function () { /* empty */ }
127
128 const update = { 124 const update = {
129 score: Sequelize.literal('score +' + value) 125 score: Sequelize.literal('score +' + value)
130 } 126 }
@@ -139,33 +135,28 @@ incrementScores = function (ids: number[], value: number, callback?: PodMethods.
139 validate: false 135 validate: false
140 } 136 }
141 137
142 return Pod.update(update, options).asCallback(callback) 138 return Pod.update(update, options)
143} 139}
144 140
145list = function (callback: PodMethods.ListCallback) { 141list = function () {
146 return Pod.findAll().asCallback(callback) 142 return Pod.findAll()
147} 143}
148 144
149listAllIds = function (transaction: Sequelize.Transaction, callback: PodMethods.ListAllIdsCallback) { 145listAllIds = function (transaction: Sequelize.Transaction) {
150 const query: any = { 146 const query: Sequelize.FindOptions = {
151 attributes: [ 'id' ] 147 attributes: [ 'id' ],
148 transaction
152 } 149 }
153 150
154 if (transaction !== null) query.transaction = transaction 151 return Pod.findAll(query).then(pods => {
155 152 return map(pods, 'id')
156 return Pod.findAll(query).asCallback(function (err: Error, pods) {
157 if (err) return callback(err)
158
159 return callback(null, map(pods, 'id'))
160 }) 153 })
161} 154}
162 155
163listRandomPodIdsWithRequest = function (limit: number, tableWithPods: string, tableWithPodsJoins: string, callback: PodMethods.ListRandomPodIdsWithRequestCallback) { 156listRandomPodIdsWithRequest = function (limit: number, tableWithPods: string, tableWithPodsJoins: string) {
164 Pod.count().asCallback(function (err, count) { 157 return Pod.count().then(count => {
165 if (err) return callback(err)
166
167 // Optimization... 158 // Optimization...
168 if (count === 0) return callback(null, []) 159 if (count === 0) return []
169 160
170 let start = Math.floor(Math.random() * count) - limit 161 let start = Math.floor(Math.random() * count) - limit
171 if (start < 0) start = 0 162 if (start < 0) start = 0
@@ -186,56 +177,55 @@ listRandomPodIdsWithRequest = function (limit: number, tableWithPods: string, ta
186 } 177 }
187 } 178 }
188 179
189 return Pod.findAll(query).asCallback(function (err, pods) { 180 return Pod.findAll(query).then(pods => {
190 if (err) return callback(err) 181 return map(pods, 'id')
191
192 return callback(null, map(pods, 'id'))
193 }) 182 })
194 }) 183 })
195} 184}
196 185
197listBadPods = function (callback: PodMethods.ListBadPodsCallback) { 186listBadPods = function () {
198 const query = { 187 const query = {
199 where: { 188 where: {
200 score: { $lte: 0 } 189 score: { $lte: 0 }
201 } 190 }
202 } 191 }
203 192
204 return Pod.findAll(query).asCallback(callback) 193 return Pod.findAll(query)
205} 194}
206 195
207load = function (id: number, callback: PodMethods.LoadCallback) { 196load = function (id: number) {
208 return Pod.findById(id).asCallback(callback) 197 return Pod.findById(id)
209} 198}
210 199
211loadByHost = function (host: string, callback: PodMethods.LoadByHostCallback) { 200loadByHost = function (host: string) {
212 const query = { 201 const query = {
213 where: { 202 where: {
214 host: host 203 host: host
215 } 204 }
216 } 205 }
217 206
218 return Pod.findOne(query).asCallback(callback) 207 return Pod.findOne(query)
219} 208}
220 209
221removeAll = function (callback: PodMethods.RemoveAllCallback) { 210removeAll = function () {
222 return Pod.destroy().asCallback(callback) 211 return Pod.destroy()
223} 212}
224 213
225updatePodsScore = function (goodPods: number[], badPods: number[]) { 214updatePodsScore = function (goodPods: number[], badPods: number[]) {
226 logger.info('Updating %d good pods and %d bad pods scores.', goodPods.length, badPods.length) 215 logger.info('Updating %d good pods and %d bad pods scores.', goodPods.length, badPods.length)
227 216
228 if (goodPods.length !== 0) { 217 if (goodPods.length !== 0) {
229 incrementScores(goodPods, PODS_SCORE.BONUS, function (err) { 218 incrementScores(goodPods, PODS_SCORE.BONUS).catch(err => {
230 if (err) logger.error('Cannot increment scores of good pods.', { error: err }) 219 logger.error('Cannot increment scores of good pods.', { error: err })
231 }) 220 })
232 } 221 }
233 222
234 if (badPods.length !== 0) { 223 if (badPods.length !== 0) {
235 incrementScores(badPods, PODS_SCORE.MALUS, function (err) { 224 incrementScores(badPods, PODS_SCORE.MALUS)
236 if (err) logger.error('Cannot decrement scores of bad pods.', { error: err }) 225 .then(() => removeBadPods())
237 removeBadPods() 226 .catch(err => {
238 }) 227 if (err) logger.error('Cannot decrement scores of bad pods.', { error: err })
228 })
239 } 229 }
240} 230}
241 231
@@ -243,32 +233,19 @@ updatePodsScore = function (goodPods: number[], badPods: number[]) {
243 233
244// Remove pods with a score of 0 (too many requests where they were unreachable) 234// Remove pods with a score of 0 (too many requests where they were unreachable)
245function removeBadPods () { 235function removeBadPods () {
246 waterfall([ 236 return listBadPods()
247 function findBadPods (callback) { 237 .then(pods => {
248 listBadPods(function (err, pods) { 238 const podsRemovePromises = pods.map(pod => pod.destroy())
249 if (err) { 239 return Promise.all(podsRemovePromises).then(() => pods.length)
250 logger.error('Cannot find bad pods.', { error: err }) 240 })
251 return callback(err) 241 .then(numberOfPodsRemoved => {
252 } 242 if (numberOfPodsRemoved) {
253 243 logger.info('Removed %d pods.', numberOfPodsRemoved)
254 return callback(null, pods) 244 } else {
255 }) 245 logger.info('No need to remove bad pods.')
256 }, 246 }
257 247 })
258 function removeTheseBadPods (pods, callback) { 248 .catch(err => {
259 each(pods, function (pod: any, callbackEach) {
260 pod.destroy().asCallback(callbackEach)
261 }, function (err) {
262 return callback(err, pods.length)
263 })
264 }
265 ], function (err, numberOfPodsRemoved) {
266 if (err) {
267 logger.error('Cannot remove bad pods.', { error: err }) 249 logger.error('Cannot remove bad pods.', { error: err })
268 } else if (numberOfPodsRemoved) { 250 })
269 logger.info('Removed %d pods.', numberOfPodsRemoved)
270 } else {
271 logger.info('No need to remove bad pods.')
272 }
273 })
274} 251}
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 @@
1import * as Promise from 'bluebird'
2
3export interface AbstractRequestClass <T> {
4 countTotalRequests: () => Promise<number>
5 listWithLimitAndRandom: (limitPods: number, limitRequestsPerPod: number) => Promise<T>
6 removeWithEmptyTo: () => Promise<number>
7 removeAll: () => Promise<void>
8}
9
10export interface AbstractRequestToPodClass {
11 removeByRequestIdsAndPod: (ids: number[], podId: number) => Promise<number>
12}
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 @@
1export * from './abstract-request-interface'
1export * from './request-interface' 2export * from './request-interface'
2export * from './request-to-pod-interface' 3export * from './request-to-pod-interface'
3export * from './request-video-event-interface' 4export * 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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
2 3
4import { AbstractRequestClass } from './abstract-request-interface'
3import { PodInstance, PodAttributes } from '../pod' 5import { PodInstance, PodAttributes } from '../pod'
4import { RequestEndpoint } from '../../../shared/models/request-scheduler.model' 6import { RequestEndpoint } from '../../../shared/models/request-scheduler.model'
5 7
@@ -11,20 +13,16 @@ export type RequestsGrouped = {
11} 13}
12 14
13export namespace RequestMethods { 15export namespace RequestMethods {
14 export type CountTotalRequestsCallback = (err: Error, total: number) => void 16 export type CountTotalRequests = () => Promise<number>
15 export type CountTotalRequests = (callback: CountTotalRequestsCallback) => void
16 17
17 export type ListWithLimitAndRandomCallback = (err: Error, requestsGrouped?: RequestsGrouped) => void 18 export type ListWithLimitAndRandom = (limitPods: number, limitRequestsPerPod: number) => Promise<RequestsGrouped>
18 export type ListWithLimitAndRandom = (limitPods, limitRequestsPerPod, callback: ListWithLimitAndRandomCallback) => void
19 19
20 export type RemoveWithEmptyToCallback = (err: Error) => void 20 export type RemoveWithEmptyTo = () => Promise<number>
21 export type RemoveWithEmptyTo = (callback: RemoveWithEmptyToCallback) => void
22 21
23 export type RemoveAllCallback = (err: Error) => void 22 export type RemoveAll = () => Promise<void>
24 export type RemoveAll = (callback: RemoveAllCallback) => void
25} 23}
26 24
27export interface RequestClass { 25export interface RequestClass extends AbstractRequestClass<RequestsGrouped> {
28 countTotalRequests: RequestMethods.CountTotalRequests 26 countTotalRequests: RequestMethods.CountTotalRequests
29 listWithLimitAndRandom: RequestMethods.ListWithLimitAndRandom 27 listWithLimitAndRandom: RequestMethods.ListWithLimitAndRandom
30 removeWithEmptyTo: RequestMethods.RemoveWithEmptyTo 28 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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4import { AbstractRequestToPodClass } from './abstract-request-interface'
2 5
3export namespace RequestToPodMethods { 6export namespace RequestToPodMethods {
4 export type RemoveByRequestIdsAndPodCallback = (err: Error) => void 7 export type RemoveByRequestIdsAndPod = (requestsIds: number[], podId: number) => Promise<number>
5 export type RemoveByRequestIdsAndPod = (requestsIds: number[], podId: number, callback?: RemoveByRequestIdsAndPodCallback) => void
6} 8}
7 9
8export interface RequestToPodClass { 10export interface RequestToPodClass extends AbstractRequestToPodClass {
9 removeByRequestIdsAndPod: RequestToPodMethods.RemoveByRequestIdsAndPod 11 removeByRequestIdsAndPod: RequestToPodMethods.RemoveByRequestIdsAndPod
10} 12}
11 13
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'
2 2
3import { addMethodsToModel } from '../utils' 3import { addMethodsToModel } from '../utils'
4import { 4import {
5 RequestToPodClass,
6 RequestToPodInstance, 5 RequestToPodInstance,
7 RequestToPodAttributes, 6 RequestToPodAttributes,
8 7
@@ -38,9 +37,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
38 37
39// --------------------------------------------------------------------------- 38// ---------------------------------------------------------------------------
40 39
41removeByRequestIdsAndPod = function (requestsIds: number[], podId: number, callback?: RequestToPodMethods.RemoveByRequestIdsAndPodCallback) { 40removeByRequestIdsAndPod = function (requestsIds: number[], podId: number) {
42 if (!callback) callback = function () { /* empty */ }
43
44 const query = { 41 const query = {
45 where: { 42 where: {
46 requestId: { 43 requestId: {
@@ -50,5 +47,5 @@ removeByRequestIdsAndPod = function (requestsIds: number[], podId: number, callb
50 } 47 }
51 } 48 }
52 49
53 RequestToPod.destroy(query).asCallback(callback) 50 return RequestToPod.destroy(query)
54} 51}
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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
2 3
4import { AbstractRequestClass, AbstractRequestToPodClass } from './abstract-request-interface'
3import { VideoInstance } from '../video' 5import { VideoInstance } from '../video'
4import { PodInstance } from '../pod' 6import { PodInstance } from '../pod'
5 7
@@ -16,20 +18,16 @@ export type RequestsVideoEventGrouped = {
16} 18}
17 19
18export namespace RequestVideoEventMethods { 20export namespace RequestVideoEventMethods {
19 export type CountTotalRequestsCallback = (err: Error, total: number) => void 21 export type CountTotalRequests = () => Promise<number>
20 export type CountTotalRequests = (callback: CountTotalRequestsCallback) => void
21 22
22 export type ListWithLimitAndRandomCallback = (err: Error, requestsGrouped?: RequestsVideoEventGrouped) => void 23 export type ListWithLimitAndRandom = (limitPods: number, limitRequestsPerPod: number) => Promise<RequestsVideoEventGrouped>
23 export type ListWithLimitAndRandom = (limitPods: number, limitRequestsPerPod: number, callback: ListWithLimitAndRandomCallback) => void
24 24
25 export type RemoveByRequestIdsAndPodCallback = () => void 25 export type RemoveByRequestIdsAndPod = (ids: number[], podId: number) => Promise<number>
26 export type RemoveByRequestIdsAndPod = (ids: number[], podId: number, callback: RemoveByRequestIdsAndPodCallback) => void
27 26
28 export type RemoveAllCallback = () => void 27 export type RemoveAll = () => Promise<void>
29 export type RemoveAll = (callback: RemoveAllCallback) => void
30} 28}
31 29
32export interface RequestVideoEventClass { 30export interface RequestVideoEventClass extends AbstractRequestClass<RequestsVideoEventGrouped>, AbstractRequestToPodClass {
33 countTotalRequests: RequestVideoEventMethods.CountTotalRequests 31 countTotalRequests: RequestVideoEventMethods.CountTotalRequests
34 listWithLimitAndRandom: RequestVideoEventMethods.ListWithLimitAndRandom 32 listWithLimitAndRandom: RequestVideoEventMethods.ListWithLimitAndRandom
35 removeByRequestIdsAndPod: RequestVideoEventMethods.RemoveByRequestIdsAndPod 33 removeByRequestIdsAndPod: RequestVideoEventMethods.RemoveByRequestIdsAndPod
@@ -41,10 +39,12 @@ export interface RequestVideoEventAttributes {
41 count: number 39 count: number
42} 40}
43 41
44export interface RequestVideoEventInstance extends RequestVideoEventClass, RequestVideoEventAttributes, Sequelize.Instance<RequestVideoEventAttributes> { 42export interface RequestVideoEventInstance
43 extends RequestVideoEventClass, RequestVideoEventAttributes, Sequelize.Instance<RequestVideoEventAttributes> {
45 id: number 44 id: number
46 45
47 Video: VideoInstance 46 Video: VideoInstance
48} 47}
49 48
50export interface RequestVideoEventModel extends RequestVideoEventClass, Sequelize.Model<RequestVideoEventInstance, RequestVideoEventAttributes> {} 49export interface RequestVideoEventModel
50 extends RequestVideoEventClass, Sequelize.Model<RequestVideoEventInstance, RequestVideoEventAttributes> {}
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'
10import { isVideoEventCountValid } from '../../helpers' 10import { isVideoEventCountValid } from '../../helpers'
11import { addMethodsToModel } from '../utils' 11import { addMethodsToModel } from '../utils'
12import { 12import {
13 RequestVideoEventClass,
14 RequestVideoEventInstance, 13 RequestVideoEventInstance,
15 RequestVideoEventAttributes, 14 RequestVideoEventAttributes,
16 15
@@ -77,23 +76,21 @@ function associate (models) {
77 }) 76 })
78} 77}
79 78
80countTotalRequests = function (callback: RequestVideoEventMethods.CountTotalRequestsCallback) { 79countTotalRequests = function () {
81 const query = {} 80 const query = {}
82 return RequestVideoEvent.count(query).asCallback(callback) 81 return RequestVideoEvent.count(query)
83} 82}
84 83
85listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number, callback: RequestVideoEventMethods.ListWithLimitAndRandomCallback) { 84listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number) {
86 const Pod = db.Pod 85 const Pod = db.Pod
87 86
88 // We make a join between videos and authors to find the podId of our video event requests 87 // We make a join between videos and authors to find the podId of our video event requests
89 const podJoins = 'INNER JOIN "Videos" ON "Videos"."authorId" = "Authors"."id" ' + 88 const podJoins = 'INNER JOIN "Videos" ON "Videos"."authorId" = "Authors"."id" ' +
90 'INNER JOIN "RequestVideoEvents" ON "RequestVideoEvents"."videoId" = "Videos"."id"' 89 'INNER JOIN "RequestVideoEvents" ON "RequestVideoEvents"."videoId" = "Videos"."id"'
91 90
92 Pod.listRandomPodIdsWithRequest(limitPods, 'Authors', podJoins, function (err, podIds) { 91 return Pod.listRandomPodIdsWithRequest(limitPods, 'Authors', podJoins).then(podIds => {
93 if (err) return callback(err)
94
95 // We don't have friends that have requests 92 // We don't have friends that have requests
96 if (podIds.length === 0) return callback(null, []) 93 if (podIds.length === 0) return []
97 94
98 const query = { 95 const query = {
99 order: [ 96 order: [
@@ -121,16 +118,14 @@ listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: numbe
121 ] 118 ]
122 } 119 }
123 120
124 RequestVideoEvent.findAll(query).asCallback(function (err, requests) { 121 return RequestVideoEvent.findAll(query).then(requests => {
125 if (err) return callback(err)
126
127 const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod) 122 const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod)
128 return callback(err, requestsGrouped) 123 return requestsGrouped
129 }) 124 })
130 }) 125 })
131} 126}
132 127
133removeByRequestIdsAndPod = function (ids: number[], podId: number, callback: RequestVideoEventMethods.RemoveByRequestIdsAndPodCallback) { 128removeByRequestIdsAndPod = function (ids: number[], podId: number) {
134 const query = { 129 const query = {
135 where: { 130 where: {
136 id: { 131 id: {
@@ -152,12 +147,12 @@ removeByRequestIdsAndPod = function (ids: number[], podId: number, callback: Req
152 ] 147 ]
153 } 148 }
154 149
155 RequestVideoEvent.destroy(query).asCallback(callback) 150 return RequestVideoEvent.destroy(query)
156} 151}
157 152
158removeAll = function (callback: RequestVideoEventMethods.RemoveAllCallback) { 153removeAll = function () {
159 // Delete all requests 154 // Delete all requests
160 RequestVideoEvent.truncate({ cascade: true }).asCallback(callback) 155 return RequestVideoEvent.truncate({ cascade: true })
161} 156}
162 157
163// --------------------------------------------------------------------------- 158// ---------------------------------------------------------------------------
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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
2 3
4import { AbstractRequestClass, AbstractRequestToPodClass } from './abstract-request-interface'
3import { VideoInstance } from '../video' 5import { VideoInstance } from '../video'
4import { PodInstance } from '../pod' 6import { PodInstance } from '../pod'
5 7
@@ -14,20 +16,16 @@ export type RequestsVideoQaduGrouped = {
14} 16}
15 17
16export namespace RequestVideoQaduMethods { 18export namespace RequestVideoQaduMethods {
17 export type CountTotalRequestsCallback = (err: Error, total: number) => void 19 export type CountTotalRequests = () => Promise<number>
18 export type CountTotalRequests = (callback: CountTotalRequestsCallback) => void
19 20
20 export type ListWithLimitAndRandomCallback = (err: Error, requestsGrouped?: RequestsVideoQaduGrouped) => void 21 export type ListWithLimitAndRandom = (limitPods: number, limitRequestsPerPod: number) => Promise<RequestsVideoQaduGrouped>
21 export type ListWithLimitAndRandom = (limitPods: number, limitRequestsPerPod: number, callback: ListWithLimitAndRandomCallback) => void
22 22
23 export type RemoveByRequestIdsAndPodCallback = () => void 23 export type RemoveByRequestIdsAndPod = (ids: number[], podId: number) => Promise<number>
24 export type RemoveByRequestIdsAndPod = (ids: number[], podId: number, callback: RemoveByRequestIdsAndPodCallback) => void
25 24
26 export type RemoveAllCallback = () => void 25 export type RemoveAll = () => Promise<void>
27 export type RemoveAll = (callback: RemoveAllCallback) => void
28} 26}
29 27
30export interface RequestVideoQaduClass { 28export interface RequestVideoQaduClass extends AbstractRequestClass<RequestsVideoQaduGrouped>, AbstractRequestToPodClass {
31 countTotalRequests: RequestVideoQaduMethods.CountTotalRequests 29 countTotalRequests: RequestVideoQaduMethods.CountTotalRequests
32 listWithLimitAndRandom: RequestVideoQaduMethods.ListWithLimitAndRandom 30 listWithLimitAndRandom: RequestVideoQaduMethods.ListWithLimitAndRandom
33 removeByRequestIdsAndPod: RequestVideoQaduMethods.RemoveByRequestIdsAndPod 31 removeByRequestIdsAndPod: RequestVideoQaduMethods.RemoveByRequestIdsAndPod
@@ -38,11 +36,13 @@ export interface RequestVideoQaduAttributes {
38 type: RequestVideoQaduType 36 type: RequestVideoQaduType
39} 37}
40 38
41export interface RequestVideoQaduInstance extends RequestVideoQaduClass, RequestVideoQaduAttributes, Sequelize.Instance<RequestVideoQaduAttributes> { 39export interface RequestVideoQaduInstance
40 extends RequestVideoQaduClass, RequestVideoQaduAttributes, Sequelize.Instance<RequestVideoQaduAttributes> {
42 id: number 41 id: number
43 42
44 Pod: PodInstance 43 Pod: PodInstance
45 Video: VideoInstance 44 Video: VideoInstance
46} 45}
47 46
48export interface RequestVideoQaduModel extends RequestVideoQaduClass, Sequelize.Model<RequestVideoQaduInstance, RequestVideoQaduAttributes> {} 47export interface RequestVideoQaduModel
48 extends RequestVideoQaduClass, Sequelize.Model<RequestVideoQaduInstance, RequestVideoQaduAttributes> {}
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'
16import { REQUEST_VIDEO_QADU_TYPES } from '../../initializers' 16import { REQUEST_VIDEO_QADU_TYPES } from '../../initializers'
17import { addMethodsToModel } from '../utils' 17import { addMethodsToModel } from '../utils'
18import { 18import {
19 RequestVideoQaduClass,
20 RequestVideoQaduInstance, 19 RequestVideoQaduInstance,
21 RequestVideoQaduAttributes, 20 RequestVideoQaduAttributes,
22 21
@@ -83,20 +82,18 @@ function associate (models) {
83 }) 82 })
84} 83}
85 84
86countTotalRequests = function (callback: RequestVideoQaduMethods.CountTotalRequestsCallback) { 85countTotalRequests = function () {
87 const query = {} 86 const query = {}
88 return RequestVideoQadu.count(query).asCallback(callback) 87 return RequestVideoQadu.count(query)
89} 88}
90 89
91listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number, callback: RequestVideoQaduMethods.ListWithLimitAndRandomCallback) { 90listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number) {
92 const Pod = db.Pod 91 const Pod = db.Pod
93 const tableJoin = '' 92 const tableJoin = ''
94 93
95 Pod.listRandomPodIdsWithRequest(limitPods, 'RequestVideoQadus', tableJoin, function (err, podIds) { 94 return Pod.listRandomPodIdsWithRequest(limitPods, 'RequestVideoQadus', tableJoin).then(podIds => {
96 if (err) return callback(err)
97
98 // We don't have friends that have requests 95 // We don't have friends that have requests
99 if (podIds.length === 0) return callback(null, []) 96 if (podIds.length === 0) return []
100 97
101 const query = { 98 const query = {
102 include: [ 99 include: [
@@ -114,16 +111,14 @@ listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: numbe
114 ] 111 ]
115 } 112 }
116 113
117 RequestVideoQadu.findAll(query).asCallback(function (err, requests) { 114 return RequestVideoQadu.findAll(query).then(requests => {
118 if (err) return callback(err)
119
120 const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod) 115 const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod)
121 return callback(err, requestsGrouped) 116 return requestsGrouped
122 }) 117 })
123 }) 118 })
124} 119}
125 120
126removeByRequestIdsAndPod = function (ids: number[], podId: number, callback: RequestVideoQaduMethods.RemoveByRequestIdsAndPodCallback) { 121removeByRequestIdsAndPod = function (ids: number[], podId: number) {
127 const query = { 122 const query = {
128 where: { 123 where: {
129 id: { 124 id: {
@@ -133,12 +128,12 @@ removeByRequestIdsAndPod = function (ids: number[], podId: number, callback: Req
133 } 128 }
134 } 129 }
135 130
136 RequestVideoQadu.destroy(query).asCallback(callback) 131 return RequestVideoQadu.destroy(query)
137} 132}
138 133
139removeAll = function (callback: RequestVideoQaduMethods.RemoveAllCallback) { 134removeAll = function () {
140 // Delete all requests 135 // Delete all requests
141 RequestVideoQadu.truncate({ cascade: true }).asCallback(callback) 136 return RequestVideoQadu.truncate({ cascade: true })
142} 137}
143 138
144// --------------------------------------------------------------------------- 139// ---------------------------------------------------------------------------
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'
5import { REQUEST_ENDPOINTS } from '../../initializers' 5import { REQUEST_ENDPOINTS } from '../../initializers'
6import { addMethodsToModel } from '../utils' 6import { addMethodsToModel } from '../utils'
7import { 7import {
8 RequestClass,
9 RequestInstance, 8 RequestInstance,
10 RequestAttributes, 9 RequestAttributes,
11 10
@@ -60,25 +59,23 @@ function associate (models) {
60 }) 59 })
61} 60}
62 61
63countTotalRequests = function (callback: RequestMethods.CountTotalRequestsCallback) { 62countTotalRequests = function () {
64 // We need to include Pod because there are no cascade delete when a pod is removed 63 // We need to include Pod because there are no cascade delete when a pod is removed
65 // So we could count requests that do not have existing pod anymore 64 // So we could count requests that do not have existing pod anymore
66 const query = { 65 const query = {
67 include: [ Request['sequelize'].models.Pod ] 66 include: [ Request['sequelize'].models.Pod ]
68 } 67 }
69 68
70 return Request.count(query).asCallback(callback) 69 return Request.count(query)
71} 70}
72 71
73listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number, callback: RequestMethods.ListWithLimitAndRandomCallback) { 72listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number) {
74 const Pod = db.Pod 73 const Pod = db.Pod
75 const tableJoin = '' 74 const tableJoin = ''
76 75
77 Pod.listRandomPodIdsWithRequest(limitPods, 'RequestToPods', '', function (err, podIds) { 76 return Pod.listRandomPodIdsWithRequest(limitPods, 'RequestToPods', tableJoin).then(podIds => {
78 if (err) return callback(err)
79
80 // We don't have friends that have requests 77 // We don't have friends that have requests
81 if (podIds.length === 0) return callback(null, []) 78 if (podIds.length === 0) return []
82 79
83 // The first x requests of these pods 80 // The first x requests of these pods
84 // It is very important to sort by id ASC to keep the requests order! 81 // It is very important to sort by id ASC to keep the requests order!
@@ -98,23 +95,20 @@ listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: numbe
98 ] 95 ]
99 } 96 }
100 97
101 Request.findAll(query).asCallback(function (err, requests) { 98 return Request.findAll(query).then(requests => {
102 if (err) return callback(err)
103 99
104 const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod) 100 const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod)
105 return callback(err, requestsGrouped) 101 return requestsGrouped
106 }) 102 })
107 }) 103 })
108} 104}
109 105
110removeAll = function (callback: RequestMethods.RemoveAllCallback) { 106removeAll = function () {
111 // Delete all requests 107 // Delete all requests
112 Request.truncate({ cascade: true }).asCallback(callback) 108 return Request.truncate({ cascade: true })
113} 109}
114 110
115removeWithEmptyTo = function (callback?: RequestMethods.RemoveWithEmptyToCallback) { 111removeWithEmptyTo = function () {
116 if (!callback) callback = function () { /* empty */ }
117
118 const query = { 112 const query = {
119 where: { 113 where: {
120 id: { 114 id: {
@@ -125,7 +119,7 @@ removeWithEmptyTo = function (callback?: RequestMethods.RemoveWithEmptyToCallbac
125 } 119 }
126 } 120 }
127 121
128 Request.destroy(query).asCallback(callback) 122 return Request.destroy(query)
129} 123}
130 124
131// --------------------------------------------------------------------------- 125// ---------------------------------------------------------------------------
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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Bluebird from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4// Don't use barrel, import just what we need 4// Don't use barrel, import just what we need
5import { UserRole, User as FormatedUser } from '../../../shared/models/user.model' 5import { UserRole, User as FormatedUser } from '../../../shared/models/user.model'
6import { ResultList } from '../../../shared/models/result-list.model'
6 7
7export namespace UserMethods { 8export namespace UserMethods {
8 export type IsPasswordMatchCallback = (err: Error, same: boolean) => void 9 export type IsPasswordMatch = (this: UserInstance, password: string) => Promise<boolean>
9 export type IsPasswordMatch = (this: UserInstance, password: string, callback: IsPasswordMatchCallback) => void
10 10
11 export type ToFormatedJSON = (this: UserInstance) => FormatedUser 11 export type ToFormatedJSON = (this: UserInstance) => FormatedUser
12 export type IsAdmin = (this: UserInstance) => boolean 12 export type IsAdmin = (this: UserInstance) => boolean
13 13
14 export type CountTotalCallback = (err: Error, total: number) => void 14 export type CountTotal = () => Promise<number>
15 export type CountTotal = (callback: CountTotalCallback) => void
16 15
17 export type GetByUsername = (username: string) => Bluebird<UserInstance> 16 export type GetByUsername = (username: string) => Promise<UserInstance>
18 17
19 export type ListCallback = (err: Error, userInstances: UserInstance[]) => void 18 export type List = () => Promise<UserInstance[]>
20 export type List = (callback: ListCallback) => void
21 19
22 export type ListForApiCallback = (err: Error, userInstances?: UserInstance[], total?: number) => void 20 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<UserInstance> >
23 export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void
24 21
25 export type LoadByIdCallback = (err: Error, userInstance: UserInstance) => void 22 export type LoadById = (id: number) => Promise<UserInstance>
26 export type LoadById = (id: number, callback: LoadByIdCallback) => void
27 23
28 export type LoadByUsernameCallback = (err: Error, userInstance: UserInstance) => void 24 export type LoadByUsername = (username: string) => Promise<UserInstance>
29 export type LoadByUsername = (username: string, callback: LoadByUsernameCallback) => void
30 25
31 export type LoadByUsernameOrEmailCallback = (err: Error, userInstance: UserInstance) => void 26 export type LoadByUsernameOrEmail = (username: string, email: string) => Promise<UserInstance>
32 export type LoadByUsernameOrEmail = (username: string, email: string, callback: LoadByUsernameOrEmailCallback) => void
33} 27}
34 28
35export interface UserClass { 29export 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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
2 3
3import { VideoRateType } from '../../../shared/models/user-video-rate.model' 4import { VideoRateType } from '../../../shared/models/user-video-rate.model'
4 5
5export namespace UserVideoRateMethods { 6export namespace UserVideoRateMethods {
6 export type LoadCallback = (err: Error, userVideoRateInstance: UserVideoRateInstance) => void 7 export type Load = (userId: number, videoId: string, transaction: Sequelize.Transaction) => Promise<UserVideoRateInstance>
7 export type Load = (userId: number, videoId: string, transaction: Sequelize.Transaction, callback: LoadCallback) => void
8} 8}
9 9
10export interface UserVideoRateClass { 10export 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'
8 8
9import { addMethodsToModel } from '../utils' 9import { addMethodsToModel } from '../utils'
10import { 10import {
11 UserVideoRateClass,
12 UserVideoRateInstance, 11 UserVideoRateInstance,
13 UserVideoRateAttributes, 12 UserVideoRateAttributes,
14 13
@@ -66,7 +65,7 @@ function associate (models) {
66 }) 65 })
67} 66}
68 67
69load = function (userId: number, videoId: string, transaction: Sequelize.Transaction, callback: UserVideoRateMethods.LoadCallback) { 68load = function (userId: number, videoId: string, transaction: Sequelize.Transaction) {
70 const options: Sequelize.FindOptions = { 69 const options: Sequelize.FindOptions = {
71 where: { 70 where: {
72 userId, 71 userId,
@@ -75,5 +74,5 @@ load = function (userId: number, videoId: string, transaction: Sequelize.Transac
75 } 74 }
76 if (transaction) options.transaction = transaction 75 if (transaction) options.transaction = transaction
77 76
78 return UserVideoRate.findOne(options).asCallback(callback) 77 return UserVideoRate.findOne(options)
79} 78}
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 {
13 13
14import { addMethodsToModel } from '../utils' 14import { addMethodsToModel } from '../utils'
15import { 15import {
16 UserClass,
17 UserInstance, 16 UserInstance,
18 UserAttributes, 17 UserAttributes,
19 18
@@ -118,21 +117,16 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
118} 117}
119 118
120function beforeCreateOrUpdate (user: UserInstance) { 119function beforeCreateOrUpdate (user: UserInstance) {
121 return new Promise(function (resolve, reject) { 120 return cryptPassword(user.password).then(hash => {
122 cryptPassword(user.password, function (err, hash) { 121 user.password = hash
123 if (err) return reject(err) 122 return undefined
124
125 user.password = hash
126
127 return resolve()
128 })
129 }) 123 })
130} 124}
131 125
132// ------------------------------ METHODS ------------------------------ 126// ------------------------------ METHODS ------------------------------
133 127
134isPasswordMatch = function (this: UserInstance, password: string, callback: UserMethods.IsPasswordMatchCallback) { 128isPasswordMatch = function (this: UserInstance, password: string) {
135 return comparePassword(password, this.password, callback) 129 return comparePassword(password, this.password)
136} 130}
137 131
138toFormatedJSON = function (this: UserInstance) { 132toFormatedJSON = function (this: UserInstance) {
@@ -164,8 +158,8 @@ function associate (models) {
164 }) 158 })
165} 159}
166 160
167countTotal = function (callback: UserMethods.CountTotalCallback) { 161countTotal = function () {
168 return this.count().asCallback(callback) 162 return this.count()
169} 163}
170 164
171getByUsername = function (username: string) { 165getByUsername = function (username: string) {
@@ -178,44 +172,45 @@ getByUsername = function (username: string) {
178 return User.findOne(query) 172 return User.findOne(query)
179} 173}
180 174
181list = function (callback: UserMethods.ListCallback) { 175list = function () {
182 return User.find().asCallback(callback) 176 return User.findAll()
183} 177}
184 178
185listForApi = function (start: number, count: number, sort: string, callback: UserMethods.ListForApiCallback) { 179listForApi = function (start: number, count: number, sort: string) {
186 const query = { 180 const query = {
187 offset: start, 181 offset: start,
188 limit: count, 182 limit: count,
189 order: [ getSort(sort) ] 183 order: [ getSort(sort) ]
190 } 184 }
191 185
192 return User.findAndCountAll(query).asCallback(function (err, result) { 186 return User.findAndCountAll(query).then(({ rows, count }) => {
193 if (err) return callback(err) 187 return {
194 188 data: rows,
195 return callback(null, result.rows, result.count) 189 total: count
190 }
196 }) 191 })
197} 192}
198 193
199loadById = function (id: number, callback: UserMethods.LoadByIdCallback) { 194loadById = function (id: number) {
200 return User.findById(id).asCallback(callback) 195 return User.findById(id)
201} 196}
202 197
203loadByUsername = function (username: string, callback: UserMethods.LoadByUsernameCallback) { 198loadByUsername = function (username: string) {
204 const query = { 199 const query = {
205 where: { 200 where: {
206 username: username 201 username: username
207 } 202 }
208 } 203 }
209 204
210 return User.findOne(query).asCallback(callback) 205 return User.findOne(query)
211} 206}
212 207
213loadByUsernameOrEmail = function (username: string, email: string, callback: UserMethods.LoadByUsernameOrEmailCallback) { 208loadByUsernameOrEmail = function (username: string, email: string) {
214 const query = { 209 const query = {
215 where: { 210 where: {
216 $or: [ { username }, { email } ] 211 $or: [ { username }, { email } ]
217 } 212 }
218 } 213 }
219 214
220 return User.findOne(query).asCallback(callback) 215 return User.findOne(query)
221} 216}
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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
2 3
3import { PodInstance } from '../pod' 4import { PodInstance } from '../pod'
4 5
5export namespace AuthorMethods { 6export namespace AuthorMethods {
6 export type FindOrCreateAuthorCallback = (err: Error, authorInstance?: AuthorInstance) => void 7 export type FindOrCreateAuthor = (
7 export type FindOrCreateAuthor = (name: string, podId: number, userId: number, transaction: Sequelize.Transaction, callback: FindOrCreateAuthorCallback) => void 8 name: string,
9 podId: number,
10 userId: number,
11 transaction: Sequelize.Transaction
12 ) => Promise<AuthorInstance>
8} 13}
9 14
10export interface AuthorClass { 15export 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'
4 4
5import { addMethodsToModel } from '../utils' 5import { addMethodsToModel } from '../utils'
6import { 6import {
7 AuthorClass,
8 AuthorInstance, 7 AuthorInstance,
9 AuthorAttributes, 8 AuthorAttributes,
10 9
@@ -74,30 +73,18 @@ function associate (models) {
74 }) 73 })
75} 74}
76 75
77findOrCreateAuthor = function ( 76findOrCreateAuthor = function (name: string, podId: number, userId: number, transaction: Sequelize.Transaction) {
78 name: string,
79 podId: number,
80 userId: number,
81 transaction: Sequelize.Transaction,
82 callback: AuthorMethods.FindOrCreateAuthorCallback
83) {
84 const author = { 77 const author = {
85 name, 78 name,
86 podId, 79 podId,
87 userId 80 userId
88 } 81 }
89 82
90 const query: any = { 83 const query: Sequelize.FindOrInitializeOptions<AuthorAttributes> = {
91 where: author, 84 where: author,
92 defaults: author 85 defaults: author,
86 transaction
93 } 87 }
94 88
95 if (transaction !== null) query.transaction = transaction 89 return Author.findOrCreate(query).then(([ authorInstance ]) => authorInstance)
96
97 Author.findOrCreate(query).asCallback(function (err, result) {
98 if (err) return callback(err)
99
100 // [ instance, wasCreated ]
101 return callback(null, result[0])
102 })
103} 90}
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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
2 3
3export namespace TagMethods { 4export namespace TagMethods {
4 export type FindOrCreateTagsCallback = (err: Error, tagInstances: TagInstance[]) => void 5 export type FindOrCreateTags = (tags: string[], transaction: Sequelize.Transaction) => Promise<TagInstance[]>
5 export type FindOrCreateTags = (tags: string[], transaction: Sequelize.Transaction, callback: FindOrCreateTagsCallback) => void
6} 6}
7 7
8export interface TagClass { 8export 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 @@
1import { each } from 'async'
2import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3 3
4import { addMethodsToModel } from '../utils' 4import { addMethodsToModel } from '../utils'
5import { 5import {
6 TagClass,
7 TagInstance, 6 TagInstance,
8 TagAttributes, 7 TagAttributes,
9 8
@@ -52,10 +51,9 @@ function associate (models) {
52 }) 51 })
53} 52}
54 53
55findOrCreateTags = function (tags: string[], transaction: Sequelize.Transaction, callback: TagMethods.FindOrCreateTagsCallback) { 54findOrCreateTags = function (tags: string[], transaction: Sequelize.Transaction) {
56 const tagInstances = [] 55 const tasks: Promise<TagInstance>[] = []
57 56 tags.forEach(tag => {
58 each<string, Error>(tags, function (tag, callbackEach) {
59 const query: any = { 57 const query: any = {
60 where: { 58 where: {
61 name: tag 59 name: tag
@@ -67,15 +65,9 @@ findOrCreateTags = function (tags: string[], transaction: Sequelize.Transaction,
67 65
68 if (transaction) query.transaction = transaction 66 if (transaction) query.transaction = transaction
69 67
70 Tag.findOrCreate(query).asCallback(function (err, res) { 68 const promise = Tag.findOrCreate(query).then(([ tagInstance ]) => tagInstance)
71 if (err) return callbackEach(err) 69 tasks.push(promise)
72
73 // res = [ tag, isCreated ]
74 const tag = res[0]
75 tagInstances.push(tag)
76 return callbackEach()
77 })
78 }, function (err) {
79 return callback(err, tagInstances)
80 }) 70 })
71
72 return Promise.all(tasks)
81} 73}
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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
2 3
3import { PodInstance } from '../pod' 4import { PodInstance } from '../pod'
5import { ResultList } from '../../../shared'
4 6
5// Don't use barrel, import just what we need 7// Don't use barrel, import just what we need
6import { VideoAbuse as FormatedVideoAbuse } from '../../../shared/models/video-abuse.model' 8import { VideoAbuse as FormatedVideoAbuse } from '../../../shared/models/video-abuse.model'
@@ -8,8 +10,7 @@ import { VideoAbuse as FormatedVideoAbuse } from '../../../shared/models/video-a
8export namespace VideoAbuseMethods { 10export namespace VideoAbuseMethods {
9 export type ToFormatedJSON = (this: VideoAbuseInstance) => FormatedVideoAbuse 11 export type ToFormatedJSON = (this: VideoAbuseInstance) => FormatedVideoAbuse
10 12
11 export type ListForApiCallback = (err: Error, videoAbuseInstances?: VideoAbuseInstance[], total?: number) => void 13 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoAbuseInstance> >
12 export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void
13} 14}
14 15
15export interface VideoAbuseClass { 16export 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 '../.
5 5
6import { addMethodsToModel, getSort } from '../utils' 6import { addMethodsToModel, getSort } from '../utils'
7import { 7import {
8 VideoAbuseClass,
9 VideoAbuseInstance, 8 VideoAbuseInstance,
10 VideoAbuseAttributes, 9 VideoAbuseAttributes,
11 10
@@ -109,7 +108,7 @@ function associate (models) {
109 }) 108 })
110} 109}
111 110
112listForApi = function (start: number, count: number, sort: string, callback: VideoAbuseMethods.ListForApiCallback) { 111listForApi = function (start: number, count: number, sort: string) {
113 const query = { 112 const query = {
114 offset: start, 113 offset: start,
115 limit: count, 114 limit: count,
@@ -122,11 +121,7 @@ listForApi = function (start: number, count: number, sort: string, callback: Vid
122 ] 121 ]
123 } 122 }
124 123
125 return VideoAbuse.findAndCountAll(query).asCallback(function (err, result) { 124 return VideoAbuse.findAndCountAll(query).then(({ rows, count }) => {
126 if (err) return callback(err) 125 return { total: count, data: rows }
127
128 return callback(null, result.rows, result.count)
129 }) 126 })
130} 127}
131
132
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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4import { ResultList } from '../../../shared'
2 5
3// Don't use barrel, import just what we need 6// Don't use barrel, import just what we need
4import { BlacklistedVideo as FormatedBlacklistedVideo } from '../../../shared/models/video-blacklist.model' 7import { BlacklistedVideo as FormatedBlacklistedVideo } from '../../../shared/models/video-blacklist.model'
@@ -6,20 +9,15 @@ import { BlacklistedVideo as FormatedBlacklistedVideo } from '../../../shared/mo
6export namespace BlacklistedVideoMethods { 9export namespace BlacklistedVideoMethods {
7 export type ToFormatedJSON = (this: BlacklistedVideoInstance) => FormatedBlacklistedVideo 10 export type ToFormatedJSON = (this: BlacklistedVideoInstance) => FormatedBlacklistedVideo
8 11
9 export type CountTotalCallback = (err: Error, total: number) => void 12 export type CountTotal = () => Promise<number>
10 export type CountTotal = (callback: CountTotalCallback) => void
11 13
12 export type ListCallback = (err: Error, backlistedVideoInstances: BlacklistedVideoInstance[]) => void 14 export type List = () => Promise<BlacklistedVideoInstance[]>
13 export type List = (callback: ListCallback) => void
14 15
15 export type ListForApiCallback = (err: Error, blacklistedVIdeoInstances?: BlacklistedVideoInstance[], total?: number) => void 16 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<BlacklistedVideoInstance> >
16 export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void
17 17
18 export type LoadByIdCallback = (err: Error, blacklistedVideoInstance: BlacklistedVideoInstance) => void 18 export type LoadById = (id: number) => Promise<BlacklistedVideoInstance>
19 export type LoadById = (id: number, callback: LoadByIdCallback) => void
20 19
21 export type LoadByVideoIdCallback = (err: Error, blacklistedVideoInstance: BlacklistedVideoInstance) => void 20 export type LoadByVideoId = (id: string) => Promise<BlacklistedVideoInstance>
22 export type LoadByVideoId = (id: string, callback: LoadByVideoIdCallback) => void
23} 21}
24 22
25export interface BlacklistedVideoClass { 23export interface BlacklistedVideoClass {
@@ -35,7 +33,8 @@ export interface BlacklistedVideoAttributes {
35 videoId: string 33 videoId: string
36} 34}
37 35
38export interface BlacklistedVideoInstance extends BlacklistedVideoClass, BlacklistedVideoAttributes, Sequelize.Instance<BlacklistedVideoAttributes> { 36export interface BlacklistedVideoInstance
37 extends BlacklistedVideoClass, BlacklistedVideoAttributes, Sequelize.Instance<BlacklistedVideoAttributes> {
39 id: number 38 id: number
40 createdAt: Date 39 createdAt: Date
41 updatedAt: Date 40 updatedAt: Date
@@ -43,4 +42,5 @@ export interface BlacklistedVideoInstance extends BlacklistedVideoClass, Blackli
43 toFormatedJSON: BlacklistedVideoMethods.ToFormatedJSON 42 toFormatedJSON: BlacklistedVideoMethods.ToFormatedJSON
44} 43}
45 44
46export interface BlacklistedVideoModel extends BlacklistedVideoClass, Sequelize.Model<BlacklistedVideoInstance, BlacklistedVideoAttributes> {} 45export interface BlacklistedVideoModel
46 extends BlacklistedVideoClass, Sequelize.Model<BlacklistedVideoInstance, BlacklistedVideoAttributes> {}
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'
2 2
3import { addMethodsToModel, getSort } from '../utils' 3import { addMethodsToModel, getSort } from '../utils'
4import { 4import {
5 BlacklistedVideoClass,
6 BlacklistedVideoInstance, 5 BlacklistedVideoInstance,
7 BlacklistedVideoAttributes, 6 BlacklistedVideoAttributes,
8 7
@@ -66,38 +65,39 @@ function associate (models) {
66 }) 65 })
67} 66}
68 67
69countTotal = function (callback: BlacklistedVideoMethods.CountTotalCallback) { 68countTotal = function () {
70 return BlacklistedVideo.count().asCallback(callback) 69 return BlacklistedVideo.count()
71} 70}
72 71
73list = function (callback: BlacklistedVideoMethods.ListCallback) { 72list = function () {
74 return BlacklistedVideo.findAll().asCallback(callback) 73 return BlacklistedVideo.findAll()
75} 74}
76 75
77listForApi = function (start: number, count: number, sort: string, callback: BlacklistedVideoMethods.ListForApiCallback) { 76listForApi = function (start: number, count: number, sort: string) {
78 const query = { 77 const query = {
79 offset: start, 78 offset: start,
80 limit: count, 79 limit: count,
81 order: [ getSort(sort) ] 80 order: [ getSort(sort) ]
82 } 81 }
83 82
84 return BlacklistedVideo.findAndCountAll(query).asCallback(function (err, result) { 83 return BlacklistedVideo.findAndCountAll(query).then(({ rows, count }) => {
85 if (err) return callback(err) 84 return {
86 85 data: rows,
87 return callback(null, result.rows, result.count) 86 total: count
87 }
88 }) 88 })
89} 89}
90 90
91loadById = function (id: number, callback: BlacklistedVideoMethods.LoadByIdCallback) { 91loadById = function (id: number) {
92 return BlacklistedVideo.findById(id).asCallback(callback) 92 return BlacklistedVideo.findById(id)
93} 93}
94 94
95loadByVideoId = function (id: string, callback: BlacklistedVideoMethods.LoadByIdCallback) { 95loadByVideoId = function (id: string) {
96 const query = { 96 const query = {
97 where: { 97 where: {
98 videoId: id 98 videoId: id
99 } 99 }
100 } 100 }
101 101
102 return BlacklistedVideo.find(query).asCallback(callback) 102 return BlacklistedVideo.findOne(query)
103} 103}
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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
2 3
3import { AuthorInstance } from './author-interface' 4import { AuthorInstance } from './author-interface'
4import { VideoTagInstance } from './video-tag-interface' 5import { TagAttributes, TagInstance } from './tag-interface'
5 6
6// Don't use barrel, import just what we need 7// Don't use barrel, import just what we need
7import { Video as FormatedVideo } from '../../../shared/models/video.model' 8import { Video as FormatedVideo } from '../../../shared/models/video.model'
9import { ResultList } from '../../../shared/models/result-list.model'
8 10
9export type FormatedAddRemoteVideo = { 11export type FormatedAddRemoteVideo = {
10 name: string 12 name: string
@@ -56,46 +58,32 @@ export namespace VideoMethods {
56 export type IsOwned = (this: VideoInstance) => boolean 58 export type IsOwned = (this: VideoInstance) => boolean
57 export type ToFormatedJSON = (this: VideoInstance) => FormatedVideo 59 export type ToFormatedJSON = (this: VideoInstance) => FormatedVideo
58 60
59 export type ToAddRemoteJSONCallback = (err: Error, videoFormated?: FormatedAddRemoteVideo) => void 61 export type ToAddRemoteJSON = (this: VideoInstance) => Promise<FormatedAddRemoteVideo>
60 export type ToAddRemoteJSON = (this: VideoInstance, callback: ToAddRemoteJSONCallback) => void
61
62 export type ToUpdateRemoteJSON = (this: VideoInstance) => FormatedUpdateRemoteVideo 62 export type ToUpdateRemoteJSON = (this: VideoInstance) => FormatedUpdateRemoteVideo
63 63
64 export type TranscodeVideofileCallback = (err: Error) => void 64 export type TranscodeVideofile = (this: VideoInstance) => Promise<void>
65 export type TranscodeVideofile = (this: VideoInstance, callback: TranscodeVideofileCallback) => void 65
66 66 // Return thumbnail name
67 export type GenerateThumbnailFromDataCallback = (err: Error, thumbnailName?: string) => void 67 export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
68 export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string, callback: GenerateThumbnailFromDataCallback) => void 68 export type GetDurationFromFile = (videoPath: string) => Promise<number>
69 69
70 export type GetDurationFromFileCallback = (err: Error, duration?: number) => void 70 export type List = () => Promise<VideoInstance[]>
71 export type GetDurationFromFile = (videoPath, callback) => void 71 export type ListOwnedAndPopulateAuthorAndTags = () => Promise<VideoInstance[]>
72 72 export type ListOwnedByAuthor = (author: string) => Promise<VideoInstance[]>
73 export type ListCallback = (err: Error, videoInstances: VideoInstance[]) => void 73
74 export type List = (callback: ListCallback) => void 74 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoInstance> >
75 75 export type SearchAndPopulateAuthorAndPodAndTags = (
76 export type ListForApiCallback = (err: Error, videoInstances?: VideoInstance[], total?: number) => void 76 value: string,
77 export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void 77 field: string,
78 78 start: number,
79 export type LoadByHostAndRemoteIdCallback = (err: Error, videoInstance: VideoInstance) => void 79 count: number,
80 export type LoadByHostAndRemoteId = (fromHost: string, remoteId: string, callback: LoadByHostAndRemoteIdCallback) => void 80 sort: string
81 81 ) => Promise< ResultList<VideoInstance> >
82 export type ListOwnedAndPopulateAuthorAndTagsCallback = (err: Error, videoInstances: VideoInstance[]) => void 82
83 export type ListOwnedAndPopulateAuthorAndTags = (callback: ListOwnedAndPopulateAuthorAndTagsCallback) => void 83 export type Load = (id: string) => Promise<VideoInstance>
84 84 export type LoadByHostAndRemoteId = (fromHost: string, remoteId: string) => Promise<VideoInstance>
85 export type ListOwnedByAuthorCallback = (err: Error, videoInstances: VideoInstance[]) => void 85 export type LoadAndPopulateAuthor = (id: string) => Promise<VideoInstance>
86 export type ListOwnedByAuthor = (author: string, callback: ListOwnedByAuthorCallback) => void 86 export type LoadAndPopulateAuthorAndPodAndTags = (id: string) => Promise<VideoInstance>
87
88 export type LoadCallback = (err: Error, videoInstance: VideoInstance) => void
89 export type Load = (id: string, callback: LoadCallback) => void
90
91 export type LoadAndPopulateAuthorCallback = (err: Error, videoInstance: VideoInstance) => void
92 export type LoadAndPopulateAuthor = (id: string, callback: LoadAndPopulateAuthorCallback) => void
93
94 export type LoadAndPopulateAuthorAndPodAndTagsCallback = (err: Error, videoInstance: VideoInstance) => void
95 export type LoadAndPopulateAuthorAndPodAndTags = (id: string, callback: LoadAndPopulateAuthorAndPodAndTagsCallback) => void
96
97 export type SearchAndPopulateAuthorAndPodAndTagsCallback = (err: Error, videoInstances?: VideoInstance[], total?: number) => void
98 export type SearchAndPopulateAuthorAndPodAndTags = (value: string, field: string, start: number, count: number, sort: string, callback: SearchAndPopulateAuthorAndPodAndTagsCallback) => void
99} 87}
100 88
101export interface VideoClass { 89export interface VideoClass {
@@ -139,7 +127,7 @@ export interface VideoAttributes {
139 dislikes?: number 127 dislikes?: number
140 128
141 Author?: AuthorInstance 129 Author?: AuthorInstance
142 Tags?: VideoTagInstance[] 130 Tags?: TagInstance[]
143} 131}
144 132
145export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> { 133export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> {
@@ -157,6 +145,8 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
157 toAddRemoteJSON: VideoMethods.ToAddRemoteJSON 145 toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
158 toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON 146 toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
159 transcodeVideofile: VideoMethods.TranscodeVideofile 147 transcodeVideofile: VideoMethods.TranscodeVideofile
148
149 setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
160} 150}
161 151
162export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {} 152export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {}
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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3import { addMethodsToModel } from '../utils'
4import { 3import {
5 VideoTagClass,
6 VideoTagInstance, 4 VideoTagInstance,
7 VideoTagAttributes, 5 VideoTagAttributes
8
9 VideoTagMethods
10} from './video-tag-interface' 6} from './video-tag-interface'
11 7
12let VideoTag: Sequelize.Model<VideoTagInstance, VideoTagAttributes> 8let VideoTag: Sequelize.Model<VideoTagInstance, VideoTagAttributes>
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 @@
1import * as safeBuffer from 'safe-buffer' 1import * as safeBuffer from 'safe-buffer'
2const Buffer = safeBuffer.Buffer 2const Buffer = safeBuffer.Buffer
3import * as createTorrent from 'create-torrent'
4import * as ffmpeg from 'fluent-ffmpeg' 3import * as ffmpeg from 'fluent-ffmpeg'
5import * as fs from 'fs'
6import * as magnetUtil from 'magnet-uri' 4import * as magnetUtil from 'magnet-uri'
7import { map, values } from 'lodash' 5import { map, values } from 'lodash'
8import { parallel, series } from 'async'
9import * as parseTorrent from 'parse-torrent' 6import * as parseTorrent from 'parse-torrent'
10import { join } from 'path' 7import { join } from 'path'
11import * as Sequelize from 'sequelize' 8import * as Sequelize from 'sequelize'
9import * as Promise from 'bluebird'
12 10
13import { database as db } from '../../initializers/database' 11import { database as db } from '../../initializers/database'
14import { VideoTagInstance } from './video-tag-interface' 12import { TagInstance } from './tag-interface'
15import { 13import {
16 logger, 14 logger,
17 isVideoNameValid, 15 isVideoNameValid,
@@ -21,7 +19,12 @@ import {
21 isVideoNSFWValid, 19 isVideoNSFWValid,
22 isVideoDescriptionValid, 20 isVideoDescriptionValid,
23 isVideoInfoHashValid, 21 isVideoInfoHashValid,
24 isVideoDurationValid 22 isVideoDurationValid,
23 readFileBufferPromise,
24 unlinkPromise,
25 renamePromise,
26 writeFilePromise,
27 createTorrentPromise
25} from '../../helpers' 28} from '../../helpers'
26import { 29import {
27 CONSTRAINTS_FIELDS, 30 CONSTRAINTS_FIELDS,
@@ -37,7 +40,6 @@ import { JobScheduler, removeVideoToFriends } from '../../lib'
37 40
38import { addMethodsToModel, getSort } from '../utils' 41import { addMethodsToModel, getSort } from '../utils'
39import { 42import {
40 VideoClass,
41 VideoInstance, 43 VideoInstance,
42 VideoAttributes, 44 VideoAttributes,
43 45
@@ -260,7 +262,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
260 toFormatedJSON, 262 toFormatedJSON,
261 toAddRemoteJSON, 263 toAddRemoteJSON,
262 toUpdateRemoteJSON, 264 toUpdateRemoteJSON,
263 transcodeVideofile, 265 transcodeVideofile
264 ] 266 ]
265 addMethodsToModel(Video, classMethods, instanceMethods) 267 addMethodsToModel(Video, classMethods, instanceMethods)
266 268
@@ -276,91 +278,53 @@ function beforeValidate (video: VideoInstance) {
276} 278}
277 279
278function beforeCreate (video: VideoInstance, options: { transaction: Sequelize.Transaction }) { 280function beforeCreate (video: VideoInstance, options: { transaction: Sequelize.Transaction }) {
279 return new Promise(function (resolve, reject) { 281 if (video.isOwned()) {
282 const videoPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
280 const tasks = [] 283 const tasks = []
281 284
282 if (video.isOwned()) { 285 tasks.push(
283 const videoPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) 286 createTorrentFromVideo(video, videoPath),
284 287 createThumbnail(video, videoPath),
285 tasks.push( 288 createPreview(video, videoPath)
286 function createVideoTorrent (callback) { 289 )
287 createTorrentFromVideo(video, videoPath, callback)
288 },
289
290 function createVideoThumbnail (callback) {
291 createThumbnail(video, videoPath, callback)
292 },
293
294 function createVideoPreview (callback) {
295 createPreview(video, videoPath, callback)
296 }
297 )
298
299 if (CONFIG.TRANSCODING.ENABLED === true) {
300 tasks.push(
301 function createVideoTranscoderJob (callback) {
302 const dataInput = {
303 id: video.id
304 }
305 290
306 JobScheduler.Instance.createJob(options.transaction, 'videoTranscoder', dataInput, callback) 291 if (CONFIG.TRANSCODING.ENABLED === true) {
307 } 292 const dataInput = {
308 ) 293 id: video.id
309 } 294 }
310 295
311 return parallel(tasks, function (err) { 296 tasks.push(
312 if (err) return reject(err) 297 JobScheduler.Instance.createJob(options.transaction, 'videoTranscoder', dataInput)
313 298 )
314 return resolve()
315 })
316 } 299 }
317 300
318 return resolve() 301 return Promise.all(tasks)
319 }) 302 }
303
304 return Promise.resolve()
320} 305}
321 306
322function afterDestroy (video: VideoInstance) { 307function afterDestroy (video: VideoInstance) {
323 return new Promise(function (resolve, reject) { 308 const tasks = []
324 const tasks = []
325
326 tasks.push(
327 function (callback) {
328 removeThumbnail(video, callback)
329 }
330 )
331
332 if (video.isOwned()) {
333 tasks.push(
334 function removeVideoFile (callback) {
335 removeFile(video, callback)
336 },
337 309
338 function removeVideoTorrent (callback) { 310 tasks.push(
339 removeTorrent(video, callback) 311 removeThumbnail(video)
340 }, 312 )
341
342 function removeVideoPreview (callback) {
343 removePreview(video, callback)
344 },
345
346 function notifyFriends (callback) {
347 const params = {
348 remoteId: video.id
349 }
350
351 removeVideoToFriends(params)
352 313
353 return callback() 314 if (video.isOwned()) {
354 } 315 const removeVideoToFriendsParams = {
355 ) 316 remoteId: video.id
356 } 317 }
357 318
358 parallel(tasks, function (err) { 319 tasks.push(
359 if (err) return reject(err) 320 removeFile(video),
321 removeTorrent(video),
322 removePreview(video),
323 removeVideoToFriends(removeVideoToFriendsParams)
324 )
325 }
360 326
361 return resolve() 327 return Promise.all(tasks)
362 })
363 })
364} 328}
365 329
366// ------------------------------ METHODS ------------------------------ 330// ------------------------------ METHODS ------------------------------
@@ -488,7 +452,7 @@ toFormatedJSON = function (this: VideoInstance) {
488 views: this.views, 452 views: this.views,
489 likes: this.likes, 453 likes: this.likes,
490 dislikes: this.dislikes, 454 dislikes: this.dislikes,
491 tags: map<VideoTagInstance, string>(this.Tags, 'name'), 455 tags: map<TagInstance, string>(this.Tags, 'name'),
492 thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), 456 thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
493 createdAt: this.createdAt, 457 createdAt: this.createdAt,
494 updatedAt: this.updatedAt 458 updatedAt: this.updatedAt
@@ -497,15 +461,11 @@ toFormatedJSON = function (this: VideoInstance) {
497 return json 461 return json
498} 462}
499 463
500toAddRemoteJSON = function (this: VideoInstance, callback: VideoMethods.ToAddRemoteJSONCallback) { 464toAddRemoteJSON = function (this: VideoInstance) {
501 // Get thumbnail data to send to the other pod 465 // Get thumbnail data to send to the other pod
502 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) 466 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
503 fs.readFile(thumbnailPath, (err, thumbnailData) => {
504 if (err) {
505 logger.error('Cannot read the thumbnail of the video')
506 return callback(err)
507 }
508 467
468 return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
509 const remoteVideo = { 469 const remoteVideo = {
510 name: this.name, 470 name: this.name,
511 category: this.category, 471 category: this.category,
@@ -518,7 +478,7 @@ toAddRemoteJSON = function (this: VideoInstance, callback: VideoMethods.ToAddRem
518 author: this.Author.name, 478 author: this.Author.name,
519 duration: this.duration, 479 duration: this.duration,
520 thumbnailData: thumbnailData.toString('binary'), 480 thumbnailData: thumbnailData.toString('binary'),
521 tags: map<VideoTagInstance, string>(this.Tags, 'name'), 481 tags: map<TagInstance, string>(this.Tags, 'name'),
522 createdAt: this.createdAt, 482 createdAt: this.createdAt,
523 updatedAt: this.updatedAt, 483 updatedAt: this.updatedAt,
524 extname: this.extname, 484 extname: this.extname,
@@ -527,7 +487,7 @@ toAddRemoteJSON = function (this: VideoInstance, callback: VideoMethods.ToAddRem
527 dislikes: this.dislikes 487 dislikes: this.dislikes
528 } 488 }
529 489
530 return callback(null, remoteVideo) 490 return remoteVideo
531 }) 491 })
532} 492}
533 493
@@ -543,7 +503,7 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
543 remoteId: this.id, 503 remoteId: this.id,
544 author: this.Author.name, 504 author: this.Author.name,
545 duration: this.duration, 505 duration: this.duration,
546 tags: map<VideoTagInstance, string>(this.Tags, 'name'), 506 tags: map<TagInstance, string>(this.Tags, 'name'),
547 createdAt: this.createdAt, 507 createdAt: this.createdAt,
548 updatedAt: this.updatedAt, 508 updatedAt: this.updatedAt,
549 extname: this.extname, 509 extname: this.extname,
@@ -555,7 +515,7 @@ toUpdateRemoteJSON = function (this: VideoInstance) {
555 return json 515 return json
556} 516}
557 517
558transcodeVideofile = function (this: VideoInstance, finalCallback: VideoMethods.TranscodeVideofileCallback) { 518transcodeVideofile = function (this: VideoInstance) {
559 const video = this 519 const video = this
560 520
561 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 521 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
@@ -563,78 +523,73 @@ transcodeVideofile = function (this: VideoInstance, finalCallback: VideoMethods.
563 const videoInputPath = join(videosDirectory, video.getVideoFilename()) 523 const videoInputPath = join(videosDirectory, video.getVideoFilename())
564 const videoOutputPath = join(videosDirectory, video.id + '-transcoded' + newExtname) 524 const videoOutputPath = join(videosDirectory, video.id + '-transcoded' + newExtname)
565 525
566 ffmpeg(videoInputPath) 526 return new Promise<void>((res, rej) => {
567 .output(videoOutputPath) 527 ffmpeg(videoInputPath)
568 .videoCodec('libx264') 528 .output(videoOutputPath)
569 .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) 529 .videoCodec('libx264')
570 .outputOption('-movflags faststart') 530 .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
571 .on('error', finalCallback) 531 .outputOption('-movflags faststart')
572 .on('end', function () { 532 .on('error', rej)
573 series([ 533 .on('end', () => {
574 function removeOldFile (callback) { 534
575 fs.unlink(videoInputPath, callback) 535 return unlinkPromise(videoInputPath)
576 }, 536 .then(() => {
577 537 // Important to do this before getVideoFilename() to take in account the new file extension
578 function moveNewFile (callback) { 538 video.set('extname', newExtname)
579 // Important to do this before getVideoFilename() to take in account the new file extension 539
580 video.set('extname', newExtname) 540 const newVideoPath = join(videosDirectory, video.getVideoFilename())
581 541 return renamePromise(videoOutputPath, newVideoPath)
582 const newVideoPath = join(videosDirectory, video.getVideoFilename())
583 fs.rename(videoOutputPath, newVideoPath, callback)
584 },
585
586 function torrent (callback) {
587 const newVideoPath = join(videosDirectory, video.getVideoFilename())
588 createTorrentFromVideo(video, newVideoPath, callback)
589 },
590
591 function videoExtension (callback) {
592 video.save().asCallback(callback)
593 }
594
595 ], function (err: Error) {
596 if (err) {
597 // Autodesctruction...
598 video.destroy().asCallback(function (err) {
599 if (err) logger.error('Cannot destruct video after transcoding failure.', { error: err })
600 }) 542 })
543 .then(() => {
544 const newVideoPath = join(videosDirectory, video.getVideoFilename())
545 return createTorrentFromVideo(video, newVideoPath)
546 })
547 .then(() => {
548 return video.save()
549 })
550 .then(() => {
551 return res()
552 })
553 .catch(err => {
554 // Autodesctruction...
555 video.destroy().asCallback(function (err) {
556 if (err) logger.error('Cannot destruct video after transcoding failure.', { error: err })
557 })
601 558
602 return finalCallback(err) 559 return rej(err)
603 } 560 })
604
605 return finalCallback(null)
606 }) 561 })
607 }) 562 .run()
608 .run() 563 })
609} 564}
610 565
611// ------------------------------ STATICS ------------------------------ 566// ------------------------------ STATICS ------------------------------
612 567
613generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string, callback: VideoMethods.GenerateThumbnailFromDataCallback) { 568generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
614 // Creating the thumbnail for a remote video 569 // Creating the thumbnail for a remote video
615 570
616 const thumbnailName = video.getThumbnailName() 571 const thumbnailName = video.getThumbnailName()
617 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) 572 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
618 fs.writeFile(thumbnailPath, Buffer.from(thumbnailData, 'binary'), function (err) { 573 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
619 if (err) return callback(err) 574 return thumbnailName
620
621 return callback(null, thumbnailName)
622 }) 575 })
623} 576}
624 577
625getDurationFromFile = function (videoPath: string, callback: VideoMethods.GetDurationFromFileCallback) { 578getDurationFromFile = function (videoPath: string) {
626 ffmpeg.ffprobe(videoPath, function (err, metadata) { 579 return new Promise<number>((res, rej) => {
627 if (err) return callback(err) 580 ffmpeg.ffprobe(videoPath, function (err, metadata) {
581 if (err) return rej(err)
628 582
629 return callback(null, Math.floor(metadata.format.duration)) 583 return res(Math.floor(metadata.format.duration))
584 })
630 }) 585 })
631} 586}
632 587
633list = function (callback: VideoMethods.ListCallback) { 588list = function () {
634 return Video.findAll().asCallback(callback) 589 return Video.findAll()
635} 590}
636 591
637listForApi = function (start: number, count: number, sort: string, callback: VideoMethods.ListForApiCallback) { 592listForApi = function (start: number, count: number, sort: string) {
638 // Exclude Blakclisted videos from the list 593 // Exclude Blakclisted videos from the list
639 const query = { 594 const query = {
640 distinct: true, 595 distinct: true,
@@ -652,14 +607,15 @@ listForApi = function (start: number, count: number, sort: string, callback: Vid
652 where: createBaseVideosWhere() 607 where: createBaseVideosWhere()
653 } 608 }
654 609
655 return Video.findAndCountAll(query).asCallback(function (err, result) { 610 return Video.findAndCountAll(query).then(({ rows, count }) => {
656 if (err) return callback(err) 611 return {
657 612 data: rows,
658 return callback(null, result.rows, result.count) 613 total: count
614 }
659 }) 615 })
660} 616}
661 617
662loadByHostAndRemoteId = function (fromHost: string, remoteId: string, callback: VideoMethods.LoadByHostAndRemoteIdCallback) { 618loadByHostAndRemoteId = function (fromHost: string, remoteId: string) {
663 const query = { 619 const query = {
664 where: { 620 where: {
665 remoteId: remoteId 621 remoteId: remoteId
@@ -680,10 +636,10 @@ loadByHostAndRemoteId = function (fromHost: string, remoteId: string, callback:
680 ] 636 ]
681 } 637 }
682 638
683 return Video.findOne(query).asCallback(callback) 639 return Video.findOne(query)
684} 640}
685 641
686listOwnedAndPopulateAuthorAndTags = function (callback: VideoMethods.ListOwnedAndPopulateAuthorAndTagsCallback) { 642listOwnedAndPopulateAuthorAndTags = function () {
687 // If remoteId is null this is *our* video 643 // If remoteId is null this is *our* video
688 const query = { 644 const query = {
689 where: { 645 where: {
@@ -692,10 +648,10 @@ listOwnedAndPopulateAuthorAndTags = function (callback: VideoMethods.ListOwnedAn
692 include: [ Video['sequelize'].models.Author, Video['sequelize'].models.Tag ] 648 include: [ Video['sequelize'].models.Author, Video['sequelize'].models.Tag ]
693 } 649 }
694 650
695 return Video.findAll(query).asCallback(callback) 651 return Video.findAll(query)
696} 652}
697 653
698listOwnedByAuthor = function (author: string, callback: VideoMethods.ListOwnedByAuthorCallback) { 654listOwnedByAuthor = function (author: string) {
699 const query = { 655 const query = {
700 where: { 656 where: {
701 remoteId: null 657 remoteId: null
@@ -710,22 +666,22 @@ listOwnedByAuthor = function (author: string, callback: VideoMethods.ListOwnedBy
710 ] 666 ]
711 } 667 }
712 668
713 return Video.findAll(query).asCallback(callback) 669 return Video.findAll(query)
714} 670}
715 671
716load = function (id: string, callback: VideoMethods.LoadCallback) { 672load = function (id: string) {
717 return Video.findById(id).asCallback(callback) 673 return Video.findById(id)
718} 674}
719 675
720loadAndPopulateAuthor = function (id: string, callback: VideoMethods.LoadAndPopulateAuthorCallback) { 676loadAndPopulateAuthor = function (id: string) {
721 const options = { 677 const options = {
722 include: [ Video['sequelize'].models.Author ] 678 include: [ Video['sequelize'].models.Author ]
723 } 679 }
724 680
725 return Video.findById(id, options).asCallback(callback) 681 return Video.findById(id, options)
726} 682}
727 683
728loadAndPopulateAuthorAndPodAndTags = function (id: string, callback: VideoMethods.LoadAndPopulateAuthorAndPodAndTagsCallback) { 684loadAndPopulateAuthorAndPodAndTags = function (id: string) {
729 const options = { 685 const options = {
730 include: [ 686 include: [
731 { 687 {
@@ -736,17 +692,10 @@ loadAndPopulateAuthorAndPodAndTags = function (id: string, callback: VideoMethod
736 ] 692 ]
737 } 693 }
738 694
739 return Video.findById(id, options).asCallback(callback) 695 return Video.findById(id, options)
740} 696}
741 697
742searchAndPopulateAuthorAndPodAndTags = function ( 698searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
743 value: string,
744 field: string,
745 start: number,
746 count: number,
747 sort: string,
748 callback: VideoMethods.SearchAndPopulateAuthorAndPodAndTagsCallback
749) {
750 const podInclude: any = { 699 const podInclude: any = {
751 model: Video['sequelize'].models.Pod, 700 model: Video['sequelize'].models.Pod,
752 required: false 701 required: false
@@ -778,7 +727,11 @@ searchAndPopulateAuthorAndPodAndTags = function (
778 } else if (field === 'tags') { 727 } else if (field === 'tags') {
779 const escapedValue = Video['sequelize'].escape('%' + value + '%') 728 const escapedValue = Video['sequelize'].escape('%' + value + '%')
780 query.where.id.$in = Video['sequelize'].literal( 729 query.where.id.$in = Video['sequelize'].literal(
781 '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')' 730 `(SELECT "VideoTags"."videoId"
731 FROM "Tags"
732 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
733 WHERE name LIKE ${escapedValue}
734 )`
782 ) 735 )
783 } else if (field === 'host') { 736 } else if (field === 'host') {
784 // FIXME: Include our pod? (not stored in the database) 737 // FIXME: Include our pod? (not stored in the database)
@@ -810,10 +763,11 @@ searchAndPopulateAuthorAndPodAndTags = function (
810 // query.include.push([ Video['sequelize'].models.Tag ]) 763 // query.include.push([ Video['sequelize'].models.Tag ])
811 } 764 }
812 765
813 return Video.findAndCountAll(query).asCallback(function (err, result) { 766 return Video.findAndCountAll(query).then(({ rows, count }) => {
814 if (err) return callback(err) 767 return {
815 768 data: rows,
816 return callback(null, result.rows, result.count) 769 total: count
770 }
817 }) 771 })
818} 772}
819 773
@@ -829,27 +783,27 @@ function createBaseVideosWhere () {
829 } 783 }
830} 784}
831 785
832function removeThumbnail (video: VideoInstance, callback: (err: Error) => void) { 786function removeThumbnail (video: VideoInstance) {
833 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()) 787 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
834 fs.unlink(thumbnailPath, callback) 788 return unlinkPromise(thumbnailPath)
835} 789}
836 790
837function removeFile (video: VideoInstance, callback: (err: Error) => void) { 791function removeFile (video: VideoInstance) {
838 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) 792 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
839 fs.unlink(filePath, callback) 793 return unlinkPromise(filePath)
840} 794}
841 795
842function removeTorrent (video: VideoInstance, callback: (err: Error) => void) { 796function removeTorrent (video: VideoInstance) {
843 const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) 797 const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
844 fs.unlink(torrenPath, callback) 798 return unlinkPromise(torrenPath)
845} 799}
846 800
847function removePreview (video: VideoInstance, callback: (err: Error) => void) { 801function removePreview (video: VideoInstance) {
848 // Same name than video thumnail 802 // Same name than video thumnail
849 fs.unlink(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName(), callback) 803 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName())
850} 804}
851 805
852function createTorrentFromVideo (video: VideoInstance, videoPath: string, callback: (err: Error) => void) { 806function createTorrentFromVideo (video: VideoInstance, videoPath: string) {
853 const options = { 807 const options = {
854 announceList: [ 808 announceList: [
855 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] 809 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
@@ -859,30 +813,27 @@ function createTorrentFromVideo (video: VideoInstance, videoPath: string, callba
859 ] 813 ]
860 } 814 }
861 815
862 createTorrent(videoPath, options, function (err, torrent) { 816 return createTorrentPromise(videoPath, options)
863 if (err) return callback(err) 817 .then(torrent => {
864 818 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
865 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) 819 return writeFilePromise(filePath, torrent).then(() => torrent)
866 fs.writeFile(filePath, torrent, function (err) { 820 })
867 if (err) return callback(err) 821 .then(torrent => {
868
869 const parsedTorrent = parseTorrent(torrent) 822 const parsedTorrent = parseTorrent(torrent)
870 video.set('infoHash', parsedTorrent.infoHash) 823 video.set('infoHash', parsedTorrent.infoHash)
871 video.validate().asCallback(callback) 824 return video.validate()
872 }) 825 })
873 })
874} 826}
875 827
876function createPreview (video: VideoInstance, videoPath: string, callback: (err: Error) => void) { 828function createPreview (video: VideoInstance, videoPath: string) {
877 generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null, callback) 829 return generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null)
878} 830}
879 831
880function createThumbnail (video: VideoInstance, videoPath: string, callback: (err: Error) => void) { 832function createThumbnail (video: VideoInstance, videoPath: string) {
881 generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE, callback) 833 return generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE)
882} 834}
883 835
884type GenerateImageCallback = (err: Error, imageName: string) => void 836function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) {
885function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string, callback?: GenerateImageCallback) {
886 const options: any = { 837 const options: any = {
887 filename: imageName, 838 filename: imageName,
888 count: 1, 839 count: 1,
@@ -893,29 +844,25 @@ function generateImage (video: VideoInstance, videoPath: string, folder: string,
893 options.size = size 844 options.size = size
894 } 845 }
895 846
896 ffmpeg(videoPath) 847 return new Promise<string>((res, rej) => {
897 .on('error', callback) 848 ffmpeg(videoPath)
898 .on('end', function () { 849 .on('error', rej)
899 callback(null, imageName) 850 .on('end', function () {
900 }) 851 return res(imageName)
901 .thumbnail(options) 852 })
853 .thumbnail(options)
854 })
902} 855}
903 856
904function removeFromBlacklist (video: VideoInstance, callback: (err: Error) => void) { 857function removeFromBlacklist (video: VideoInstance) {
905 // Find the blacklisted video 858 // Find the blacklisted video
906 db.BlacklistedVideo.loadByVideoId(video.id, function (err, video) { 859 return db.BlacklistedVideo.loadByVideoId(video.id).then(video => {
907 // If an error occured, stop here 860 // Not found the video, skip
908 if (err) { 861 if (!video) {
909 logger.error('Error when fetching video from blacklist.', { error: err }) 862 return null
910 return callback(err)
911 } 863 }
912 864
913 // If we found the video, remove it from the blacklist 865 // If we found the video, remove it from the blacklist
914 if (video) { 866 return video.destroy()
915 video.destroy().asCallback(callback)
916 } else {
917 // If haven't found it, simply ignore it and do nothing
918 return callback(null)
919 }
920 }) 867 })
921} 868}
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 () {
171 function (next) { 171 function (next) {
172 setTimeout(next, 22000) 172 setTimeout(next, 22000)
173 }, 173 },
174 // Check the pods 1, 2 expulsed pod 4
175 function (next) {
176 each([ 1, 2 ], function (i, callback) {
177 getFriendsList(i, function (err, res) {
178 if (err) throw err
179
180 // Pod 4 should not be our friend
181 const result = res.body.data
182 expect(result.length).to.equal(2)
183 for (const pod of result) {
184 expect(pod.host).not.equal(servers[3].host)
185 }
186
187 callback()
188 })
189 }, next)
190 },
174 // Rerun server 4 191 // Rerun server 4
175 function (next) { 192 function (next) {
176 serversUtils.runServer(4, function (server) { 193 serversUtils.runServer(4, function (server) {
@@ -187,7 +204,7 @@ describe('Test advanced friends', function () {
187 next() 204 next()
188 }) 205 })
189 }, 206 },
190 // Pod 6 ask pod 1, 2 and 3 207 // Pod 6 asks pod 1, 2 and 3
191 function (next) { 208 function (next) {
192 makeFriends(6, next) 209 makeFriends(6, next)
193 }, 210 },