]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/redis.ts
Move config in its own file
[github/Chocobozzz/PeerTube.git] / server / lib / redis.ts
1 import * as 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 VIDEO_VIEW_LIFETIME,
10 WEBSERVER
11 } from '../initializers'
12 import { CONFIG } from '../initializers/config'
13
14 type CachedRoute = {
15 body: string,
16 contentType?: string
17 statusCode?: string
18 }
19
20 class Redis {
21
22 private static instance: Redis
23 private initialized = false
24 private client: RedisClient
25 private prefix: string
26
27 private constructor () {}
28
29 init () {
30 // Already initialized
31 if (this.initialized === true) return
32 this.initialized = true
33
34 this.client = createClient(Redis.getRedisClient())
35
36 this.client.on('error', err => {
37 logger.error('Error in Redis client.', { err })
38 process.exit(-1)
39 })
40
41 if (CONFIG.REDIS.AUTH) {
42 this.client.auth(CONFIG.REDIS.AUTH)
43 }
44
45 this.prefix = 'redis-' + WEBSERVER.HOST + '-'
46 }
47
48 static getRedisClient () {
49 return Object.assign({},
50 (CONFIG.REDIS.AUTH && CONFIG.REDIS.AUTH != null) ? { password: CONFIG.REDIS.AUTH } : {},
51 (CONFIG.REDIS.DB) ? { db: CONFIG.REDIS.DB } : {},
52 (CONFIG.REDIS.HOSTNAME && CONFIG.REDIS.PORT) ?
53 { host: CONFIG.REDIS.HOSTNAME, port: CONFIG.REDIS.PORT } :
54 { path: CONFIG.REDIS.SOCKET }
55 )
56 }
57
58 /************* Forgot password *************/
59
60 async setResetPasswordVerificationString (userId: number) {
61 const generatedString = await generateRandomString(32)
62
63 await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_RESET_LIFETIME)
64
65 return generatedString
66 }
67
68 async getResetPasswordLink (userId: number) {
69 return this.getValue(this.generateResetPasswordKey(userId))
70 }
71
72 /************* Email verification *************/
73
74 async setVerifyEmailVerificationString (userId: number) {
75 const generatedString = await generateRandomString(32)
76
77 await this.setValue(this.generateVerifyEmailKey(userId), generatedString, USER_EMAIL_VERIFY_LIFETIME)
78
79 return generatedString
80 }
81
82 async getVerifyEmailLink (userId: number) {
83 return this.getValue(this.generateVerifyEmailKey(userId))
84 }
85
86 /************* Contact form per IP *************/
87
88 async setContactFormIp (ip: string) {
89 return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME)
90 }
91
92 async doesContactFormIpExist (ip: string) {
93 return this.exists(this.generateContactFormKey(ip))
94 }
95
96 /************* Views per IP *************/
97
98 setIPVideoView (ip: string, videoUUID: string) {
99 return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME)
100 }
101
102 async doesVideoIPViewExist (ip: string, videoUUID: string) {
103 return this.exists(this.generateViewKey(ip, videoUUID))
104 }
105
106 /************* API cache *************/
107
108 async getCachedRoute (req: express.Request) {
109 const cached = await this.getObject(this.generateCachedRouteKey(req))
110
111 return cached as CachedRoute
112 }
113
114 setCachedRoute (req: express.Request, body: any, lifetime: number, contentType?: string, statusCode?: number) {
115 const cached: CachedRoute = Object.assign({}, {
116 body: body.toString()
117 },
118 (contentType) ? { contentType } : null,
119 (statusCode) ? { statusCode: statusCode.toString() } : null
120 )
121
122 return this.setObject(this.generateCachedRouteKey(req), cached, lifetime)
123 }
124
125 /************* Video views *************/
126
127 addVideoView (videoId: number) {
128 const keyIncr = this.generateVideoViewKey(videoId)
129 const keySet = this.generateVideosViewKey()
130
131 return Promise.all([
132 this.addToSet(keySet, videoId.toString()),
133 this.increment(keyIncr)
134 ])
135 }
136
137 async getVideoViews (videoId: number, hour: number) {
138 const key = this.generateVideoViewKey(videoId, hour)
139
140 const valueString = await this.getValue(key)
141 const valueInt = parseInt(valueString, 10)
142
143 if (isNaN(valueInt)) {
144 logger.error('Cannot get videos views of video %d in hour %d: views number is NaN (%s).', videoId, hour, valueString)
145 return undefined
146 }
147
148 return valueInt
149 }
150
151 async getVideosIdViewed (hour: number) {
152 const key = this.generateVideosViewKey(hour)
153
154 const stringIds = await this.getSet(key)
155 return stringIds.map(s => parseInt(s, 10))
156 }
157
158 deleteVideoViews (videoId: number, hour: number) {
159 const keySet = this.generateVideosViewKey(hour)
160 const keyIncr = this.generateVideoViewKey(videoId, hour)
161
162 return Promise.all([
163 this.deleteFromSet(keySet, videoId.toString()),
164 this.deleteKey(keyIncr)
165 ])
166 }
167
168 /************* Keys generation *************/
169
170 generateCachedRouteKey (req: express.Request) {
171 return req.method + '-' + req.originalUrl
172 }
173
174 private generateVideosViewKey (hour?: number) {
175 if (!hour) hour = new Date().getHours()
176
177 return `videos-view-h${hour}`
178 }
179
180 private generateVideoViewKey (videoId: number, hour?: number) {
181 if (!hour) hour = new Date().getHours()
182
183 return `video-view-${videoId}-h${hour}`
184 }
185
186 private generateResetPasswordKey (userId: number) {
187 return 'reset-password-' + userId
188 }
189
190 private generateVerifyEmailKey (userId: number) {
191 return 'verify-email-' + userId
192 }
193
194 private generateViewKey (ip: string, videoUUID: string) {
195 return `views-${videoUUID}-${ip}`
196 }
197
198 private generateContactFormKey (ip: string) {
199 return 'contact-form-' + ip
200 }
201
202 /************* Redis helpers *************/
203
204 private getValue (key: string) {
205 return new Promise<string>((res, rej) => {
206 this.client.get(this.prefix + key, (err, value) => {
207 if (err) return rej(err)
208
209 return res(value)
210 })
211 })
212 }
213
214 private getSet (key: string) {
215 return new Promise<string[]>((res, rej) => {
216 this.client.smembers(this.prefix + key, (err, value) => {
217 if (err) return rej(err)
218
219 return res(value)
220 })
221 })
222 }
223
224 private addToSet (key: string, value: string) {
225 return new Promise<string[]>((res, rej) => {
226 this.client.sadd(this.prefix + key, value, err => err ? rej(err) : res())
227 })
228 }
229
230 private deleteFromSet (key: string, value: string) {
231 return new Promise<void>((res, rej) => {
232 this.client.srem(this.prefix + key, value, err => err ? rej(err) : res())
233 })
234 }
235
236 private deleteKey (key: string) {
237 return new Promise<void>((res, rej) => {
238 this.client.del(this.prefix + key, err => err ? rej(err) : res())
239 })
240 }
241
242 private deleteFieldInHash (key: string, field: string) {
243 return new Promise<void>((res, rej) => {
244 this.client.hdel(this.prefix + key, field, err => err ? rej(err) : res())
245 })
246 }
247
248 private setValue (key: string, value: string, expirationMilliseconds: number) {
249 return new Promise<void>((res, rej) => {
250 this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => {
251 if (err) return rej(err)
252
253 if (ok !== 'OK') return rej(new Error('Redis set result is not OK.'))
254
255 return res()
256 })
257 })
258 }
259
260 private setObject (key: string, obj: { [ id: string ]: string }, expirationMilliseconds: number) {
261 return new Promise<void>((res, rej) => {
262 this.client.hmset(this.prefix + key, obj, (err, ok) => {
263 if (err) return rej(err)
264 if (!ok) return rej(new Error('Redis mset result is not OK.'))
265
266 this.client.pexpire(this.prefix + key, expirationMilliseconds, (err, ok) => {
267 if (err) return rej(err)
268 if (!ok) return rej(new Error('Redis expiration result is not OK.'))
269
270 return res()
271 })
272 })
273 })
274 }
275
276 private getObject (key: string) {
277 return new Promise<{ [ id: string ]: string }>((res, rej) => {
278 this.client.hgetall(this.prefix + key, (err, value) => {
279 if (err) return rej(err)
280
281 return res(value)
282 })
283 })
284 }
285
286 private setValueInHash (key: string, field: string, value: string) {
287 return new Promise<void>((res, rej) => {
288 this.client.hset(this.prefix + key, field, value, (err) => {
289 if (err) return rej(err)
290
291 return res()
292 })
293 })
294 }
295
296 private increment (key: string) {
297 return new Promise<number>((res, rej) => {
298 this.client.incr(this.prefix + key, (err, value) => {
299 if (err) return rej(err)
300
301 return res(value)
302 })
303 })
304 }
305
306 private exists (key: string) {
307 return new Promise<boolean>((res, rej) => {
308 this.client.exists(this.prefix + key, (err, existsNumber) => {
309 if (err) return rej(err)
310
311 return res(existsNumber === 1)
312 })
313 })
314 }
315
316 static get Instance () {
317 return this.instance || (this.instance = new this())
318 }
319 }
320
321 // ---------------------------------------------------------------------------
322
323 export {
324 Redis
325 }