]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/redis.ts
Remove resumable cache after upload success
[github/Chocobozzz/PeerTube.git] / server / lib / redis.ts
CommitLineData
41fb13c3 1import express from 'express'
ecb4e35f
C
2import { createClient, RedisClient } from 'redis'
3import { logger } from '../helpers/logger'
4import { generateRandomString } from '../helpers/utils'
a4101923 5import {
a4101923
C
6 CONTACT_FORM_LIFETIME,
7 USER_EMAIL_VERIFY_LIFETIME,
8 USER_PASSWORD_RESET_LIFETIME,
45f1bd72 9 USER_PASSWORD_CREATE_LIFETIME,
e4bf7856 10 VIEW_LIFETIME,
db48de85 11 WEBSERVER,
276250f0
RK
12 TRACKER_RATE_LIMITS,
13 RESUMABLE_UPLOAD_SESSION_LIFETIME
74dc3bca 14} from '../initializers/constants'
6dd9de95 15import { CONFIG } from '../initializers/config'
51353d9a 16import { exists } from '@server/helpers/custom-validators/misc'
4195cd2b
C
17
18type CachedRoute = {
a1587156 19 body: string
2cebd797
C
20 contentType?: string
21 statusCode?: string
4195cd2b 22}
ecb4e35f
C
23
24class Redis {
25
26 private static instance: Redis
27 private initialized = false
28 private client: RedisClient
29 private prefix: string
30
a1587156
C
31 private constructor () {
32 }
ecb4e35f
C
33
34 init () {
35 // Already initialized
36 if (this.initialized === true) return
37 this.initialized = true
38
47f6409b 39 this.client = createClient(Redis.getRedisClientOptions())
ecb4e35f
C
40
41 this.client.on('error', err => {
d5b7d911 42 logger.error('Error in Redis client.', { err })
ecb4e35f
C
43 process.exit(-1)
44 })
45
46 if (CONFIG.REDIS.AUTH) {
47 this.client.auth(CONFIG.REDIS.AUTH)
48 }
49
6dd9de95 50 this.prefix = 'redis-' + WEBSERVER.HOST + '-'
ecb4e35f
C
51 }
52
47f6409b 53 static getRedisClientOptions () {
19f7b248
RK
54 return Object.assign({},
55 (CONFIG.REDIS.AUTH && CONFIG.REDIS.AUTH != null) ? { password: CONFIG.REDIS.AUTH } : {},
56 (CONFIG.REDIS.DB) ? { db: CONFIG.REDIS.DB } : {},
a1587156
C
57 (CONFIG.REDIS.HOSTNAME && CONFIG.REDIS.PORT)
58 ? { host: CONFIG.REDIS.HOSTNAME, port: CONFIG.REDIS.PORT }
59 : { path: CONFIG.REDIS.SOCKET }
19f7b248
RK
60 )
61 }
62
47f6409b
C
63 getClient () {
64 return this.client
65 }
66
67 getPrefix () {
68 return this.prefix
69 }
70
a1587156 71 /* ************ Forgot password ************ */
6e46de09 72
ecb4e35f
C
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
45f1bd72
JL
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
ecb4e35f
C
86 return generatedString
87 }
88
e9c5f123
C
89 async removePasswordVerificationString (userId: number) {
90 return this.removeValue(this.generateResetPasswordKey(userId))
91 }
92
ecb4e35f
C
93 async getResetPasswordLink (userId: number) {
94 return this.getValue(this.generateResetPasswordKey(userId))
95 }
96
a1587156 97 /* ************ Email verification ************ */
6e46de09 98
d9eaee39
JM
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
a1587156 111 /* ************ Contact form per IP ************ */
a4101923
C
112
113 async setContactFormIp (ip: string) {
114 return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME)
115 }
116
0f6acda1 117 async doesContactFormIpExist (ip: string) {
a4101923
C
118 return this.exists(this.generateContactFormKey(ip))
119 }
120
a1587156 121 /* ************ Views per IP ************ */
6e46de09 122
51353d9a
C
123 setIPVideoView (ip: string, videoUUID: string) {
124 return this.setValue(this.generateIPViewKey(ip, videoUUID), '1', VIEW_LIFETIME.VIEW)
125 }
e4bf7856 126
51353d9a
C
127 setIPVideoViewer (ip: string, videoUUID: string) {
128 return this.setValue(this.generateIPViewerKey(ip, videoUUID), '1', VIEW_LIFETIME.VIEWER)
b5c0e955
C
129 }
130
0f6acda1 131 async doesVideoIPViewExist (ip: string, videoUUID: string) {
51353d9a
C
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))
b5c0e955
C
137 }
138
db48de85
C
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
a1587156 149 /* ************ API cache ************ */
6e46de09 150
4195cd2b 151 async getCachedRoute (req: express.Request) {
6e46de09 152 const cached = await this.getObject(this.generateCachedRouteKey(req))
4195cd2b
C
153
154 return cached as CachedRoute
155 }
156
fd4484f1 157 setCachedRoute (req: express.Request, body: any, lifetime: number, contentType?: string, statusCode?: number) {
a1587156
C
158 const cached: CachedRoute = Object.assign(
159 {},
160 { body: body.toString() },
161 (contentType) ? { contentType } : null,
162 (statusCode) ? { statusCode: statusCode.toString() } : null
c1e791ba 163 )
4195cd2b 164
6e46de09 165 return this.setObject(this.generateCachedRouteKey(req), cached, lifetime)
4195cd2b
C
166 }
167
51353d9a 168 /* ************ Video views stats ************ */
6e46de09 169
51353d9a
C
170 addVideoViewStats (videoId: number) {
171 const { videoKey, setKey } = this.generateVideoViewStatsKeys({ videoId })
6b616860
C
172
173 return Promise.all([
51353d9a
C
174 this.addToSet(setKey, videoId.toString()),
175 this.increment(videoKey)
6b616860
C
176 ])
177 }
178
51353d9a
C
179 async getVideoViewsStats (videoId: number, hour: number) {
180 const { videoKey } = this.generateVideoViewStatsKeys({ videoId, hour })
6b616860 181
51353d9a 182 const valueString = await this.getValue(videoKey)
6040f87d
C
183 const valueInt = parseInt(valueString, 10)
184
185 if (isNaN(valueInt)) {
51353d9a 186 logger.error('Cannot get videos views stats of video %d in hour %d: views number is NaN (%s).', videoId, hour, valueString)
6040f87d
C
187 return undefined
188 }
189
190 return valueInt
6b616860
C
191 }
192
51353d9a
C
193 async listVideosViewedForStats (hour: number) {
194 const { setKey } = this.generateVideoViewStatsKeys({ hour })
6b616860 195
51353d9a 196 const stringIds = await this.getSet(setKey)
6b616860
C
197 return stringIds.map(s => parseInt(s, 10))
198 }
199
51353d9a
C
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)
6b616860
C
213
214 return Promise.all([
51353d9a
C
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)
6b616860
C
247 ])
248 }
249
276250f0
RK
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
020d3d3d
C
274 deleteUploadSession (uploadId: string) {
275 return this.deleteKey('resumable-upload-' + uploadId)
276 }
277
a1587156 278 /* ************ Keys generation ************ */
6e46de09
C
279
280 generateCachedRouteKey (req: express.Request) {
281 return req.method + '-' + req.originalUrl
282 }
283
51353d9a
C
284 private generateLocalVideoViewsKeys (videoId?: Number) {
285 return { setKey: `local-video-views-buffer`, videoKey: `local-video-views-buffer-${videoId}` }
6b616860
C
286 }
287
51353d9a
C
288 private generateVideoViewStatsKeys (options: { videoId?: number, hour?: number }) {
289 const hour = exists(options.hour)
290 ? options.hour
291 : new Date().getHours()
6b616860 292
51353d9a 293 return { setKey: `videos-view-h${hour}`, videoKey: `video-view-${options.videoId}-h${hour}` }
6b616860
C
294 }
295
6e46de09 296 private generateResetPasswordKey (userId: number) {
b40f0575
C
297 return 'reset-password-' + userId
298 }
299
6e46de09 300 private generateVerifyEmailKey (userId: number) {
d9eaee39
JM
301 return 'verify-email-' + userId
302 }
303
51353d9a 304 private generateIPViewKey (ip: string, videoUUID: string) {
a4101923
C
305 return `views-${videoUUID}-${ip}`
306 }
307
51353d9a
C
308 private generateIPViewerKey (ip: string, videoUUID: string) {
309 return `viewer-${videoUUID}-${ip}`
310 }
311
db48de85
C
312 private generateTrackerBlockIPKey (ip: string) {
313 return `tracker-block-ip-${ip}`
314 }
315
a4101923
C
316 private generateContactFormKey (ip: string) {
317 return 'contact-form-' + ip
b40f0575
C
318 }
319
a1587156 320 /* ************ Redis helpers ************ */
b40f0575 321
ecb4e35f
C
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
6b616860
C
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) {
ba5a8d89 343 return new Promise<void>((res, rej) => {
6b616860
C
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
6e46de09
C
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
ecb4e35f
C
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
4195cd2b 371 if (ok !== 'OK') return rej(new Error('Redis set result is not OK.'))
ecb4e35f
C
372
373 return res()
374 })
375 })
376 }
377
e9c5f123
C
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
a1587156 388 private setObject (key: string, obj: { [id: string]: string }, expirationMilliseconds: number) {
4195cd2b
C
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) {
a1587156 405 return new Promise<{ [id: string]: string }>((res, rej) => {
4195cd2b
C
406 this.client.hgetall(this.prefix + key, (err, value) => {
407 if (err) return rej(err)
408
409 return res(value)
410 })
411 })
412 }
413
6e46de09
C
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
6b616860
C
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
b5c0e955
C
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
ecb4e35f
C
444 static get Instance () {
445 return this.instance || (this.instance = new this())
446 }
447}
448
449// ---------------------------------------------------------------------------
450
451export {
452 Redis
453}