2
1

redis.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  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 { CONFIG, USER_PASSWORD_RESET_LIFETIME, USER_EMAIL_VERIFY_LIFETIME, VIDEO_VIEW_LIFETIME } from '../initializers'
  6. type CachedRoute = {
  7. body: string,
  8. contentType?: string
  9. statusCode?: string
  10. }
  11. class Redis {
  12. private static instance: Redis
  13. private initialized = false
  14. private client: RedisClient
  15. private prefix: string
  16. private constructor () {}
  17. init () {
  18. // Already initialized
  19. if (this.initialized === true) return
  20. this.initialized = true
  21. this.client = createClient(Redis.getRedisClient())
  22. this.client.on('error', err => {
  23. logger.error('Error in Redis client.', { err })
  24. process.exit(-1)
  25. })
  26. if (CONFIG.REDIS.AUTH) {
  27. this.client.auth(CONFIG.REDIS.AUTH)
  28. }
  29. this.prefix = 'redis-' + CONFIG.WEBSERVER.HOST + '-'
  30. }
  31. static getRedisClient () {
  32. return Object.assign({},
  33. (CONFIG.REDIS.AUTH && CONFIG.REDIS.AUTH != null) ? { password: CONFIG.REDIS.AUTH } : {},
  34. (CONFIG.REDIS.DB) ? { db: CONFIG.REDIS.DB } : {},
  35. (CONFIG.REDIS.HOSTNAME && CONFIG.REDIS.PORT) ?
  36. { host: CONFIG.REDIS.HOSTNAME, port: CONFIG.REDIS.PORT } :
  37. { path: CONFIG.REDIS.SOCKET }
  38. )
  39. }
  40. /************* Forgot password *************/
  41. async setResetPasswordVerificationString (userId: number) {
  42. const generatedString = await generateRandomString(32)
  43. await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_RESET_LIFETIME)
  44. return generatedString
  45. }
  46. async getResetPasswordLink (userId: number) {
  47. return this.getValue(this.generateResetPasswordKey(userId))
  48. }
  49. /************* Email verification *************/
  50. async setVerifyEmailVerificationString (userId: number) {
  51. const generatedString = await generateRandomString(32)
  52. await this.setValue(this.generateVerifyEmailKey(userId), generatedString, USER_EMAIL_VERIFY_LIFETIME)
  53. return generatedString
  54. }
  55. async getVerifyEmailLink (userId: number) {
  56. return this.getValue(this.generateVerifyEmailKey(userId))
  57. }
  58. /************* Views per IP *************/
  59. setIPVideoView (ip: string, videoUUID: string) {
  60. return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME)
  61. }
  62. async isVideoIPViewExists (ip: string, videoUUID: string) {
  63. return this.exists(this.generateViewKey(ip, videoUUID))
  64. }
  65. /************* API cache *************/
  66. async getCachedRoute (req: express.Request) {
  67. const cached = await this.getObject(this.generateCachedRouteKey(req))
  68. return cached as CachedRoute
  69. }
  70. setCachedRoute (req: express.Request, body: any, lifetime: number, contentType?: string, statusCode?: number) {
  71. const cached: CachedRoute = Object.assign({}, {
  72. body: body.toString()
  73. },
  74. (contentType) ? { contentType } : null,
  75. (statusCode) ? { statusCode: statusCode.toString() } : null
  76. )
  77. return this.setObject(this.generateCachedRouteKey(req), cached, lifetime)
  78. }
  79. /************* Video views *************/
  80. addVideoView (videoId: number) {
  81. const keyIncr = this.generateVideoViewKey(videoId)
  82. const keySet = this.generateVideosViewKey()
  83. return Promise.all([
  84. this.addToSet(keySet, videoId.toString()),
  85. this.increment(keyIncr)
  86. ])
  87. }
  88. async getVideoViews (videoId: number, hour: number) {
  89. const key = this.generateVideoViewKey(videoId, hour)
  90. const valueString = await this.getValue(key)
  91. return parseInt(valueString, 10)
  92. }
  93. async getVideosIdViewed (hour: number) {
  94. const key = this.generateVideosViewKey(hour)
  95. const stringIds = await this.getSet(key)
  96. return stringIds.map(s => parseInt(s, 10))
  97. }
  98. deleteVideoViews (videoId: number, hour: number) {
  99. const keySet = this.generateVideosViewKey(hour)
  100. const keyIncr = this.generateVideoViewKey(videoId, hour)
  101. return Promise.all([
  102. this.deleteFromSet(keySet, videoId.toString()),
  103. this.deleteKey(keyIncr)
  104. ])
  105. }
  106. /************* Keys generation *************/
  107. generateCachedRouteKey (req: express.Request) {
  108. return req.method + '-' + req.originalUrl
  109. }
  110. private generateVideosViewKey (hour?: number) {
  111. if (!hour) hour = new Date().getHours()
  112. return `videos-view-h${hour}`
  113. }
  114. private generateVideoViewKey (videoId: number, hour?: number) {
  115. if (!hour) hour = new Date().getHours()
  116. return `video-view-${videoId}-h${hour}`
  117. }
  118. private generateResetPasswordKey (userId: number) {
  119. return 'reset-password-' + userId
  120. }
  121. private generateVerifyEmailKey (userId: number) {
  122. return 'verify-email-' + userId
  123. }
  124. private generateViewKey (ip: string, videoUUID: string) {
  125. return videoUUID + '-' + ip
  126. }
  127. /************* Redis helpers *************/
  128. private getValue (key: string) {
  129. return new Promise<string>((res, rej) => {
  130. this.client.get(this.prefix + key, (err, value) => {
  131. if (err) return rej(err)
  132. return res(value)
  133. })
  134. })
  135. }
  136. private getSet (key: string) {
  137. return new Promise<string[]>((res, rej) => {
  138. this.client.smembers(this.prefix + key, (err, value) => {
  139. if (err) return rej(err)
  140. return res(value)
  141. })
  142. })
  143. }
  144. private addToSet (key: string, value: string) {
  145. return new Promise<string[]>((res, rej) => {
  146. this.client.sadd(this.prefix + key, value, err => err ? rej(err) : res())
  147. })
  148. }
  149. private deleteFromSet (key: string, value: string) {
  150. return new Promise<void>((res, rej) => {
  151. this.client.srem(this.prefix + key, value, err => err ? rej(err) : res())
  152. })
  153. }
  154. private deleteKey (key: string) {
  155. return new Promise<void>((res, rej) => {
  156. this.client.del(this.prefix + key, err => err ? rej(err) : res())
  157. })
  158. }
  159. private deleteFieldInHash (key: string, field: string) {
  160. return new Promise<void>((res, rej) => {
  161. this.client.hdel(this.prefix + key, field, err => err ? rej(err) : res())
  162. })
  163. }
  164. private setValue (key: string, value: string, expirationMilliseconds: number) {
  165. return new Promise<void>((res, rej) => {
  166. this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => {
  167. if (err) return rej(err)
  168. if (ok !== 'OK') return rej(new Error('Redis set result is not OK.'))
  169. return res()
  170. })
  171. })
  172. }
  173. private setObject (key: string, obj: { [ id: string ]: string }, expirationMilliseconds: number) {
  174. return new Promise<void>((res, rej) => {
  175. this.client.hmset(this.prefix + key, obj, (err, ok) => {
  176. if (err) return rej(err)
  177. if (!ok) return rej(new Error('Redis mset result is not OK.'))
  178. this.client.pexpire(this.prefix + key, expirationMilliseconds, (err, ok) => {
  179. if (err) return rej(err)
  180. if (!ok) return rej(new Error('Redis expiration result is not OK.'))
  181. return res()
  182. })
  183. })
  184. })
  185. }
  186. private getObject (key: string) {
  187. return new Promise<{ [ id: string ]: string }>((res, rej) => {
  188. this.client.hgetall(this.prefix + key, (err, value) => {
  189. if (err) return rej(err)
  190. return res(value)
  191. })
  192. })
  193. }
  194. private setValueInHash (key: string, field: string, value: string) {
  195. return new Promise<void>((res, rej) => {
  196. this.client.hset(this.prefix + key, field, value, (err) => {
  197. if (err) return rej(err)
  198. return res()
  199. })
  200. })
  201. }
  202. private increment (key: string) {
  203. return new Promise<number>((res, rej) => {
  204. this.client.incr(this.prefix + key, (err, value) => {
  205. if (err) return rej(err)
  206. return res(value)
  207. })
  208. })
  209. }
  210. private exists (key: string) {
  211. return new Promise<boolean>((res, rej) => {
  212. this.client.exists(this.prefix + key, (err, existsNumber) => {
  213. if (err) return rej(err)
  214. return res(existsNumber === 1)
  215. })
  216. })
  217. }
  218. static get Instance () {
  219. return this.instance || (this.instance = new this())
  220. }
  221. }
  222. // ---------------------------------------------------------------------------
  223. export {
  224. Redis
  225. }