]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/redis.ts
Merge branch 'release/4.0.0' into develop
[github/Chocobozzz/PeerTube.git] / server / lib / redis.ts
1 import express from 'express'
2 import { createClient, RedisClient } from 'redis'
3 import { logger } from '../helpers/logger'
4 import { generateRandomString } from '../helpers/utils'
5 import {
6 CONTACT_FORM_LIFETIME,
7 USER_EMAIL_VERIFY_LIFETIME,
8 USER_PASSWORD_RESET_LIFETIME,
9 USER_PASSWORD_CREATE_LIFETIME,
10 VIEW_LIFETIME,
11 WEBSERVER,
12 TRACKER_RATE_LIMITS,
13 RESUMABLE_UPLOAD_SESSION_LIFETIME
14 } from '../initializers/constants'
15 import { CONFIG } from '../initializers/config'
16 import { exists } from '@server/helpers/custom-validators/misc'
17
18 type CachedRoute = {
19 body: string
20 contentType?: string
21 statusCode?: string
22 }
23
24 class Redis {
25
26 private static instance: Redis
27 private initialized = false
28 private client: RedisClient
29 private prefix: string
30
31 private constructor () {
32 }
33
34 init () {
35 // Already initialized
36 if (this.initialized === true) return
37 this.initialized = true
38
39 this.client = createClient(Redis.getRedisClientOptions())
40
41 this.client.on('error', err => {
42 logger.error('Error in Redis client.', { err })
43 process.exit(-1)
44 })
45
46 if (CONFIG.REDIS.AUTH) {
47 this.client.auth(CONFIG.REDIS.AUTH)
48 }
49
50 this.prefix = 'redis-' + WEBSERVER.HOST + '-'
51 }
52
53 static getRedisClientOptions () {
54 return Object.assign({},
55 (CONFIG.REDIS.AUTH && CONFIG.REDIS.AUTH != null) ? { password: CONFIG.REDIS.AUTH } : {},
56 (CONFIG.REDIS.DB) ? { db: CONFIG.REDIS.DB } : {},
57 (CONFIG.REDIS.HOSTNAME && CONFIG.REDIS.PORT)
58 ? { host: CONFIG.REDIS.HOSTNAME, port: CONFIG.REDIS.PORT }
59 : { path: CONFIG.REDIS.SOCKET }
60 )
61 }
62
63 getClient () {
64 return this.client
65 }
66
67 getPrefix () {
68 return this.prefix
69 }
70
71 /* ************ Forgot password ************ */
72
73 async setResetPasswordVerificationString (userId: number) {
74 const generatedString = await generateRandomString(32)
75
76 await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_RESET_LIFETIME)
77
78 return generatedString
79 }
80
81 async setCreatePasswordVerificationString (userId: number) {
82 const generatedString = await generateRandomString(32)
83
84 await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_CREATE_LIFETIME)
85
86 return generatedString
87 }
88
89 async removePasswordVerificationString (userId: number) {
90 return this.removeValue(this.generateResetPasswordKey(userId))
91 }
92
93 async getResetPasswordLink (userId: number) {
94 return this.getValue(this.generateResetPasswordKey(userId))
95 }
96
97 /* ************ Email verification ************ */
98
99 async setVerifyEmailVerificationString (userId: number) {
100 const generatedString = await generateRandomString(32)
101
102 await this.setValue(this.generateVerifyEmailKey(userId), generatedString, USER_EMAIL_VERIFY_LIFETIME)
103
104 return generatedString
105 }
106
107 async getVerifyEmailLink (userId: number) {
108 return this.getValue(this.generateVerifyEmailKey(userId))
109 }
110
111 /* ************ Contact form per IP ************ */
112
113 async setContactFormIp (ip: string) {
114 return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME)
115 }
116
117 async doesContactFormIpExist (ip: string) {
118 return this.exists(this.generateContactFormKey(ip))
119 }
120
121 /* ************ Views per IP ************ */
122
123 setIPVideoView (ip: string, videoUUID: string) {
124 return this.setValue(this.generateIPViewKey(ip, videoUUID), '1', VIEW_LIFETIME.VIEW)
125 }
126
127 setIPVideoViewer (ip: string, videoUUID: string) {
128 return this.setValue(this.generateIPViewerKey(ip, videoUUID), '1', VIEW_LIFETIME.VIEWER)
129 }
130
131 async doesVideoIPViewExist (ip: string, videoUUID: string) {
132 return this.exists(this.generateIPViewKey(ip, videoUUID))
133 }
134
135 async doesVideoIPViewerExist (ip: string, videoUUID: string) {
136 return this.exists(this.generateIPViewerKey(ip, videoUUID))
137 }
138
139 /* ************ Tracker IP block ************ */
140
141 setTrackerBlockIP (ip: string) {
142 return this.setValue(this.generateTrackerBlockIPKey(ip), '1', TRACKER_RATE_LIMITS.BLOCK_IP_LIFETIME)
143 }
144
145 async doesTrackerBlockIPExist (ip: string) {
146 return this.exists(this.generateTrackerBlockIPKey(ip))
147 }
148
149 /* ************ API cache ************ */
150
151 async getCachedRoute (req: express.Request) {
152 const cached = await this.getObject(this.generateCachedRouteKey(req))
153
154 return cached as CachedRoute
155 }
156
157 setCachedRoute (req: express.Request, body: any, lifetime: number, contentType?: string, statusCode?: number) {
158 const cached: CachedRoute = Object.assign(
159 {},
160 { body: body.toString() },
161 (contentType) ? { contentType } : null,
162 (statusCode) ? { statusCode: statusCode.toString() } : null
163 )
164
165 return this.setObject(this.generateCachedRouteKey(req), cached, lifetime)
166 }
167
168 /* ************ Video views stats ************ */
169
170 addVideoViewStats (videoId: number) {
171 const { videoKey, setKey } = this.generateVideoViewStatsKeys({ videoId })
172
173 return Promise.all([
174 this.addToSet(setKey, videoId.toString()),
175 this.increment(videoKey)
176 ])
177 }
178
179 async getVideoViewsStats (videoId: number, hour: number) {
180 const { videoKey } = this.generateVideoViewStatsKeys({ videoId, hour })
181
182 const valueString = await this.getValue(videoKey)
183 const valueInt = parseInt(valueString, 10)
184
185 if (isNaN(valueInt)) {
186 logger.error('Cannot get videos views stats of video %d in hour %d: views number is NaN (%s).', videoId, hour, valueString)
187 return undefined
188 }
189
190 return valueInt
191 }
192
193 async listVideosViewedForStats (hour: number) {
194 const { setKey } = this.generateVideoViewStatsKeys({ hour })
195
196 const stringIds = await this.getSet(setKey)
197 return stringIds.map(s => parseInt(s, 10))
198 }
199
200 deleteVideoViewsStats (videoId: number, hour: number) {
201 const { setKey, videoKey } = this.generateVideoViewStatsKeys({ videoId, hour })
202
203 return Promise.all([
204 this.deleteFromSet(setKey, videoId.toString()),
205 this.deleteKey(videoKey)
206 ])
207 }
208
209 /* ************ Local video views buffer ************ */
210
211 addLocalVideoView (videoId: number) {
212 const { videoKey, setKey } = this.generateLocalVideoViewsKeys(videoId)
213
214 return Promise.all([
215 this.addToSet(setKey, videoId.toString()),
216 this.increment(videoKey)
217 ])
218 }
219
220 async getLocalVideoViews (videoId: number) {
221 const { videoKey } = this.generateLocalVideoViewsKeys(videoId)
222
223 const valueString = await this.getValue(videoKey)
224 const valueInt = parseInt(valueString, 10)
225
226 if (isNaN(valueInt)) {
227 logger.error('Cannot get videos views of video %d: views number is NaN (%s).', videoId, valueString)
228 return undefined
229 }
230
231 return valueInt
232 }
233
234 async listLocalVideosViewed () {
235 const { setKey } = this.generateLocalVideoViewsKeys()
236
237 const stringIds = await this.getSet(setKey)
238 return stringIds.map(s => parseInt(s, 10))
239 }
240
241 deleteLocalVideoViews (videoId: number) {
242 const { setKey, videoKey } = this.generateLocalVideoViewsKeys(videoId)
243
244 return Promise.all([
245 this.deleteFromSet(setKey, videoId.toString()),
246 this.deleteKey(videoKey)
247 ])
248 }
249
250 /* ************ Resumable uploads final responses ************ */
251
252 setUploadSession (uploadId: string, response?: { video: { id: number, shortUUID: string, uuid: string } }) {
253 return this.setValue(
254 'resumable-upload-' + uploadId,
255 response
256 ? JSON.stringify(response)
257 : '',
258 RESUMABLE_UPLOAD_SESSION_LIFETIME
259 )
260 }
261
262 doesUploadSessionExist (uploadId: string) {
263 return this.exists('resumable-upload-' + uploadId)
264 }
265
266 async getUploadSession (uploadId: string) {
267 const value = await this.getValue('resumable-upload-' + uploadId)
268
269 return value
270 ? JSON.parse(value)
271 : ''
272 }
273
274 deleteUploadSession (uploadId: string) {
275 return this.deleteKey('resumable-upload-' + uploadId)
276 }
277
278 /* ************ Keys generation ************ */
279
280 generateCachedRouteKey (req: express.Request) {
281 return req.method + '-' + req.originalUrl
282 }
283
284 private generateLocalVideoViewsKeys (videoId?: Number) {
285 return { setKey: `local-video-views-buffer`, videoKey: `local-video-views-buffer-${videoId}` }
286 }
287
288 private generateVideoViewStatsKeys (options: { videoId?: number, hour?: number }) {
289 const hour = exists(options.hour)
290 ? options.hour
291 : new Date().getHours()
292
293 return { setKey: `videos-view-h${hour}`, videoKey: `video-view-${options.videoId}-h${hour}` }
294 }
295
296 private generateResetPasswordKey (userId: number) {
297 return 'reset-password-' + userId
298 }
299
300 private generateVerifyEmailKey (userId: number) {
301 return 'verify-email-' + userId
302 }
303
304 private generateIPViewKey (ip: string, videoUUID: string) {
305 return `views-${videoUUID}-${ip}`
306 }
307
308 private generateIPViewerKey (ip: string, videoUUID: string) {
309 return `viewer-${videoUUID}-${ip}`
310 }
311
312 private generateTrackerBlockIPKey (ip: string) {
313 return `tracker-block-ip-${ip}`
314 }
315
316 private generateContactFormKey (ip: string) {
317 return 'contact-form-' + ip
318 }
319
320 /* ************ Redis helpers ************ */
321
322 private getValue (key: string) {
323 return new Promise<string>((res, rej) => {
324 this.client.get(this.prefix + key, (err, value) => {
325 if (err) return rej(err)
326
327 return res(value)
328 })
329 })
330 }
331
332 private getSet (key: string) {
333 return new Promise<string[]>((res, rej) => {
334 this.client.smembers(this.prefix + key, (err, value) => {
335 if (err) return rej(err)
336
337 return res(value)
338 })
339 })
340 }
341
342 private addToSet (key: string, value: string) {
343 return new Promise<void>((res, rej) => {
344 this.client.sadd(this.prefix + key, value, err => err ? rej(err) : res())
345 })
346 }
347
348 private deleteFromSet (key: string, value: string) {
349 return new Promise<void>((res, rej) => {
350 this.client.srem(this.prefix + key, value, err => err ? rej(err) : res())
351 })
352 }
353
354 private deleteKey (key: string) {
355 return new Promise<void>((res, rej) => {
356 this.client.del(this.prefix + key, err => err ? rej(err) : res())
357 })
358 }
359
360 private deleteFieldInHash (key: string, field: string) {
361 return new Promise<void>((res, rej) => {
362 this.client.hdel(this.prefix + key, field, err => err ? rej(err) : res())
363 })
364 }
365
366 private setValue (key: string, value: string, expirationMilliseconds: number) {
367 return new Promise<void>((res, rej) => {
368 this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => {
369 if (err) return rej(err)
370
371 if (ok !== 'OK') return rej(new Error('Redis set result is not OK.'))
372
373 return res()
374 })
375 })
376 }
377
378 private removeValue (key: string) {
379 return new Promise<void>((res, rej) => {
380 this.client.del(this.prefix + key, err => {
381 if (err) return rej(err)
382
383 return res()
384 })
385 })
386 }
387
388 private setObject (key: string, obj: { [id: string]: string }, expirationMilliseconds: number) {
389 return new Promise<void>((res, rej) => {
390 this.client.hmset(this.prefix + key, obj, (err, ok) => {
391 if (err) return rej(err)
392 if (!ok) return rej(new Error('Redis mset result is not OK.'))
393
394 this.client.pexpire(this.prefix + key, expirationMilliseconds, (err, ok) => {
395 if (err) return rej(err)
396 if (!ok) return rej(new Error('Redis expiration result is not OK.'))
397
398 return res()
399 })
400 })
401 })
402 }
403
404 private getObject (key: string) {
405 return new Promise<{ [id: string]: string }>((res, rej) => {
406 this.client.hgetall(this.prefix + key, (err, value) => {
407 if (err) return rej(err)
408
409 return res(value)
410 })
411 })
412 }
413
414 private setValueInHash (key: string, field: string, value: string) {
415 return new Promise<void>((res, rej) => {
416 this.client.hset(this.prefix + key, field, value, (err) => {
417 if (err) return rej(err)
418
419 return res()
420 })
421 })
422 }
423
424 private increment (key: string) {
425 return new Promise<number>((res, rej) => {
426 this.client.incr(this.prefix + key, (err, value) => {
427 if (err) return rej(err)
428
429 return res(value)
430 })
431 })
432 }
433
434 private exists (key: string) {
435 return new Promise<boolean>((res, rej) => {
436 this.client.exists(this.prefix + key, (err, existsNumber) => {
437 if (err) return rej(err)
438
439 return res(existsNumber === 1)
440 })
441 })
442 }
443
444 static get Instance () {
445 return this.instance || (this.instance = new this())
446 }
447 }
448
449 // ---------------------------------------------------------------------------
450
451 export {
452 Redis
453 }