]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/redis.ts
76b7868e850352b7647c6d0c91c4ac5a1c9c9b73
[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 /* ************ Keys generation ************ */
275
276 generateCachedRouteKey (req: express.Request) {
277 return req.method + '-' + req.originalUrl
278 }
279
280 private generateLocalVideoViewsKeys (videoId?: Number) {
281 return { setKey: `local-video-views-buffer`, videoKey: `local-video-views-buffer-${videoId}` }
282 }
283
284 private generateVideoViewStatsKeys (options: { videoId?: number, hour?: number }) {
285 const hour = exists(options.hour)
286 ? options.hour
287 : new Date().getHours()
288
289 return { setKey: `videos-view-h${hour}`, videoKey: `video-view-${options.videoId}-h${hour}` }
290 }
291
292 private generateResetPasswordKey (userId: number) {
293 return 'reset-password-' + userId
294 }
295
296 private generateVerifyEmailKey (userId: number) {
297 return 'verify-email-' + userId
298 }
299
300 private generateIPViewKey (ip: string, videoUUID: string) {
301 return `views-${videoUUID}-${ip}`
302 }
303
304 private generateIPViewerKey (ip: string, videoUUID: string) {
305 return `viewer-${videoUUID}-${ip}`
306 }
307
308 private generateTrackerBlockIPKey (ip: string) {
309 return `tracker-block-ip-${ip}`
310 }
311
312 private generateContactFormKey (ip: string) {
313 return 'contact-form-' + ip
314 }
315
316 /* ************ Redis helpers ************ */
317
318 private getValue (key: string) {
319 return new Promise<string>((res, rej) => {
320 this.client.get(this.prefix + key, (err, value) => {
321 if (err) return rej(err)
322
323 return res(value)
324 })
325 })
326 }
327
328 private getSet (key: string) {
329 return new Promise<string[]>((res, rej) => {
330 this.client.smembers(this.prefix + key, (err, value) => {
331 if (err) return rej(err)
332
333 return res(value)
334 })
335 })
336 }
337
338 private addToSet (key: string, value: string) {
339 return new Promise<void>((res, rej) => {
340 this.client.sadd(this.prefix + key, value, err => err ? rej(err) : res())
341 })
342 }
343
344 private deleteFromSet (key: string, value: string) {
345 return new Promise<void>((res, rej) => {
346 this.client.srem(this.prefix + key, value, err => err ? rej(err) : res())
347 })
348 }
349
350 private deleteKey (key: string) {
351 return new Promise<void>((res, rej) => {
352 this.client.del(this.prefix + key, err => err ? rej(err) : res())
353 })
354 }
355
356 private deleteFieldInHash (key: string, field: string) {
357 return new Promise<void>((res, rej) => {
358 this.client.hdel(this.prefix + key, field, err => err ? rej(err) : res())
359 })
360 }
361
362 private setValue (key: string, value: string, expirationMilliseconds: number) {
363 return new Promise<void>((res, rej) => {
364 this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => {
365 if (err) return rej(err)
366
367 if (ok !== 'OK') return rej(new Error('Redis set result is not OK.'))
368
369 return res()
370 })
371 })
372 }
373
374 private removeValue (key: string) {
375 return new Promise<void>((res, rej) => {
376 this.client.del(this.prefix + key, err => {
377 if (err) return rej(err)
378
379 return res()
380 })
381 })
382 }
383
384 private setObject (key: string, obj: { [id: string]: string }, expirationMilliseconds: number) {
385 return new Promise<void>((res, rej) => {
386 this.client.hmset(this.prefix + key, obj, (err, ok) => {
387 if (err) return rej(err)
388 if (!ok) return rej(new Error('Redis mset result is not OK.'))
389
390 this.client.pexpire(this.prefix + key, expirationMilliseconds, (err, ok) => {
391 if (err) return rej(err)
392 if (!ok) return rej(new Error('Redis expiration result is not OK.'))
393
394 return res()
395 })
396 })
397 })
398 }
399
400 private getObject (key: string) {
401 return new Promise<{ [id: string]: string }>((res, rej) => {
402 this.client.hgetall(this.prefix + key, (err, value) => {
403 if (err) return rej(err)
404
405 return res(value)
406 })
407 })
408 }
409
410 private setValueInHash (key: string, field: string, value: string) {
411 return new Promise<void>((res, rej) => {
412 this.client.hset(this.prefix + key, field, value, (err) => {
413 if (err) return rej(err)
414
415 return res()
416 })
417 })
418 }
419
420 private increment (key: string) {
421 return new Promise<number>((res, rej) => {
422 this.client.incr(this.prefix + key, (err, value) => {
423 if (err) return rej(err)
424
425 return res(value)
426 })
427 })
428 }
429
430 private exists (key: string) {
431 return new Promise<boolean>((res, rej) => {
432 this.client.exists(this.prefix + key, (err, existsNumber) => {
433 if (err) return rej(err)
434
435 return res(existsNumber === 1)
436 })
437 })
438 }
439
440 static get Instance () {
441 return this.instance || (this.instance = new this())
442 }
443 }
444
445 // ---------------------------------------------------------------------------
446
447 export {
448 Redis
449 }