aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-07-22 11:15:17 +0200
committerChocobozzz <me@florianbigard.com>2021-07-22 11:48:24 +0200
commit20bafcb61bee2a9a10a500908850c9a7d5e3c8c5 (patch)
tree18d0e8eb693b0fce88b21b282ea6f28836763fe6
parent13e13377918b65c30b9334920fef4b43e70b964e (diff)
downloadPeerTube-20bafcb61bee2a9a10a500908850c9a7d5e3c8c5.tar.gz
PeerTube-20bafcb61bee2a9a10a500908850c9a7d5e3c8c5.tar.zst
PeerTube-20bafcb61bee2a9a10a500908850c9a7d5e3c8c5.zip
Move apicache in peertube
Allow us to upgrade to node 16
-rw-r--r--package.json1
-rw-r--r--server/controllers/activitypub/client.ts4
-rw-r--r--server/controllers/api/server/stats.ts4
-rw-r--r--server/controllers/bots.ts4
-rw-r--r--server/controllers/feeds.ts24
-rw-r--r--server/controllers/static.ts12
-rw-r--r--server/middlewares/cache.ts28
-rw-r--r--server/middlewares/cache/cache.ts32
-rw-r--r--server/middlewares/cache/index.ts1
-rw-r--r--server/middlewares/cache/shared/api-cache.ts269
-rw-r--r--server/middlewares/cache/shared/index.ts1
-rw-r--r--server/middlewares/index.ts1
-rw-r--r--server/tests/feeds/feeds.ts40
-rw-r--r--server/typings/express/index.d.ts11
-rw-r--r--yarn.lock5
15 files changed, 372 insertions, 65 deletions
diff --git a/package.json b/package.json
index 7611ac9ab..bd18d4c64 100644
--- a/package.json
+++ b/package.json
@@ -74,7 +74,6 @@
74 }, 74 },
75 "dependencies": { 75 "dependencies": {
76 "@uploadx/core": "^4.4.0", 76 "@uploadx/core": "^4.4.0",
77 "apicache": "1.6.2",
78 "async": "^3.0.1", 77 "async": "^3.0.1",
79 "async-lru": "^1.1.1", 78 "async-lru": "^1.1.1",
80 "bcrypt": "5.0.1", 79 "bcrypt": "5.0.1",
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index d7de1b9bd..bef4bc068 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -24,7 +24,7 @@ import {
24 videosCustomGetValidator, 24 videosCustomGetValidator,
25 videosShareValidator 25 videosShareValidator
26} from '../../middlewares' 26} from '../../middlewares'
27import { cacheRoute } from '../../middlewares/cache' 27import { cacheRoute } from '../../middlewares/cache/cache'
28import { getAccountVideoRateValidatorFactory, videoCommentGetValidator } from '../../middlewares/validators' 28import { getAccountVideoRateValidatorFactory, videoCommentGetValidator } from '../../middlewares/validators'
29import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy' 29import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy'
30import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists' 30import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists'
@@ -77,7 +77,7 @@ activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
77activityPubClientRouter.get( 77activityPubClientRouter.get(
78 [ '/videos/watch/:id', '/w/:id' ], 78 [ '/videos/watch/:id', '/w/:id' ],
79 executeIfActivityPub, 79 executeIfActivityPub,
80 asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS)), 80 cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
81 asyncMiddleware(videosCustomGetValidator('all')), 81 asyncMiddleware(videosCustomGetValidator('all')),
82 asyncMiddleware(videoController) 82 asyncMiddleware(videoController)
83) 83)
diff --git a/server/controllers/api/server/stats.ts b/server/controllers/api/server/stats.ts
index 3aea12450..397702548 100644
--- a/server/controllers/api/server/stats.ts
+++ b/server/controllers/api/server/stats.ts
@@ -2,12 +2,12 @@ import * as express from 'express'
2import { StatsManager } from '@server/lib/stat-manager' 2import { StatsManager } from '@server/lib/stat-manager'
3import { ROUTE_CACHE_LIFETIME } from '../../../initializers/constants' 3import { ROUTE_CACHE_LIFETIME } from '../../../initializers/constants'
4import { asyncMiddleware } from '../../../middlewares' 4import { asyncMiddleware } from '../../../middlewares'
5import { cacheRoute } from '../../../middlewares/cache' 5import { cacheRoute } from '../../../middlewares/cache/cache'
6 6
7const statsRouter = express.Router() 7const statsRouter = express.Router()
8 8
9statsRouter.get('/stats', 9statsRouter.get('/stats',
10 asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.STATS)), 10 cacheRoute(ROUTE_CACHE_LIFETIME.STATS),
11 asyncMiddleware(getStats) 11 asyncMiddleware(getStats)
12) 12)
13 13
diff --git a/server/controllers/bots.ts b/server/controllers/bots.ts
index 9e92063d4..93aa0cf30 100644
--- a/server/controllers/bots.ts
+++ b/server/controllers/bots.ts
@@ -5,7 +5,7 @@ import { SitemapStream, streamToPromise } from 'sitemap'
5import { VideoModel } from '../models/video/video' 5import { VideoModel } from '../models/video/video'
6import { VideoChannelModel } from '../models/video/video-channel' 6import { VideoChannelModel } from '../models/video/video-channel'
7import { AccountModel } from '../models/account/account' 7import { AccountModel } from '../models/account/account'
8import { cacheRoute } from '../middlewares/cache' 8import { cacheRoute } from '../middlewares/cache/cache'
9import { buildNSFWFilter } from '../helpers/express-utils' 9import { buildNSFWFilter } from '../helpers/express-utils'
10import { truncate } from 'lodash' 10import { truncate } from 'lodash'
11 11
@@ -14,7 +14,7 @@ const botsRouter = express.Router()
14// Special route that add OpenGraph and oEmbed tags 14// Special route that add OpenGraph and oEmbed tags
15// Do not use a template engine for a so little thing 15// Do not use a template engine for a so little thing
16botsRouter.use('/sitemap.xml', 16botsRouter.use('/sitemap.xml',
17 asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.SITEMAP)), 17 cacheRoute(ROUTE_CACHE_LIFETIME.SITEMAP),
18 asyncMiddleware(getSitemap) 18 asyncMiddleware(getSitemap)
19) 19)
20 20
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index 435b12193..cdc6bfb8b 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -16,20 +16,20 @@ import {
16 videosSortValidator, 16 videosSortValidator,
17 videoSubscriptionFeedsValidator 17 videoSubscriptionFeedsValidator
18} from '../middlewares' 18} from '../middlewares'
19import { cacheRoute } from '../middlewares/cache' 19import { cacheRouteFactory } from '../middlewares/cache/cache'
20import { VideoModel } from '../models/video/video' 20import { VideoModel } from '../models/video/video'
21import { VideoCommentModel } from '../models/video/video-comment' 21import { VideoCommentModel } from '../models/video/video-comment'
22 22
23const feedsRouter = express.Router() 23const feedsRouter = express.Router()
24 24
25const cacheRoute = cacheRouteFactory({
26 headerBlacklist: [ 'Content-Type' ]
27})
28
25feedsRouter.get('/feeds/video-comments.:format', 29feedsRouter.get('/feeds/video-comments.:format',
26 feedsFormatValidator, 30 feedsFormatValidator,
27 setFeedFormatContentType, 31 setFeedFormatContentType,
28 asyncMiddleware(cacheRoute({ 32 cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS),
29 headerBlacklist: [
30 'Content-Type'
31 ]
32 })(ROUTE_CACHE_LIFETIME.FEEDS)),
33 asyncMiddleware(videoFeedsValidator), 33 asyncMiddleware(videoFeedsValidator),
34 asyncMiddleware(videoCommentsFeedsValidator), 34 asyncMiddleware(videoCommentsFeedsValidator),
35 asyncMiddleware(generateVideoCommentsFeed) 35 asyncMiddleware(generateVideoCommentsFeed)
@@ -40,11 +40,7 @@ feedsRouter.get('/feeds/videos.:format',
40 setDefaultVideosSort, 40 setDefaultVideosSort,
41 feedsFormatValidator, 41 feedsFormatValidator,
42 setFeedFormatContentType, 42 setFeedFormatContentType,
43 asyncMiddleware(cacheRoute({ 43 cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS),
44 headerBlacklist: [
45 'Content-Type'
46 ]
47 })(ROUTE_CACHE_LIFETIME.FEEDS)),
48 commonVideosFiltersValidator, 44 commonVideosFiltersValidator,
49 asyncMiddleware(videoFeedsValidator), 45 asyncMiddleware(videoFeedsValidator),
50 asyncMiddleware(generateVideoFeed) 46 asyncMiddleware(generateVideoFeed)
@@ -55,11 +51,7 @@ feedsRouter.get('/feeds/subscriptions.:format',
55 setDefaultVideosSort, 51 setDefaultVideosSort,
56 feedsFormatValidator, 52 feedsFormatValidator,
57 setFeedFormatContentType, 53 setFeedFormatContentType,
58 asyncMiddleware(cacheRoute({ 54 cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS),
59 headerBlacklist: [
60 'Content-Type'
61 ]
62 })(ROUTE_CACHE_LIFETIME.FEEDS)),
63 commonVideosFiltersValidator, 55 commonVideosFiltersValidator,
64 asyncMiddleware(videoSubscriptionFeedsValidator), 56 asyncMiddleware(videoSubscriptionFeedsValidator),
65 asyncMiddleware(generateVideoFeedForSubscriptions) 57 asyncMiddleware(generateVideoFeedForSubscriptions)
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index 5900eaff3..912d7e36c 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -19,7 +19,7 @@ import {
19} from '../initializers/constants' 19} from '../initializers/constants'
20import { getThemeOrDefault } from '../lib/plugins/theme-utils' 20import { getThemeOrDefault } from '../lib/plugins/theme-utils'
21import { asyncMiddleware } from '../middlewares' 21import { asyncMiddleware } from '../middlewares'
22import { cacheRoute } from '../middlewares/cache' 22import { cacheRoute } from '../middlewares/cache/cache'
23import { UserModel } from '../models/user/user' 23import { UserModel } from '../models/user/user'
24import { VideoModel } from '../models/video/video' 24import { VideoModel } from '../models/video/video'
25import { VideoCommentModel } from '../models/video/video-comment' 25import { VideoCommentModel } from '../models/video/video-comment'
@@ -66,7 +66,7 @@ staticRouter.use(
66 66
67// robots.txt service 67// robots.txt service
68staticRouter.get('/robots.txt', 68staticRouter.get('/robots.txt',
69 asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.ROBOTS)), 69 cacheRoute(ROUTE_CACHE_LIFETIME.ROBOTS),
70 (_, res: express.Response) => { 70 (_, res: express.Response) => {
71 res.type('text/plain') 71 res.type('text/plain')
72 return res.send(CONFIG.INSTANCE.ROBOTS) 72 return res.send(CONFIG.INSTANCE.ROBOTS)
@@ -86,7 +86,7 @@ staticRouter.get('/security.txt',
86) 86)
87 87
88staticRouter.get('/.well-known/security.txt', 88staticRouter.get('/.well-known/security.txt',
89 asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.SECURITYTXT)), 89 cacheRoute(ROUTE_CACHE_LIFETIME.SECURITYTXT),
90 (_, res: express.Response) => { 90 (_, res: express.Response) => {
91 res.type('text/plain') 91 res.type('text/plain')
92 return res.send(CONFIG.INSTANCE.SECURITYTXT + CONFIG.INSTANCE.SECURITYTXT_CONTACT) 92 return res.send(CONFIG.INSTANCE.SECURITYTXT + CONFIG.INSTANCE.SECURITYTXT_CONTACT)
@@ -95,7 +95,7 @@ staticRouter.get('/.well-known/security.txt',
95 95
96// nodeinfo service 96// nodeinfo service
97staticRouter.use('/.well-known/nodeinfo', 97staticRouter.use('/.well-known/nodeinfo',
98 asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.NODEINFO)), 98 cacheRoute(ROUTE_CACHE_LIFETIME.NODEINFO),
99 (_, res: express.Response) => { 99 (_, res: express.Response) => {
100 return res.json({ 100 return res.json({
101 links: [ 101 links: [
@@ -108,13 +108,13 @@ staticRouter.use('/.well-known/nodeinfo',
108 } 108 }
109) 109)
110staticRouter.use('/nodeinfo/:version.json', 110staticRouter.use('/nodeinfo/:version.json',
111 asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.NODEINFO)), 111 cacheRoute(ROUTE_CACHE_LIFETIME.NODEINFO),
112 asyncMiddleware(generateNodeinfo) 112 asyncMiddleware(generateNodeinfo)
113) 113)
114 114
115// dnt-policy.txt service (see https://www.eff.org/dnt-policy) 115// dnt-policy.txt service (see https://www.eff.org/dnt-policy)
116staticRouter.use('/.well-known/dnt-policy.txt', 116staticRouter.use('/.well-known/dnt-policy.txt',
117 asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.DNT_POLICY)), 117 cacheRoute(ROUTE_CACHE_LIFETIME.DNT_POLICY),
118 (_, res: express.Response) => { 118 (_, res: express.Response) => {
119 res.type('text/plain') 119 res.type('text/plain')
120 120
diff --git a/server/middlewares/cache.ts b/server/middlewares/cache.ts
deleted file mode 100644
index e508b22a6..000000000
--- a/server/middlewares/cache.ts
+++ /dev/null
@@ -1,28 +0,0 @@
1import * as apicache from 'apicache'
2import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
3import { Redis } from '../lib/redis'
4
5// Ensure Redis is initialized
6Redis.Instance.init()
7
8const defaultOptions = {
9 redisClient: Redis.Instance.getClient(),
10 appendKey: () => Redis.Instance.getPrefix(),
11 statusCodes: {
12 exclude: [
13 HttpStatusCode.FORBIDDEN_403,
14 HttpStatusCode.NOT_FOUND_404
15 ]
16 }
17}
18
19const cacheRoute = (extraOptions = {}) => apicache.options({
20 ...defaultOptions,
21 ...extraOptions
22}).middleware
23
24// ---------------------------------------------------------------------------
25
26export {
27 cacheRoute
28}
diff --git a/server/middlewares/cache/cache.ts b/server/middlewares/cache/cache.ts
new file mode 100644
index 000000000..48162a0ae
--- /dev/null
+++ b/server/middlewares/cache/cache.ts
@@ -0,0 +1,32 @@
1import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
2import { Redis } from '../../lib/redis'
3import { ApiCache, APICacheOptions } from './shared'
4
5// Ensure Redis is initialized
6Redis.Instance.init()
7
8const defaultOptions: APICacheOptions = {
9 excludeStatus: [
10 HttpStatusCode.FORBIDDEN_403,
11 HttpStatusCode.NOT_FOUND_404
12 ]
13}
14
15function cacheRoute (duration: string) {
16 const instance = new ApiCache(defaultOptions)
17
18 return instance.buildMiddleware(duration)
19}
20
21function cacheRouteFactory (options: APICacheOptions) {
22 const instance = new ApiCache({ ...defaultOptions, ...options })
23
24 return instance.buildMiddleware.bind(instance)
25}
26
27// ---------------------------------------------------------------------------
28
29export {
30 cacheRoute,
31 cacheRouteFactory
32}
diff --git a/server/middlewares/cache/index.ts b/server/middlewares/cache/index.ts
new file mode 100644
index 000000000..79b512828
--- /dev/null
+++ b/server/middlewares/cache/index.ts
@@ -0,0 +1 @@
export * from './cache'
diff --git a/server/middlewares/cache/shared/api-cache.ts b/server/middlewares/cache/shared/api-cache.ts
new file mode 100644
index 000000000..f9f7b1b67
--- /dev/null
+++ b/server/middlewares/cache/shared/api-cache.ts
@@ -0,0 +1,269 @@
1// Thanks: https://github.com/kwhitley/apicache
2// We duplicated the library because it is unmaintened and prevent us to upgrade to recent NodeJS versions
3
4import * as express from 'express'
5import { OutgoingHttpHeaders } from 'http'
6import { isTestInstance, parseDurationToMs } from '@server/helpers/core-utils'
7import { logger } from '@server/helpers/logger'
8import { Redis } from '@server/lib/redis'
9import { HttpStatusCode } from '@shared/models'
10
11export interface APICacheOptions {
12 headerBlacklist?: string[]
13 excludeStatus?: HttpStatusCode[]
14}
15
16interface CacheObject {
17 status: number
18 headers: OutgoingHttpHeaders
19 data: any
20 encoding: BufferEncoding
21 timestamp: number
22}
23
24export class ApiCache {
25
26 private readonly options: APICacheOptions
27 private readonly timers: { [ id: string ]: NodeJS.Timeout } = {}
28
29 private index: { all: string[] } = { all: [] }
30
31 constructor (options: APICacheOptions) {
32 this.options = {
33 headerBlacklist: [],
34 excludeStatus: [],
35
36 ...options
37 }
38 }
39
40 buildMiddleware (strDuration: string) {
41 const duration = parseDurationToMs(strDuration)
42
43 return (req: express.Request, res: express.Response, next: express.NextFunction) => {
44 const key = Redis.Instance.getPrefix() + 'api-cache-' + req.originalUrl
45 const redis = Redis.Instance.getClient()
46
47 if (!redis.connected) return this.makeResponseCacheable(res, next, key, duration)
48
49 try {
50 redis.hgetall(key, (err, obj) => {
51 if (!err && obj && obj.response) {
52 return this.sendCachedResponse(req, res, JSON.parse(obj.response), duration)
53 }
54
55 return this.makeResponseCacheable(res, next, key, duration)
56 })
57 } catch (err) {
58 return this.makeResponseCacheable(res, next, key, duration)
59 }
60 }
61 }
62
63 private shouldCacheResponse (response: express.Response) {
64 if (!response) return false
65 if (this.options.excludeStatus.includes(response.statusCode)) return false
66
67 return true
68 }
69
70 private addIndexEntries (key: string) {
71 this.index.all.unshift(key)
72 }
73
74 private filterBlacklistedHeaders (headers: OutgoingHttpHeaders) {
75 return Object.keys(headers)
76 .filter(key => !this.options.headerBlacklist.includes(key))
77 .reduce((acc, header) => {
78 acc[header] = headers[header]
79
80 return acc
81 }, {})
82 }
83
84 private createCacheObject (status: number, headers: OutgoingHttpHeaders, data: any, encoding: BufferEncoding) {
85 return {
86 status,
87 headers: this.filterBlacklistedHeaders(headers),
88 data,
89 encoding,
90
91 // Seconds since epoch, used to properly decrement max-age headers in cached responses.
92 timestamp: new Date().getTime() / 1000
93 } as CacheObject
94 }
95
96 private cacheResponse (key: string, value: object, duration: number) {
97 const redis = Redis.Instance.getClient()
98
99 if (redis.connected) {
100 try {
101 redis.hset(key, 'response', JSON.stringify(value))
102 redis.hset(key, 'duration', duration + '')
103 redis.expire(key, duration / 1000)
104 } catch (err) {
105 logger.error('Cannot set cache in redis.', { err })
106 }
107 }
108
109 // add automatic cache clearing from duration, includes max limit on setTimeout
110 this.timers[key] = setTimeout(() => this.clear(key), Math.min(duration, 2147483647))
111 }
112
113 private accumulateContent (res: express.Response, content: any) {
114 if (!content) return
115
116 if (typeof content === 'string') {
117 res.locals.apicache.content = (res.locals.apicache.content || '') + content
118 return
119 }
120
121 if (Buffer.isBuffer(content)) {
122 let oldContent = res.locals.apicache.content
123
124 if (typeof oldContent === 'string') {
125 oldContent = Buffer.from(oldContent)
126 }
127
128 if (!oldContent) {
129 oldContent = Buffer.alloc(0)
130 }
131
132 res.locals.apicache.content = Buffer.concat(
133 [ oldContent, content ],
134 oldContent.length + content.length
135 )
136
137 return
138 }
139
140 res.locals.apicache.content = content
141 }
142
143 private makeResponseCacheable (res: express.Response, next: express.NextFunction, key: string, duration: number) {
144 const self = this
145
146 res.locals.apicache = {
147 write: res.write,
148 writeHead: res.writeHead,
149 end: res.end,
150 cacheable: true,
151 content: undefined,
152 headers: {}
153 }
154
155 // Patch express
156 res.writeHead = function () {
157 if (self.shouldCacheResponse(res)) {
158 res.setHeader('cache-control', 'max-age=' + (duration / 1000).toFixed(0))
159 } else {
160 res.setHeader('cache-control', 'no-cache, no-store, must-revalidate')
161 }
162
163 res.locals.apicache.headers = Object.assign({}, res.getHeaders())
164 return res.locals.apicache.writeHead.apply(this, arguments as any)
165 }
166
167 res.write = function (chunk: any) {
168 self.accumulateContent(res, chunk)
169 return res.locals.apicache.write.apply(this, arguments as any)
170 }
171
172 res.end = function (content: any, encoding: BufferEncoding) {
173 if (self.shouldCacheResponse(res)) {
174 self.accumulateContent(res, content)
175
176 if (res.locals.apicache.cacheable && res.locals.apicache.content) {
177 self.addIndexEntries(key)
178
179 const headers = res.locals.apicache.headers || res.getHeaders()
180 const cacheObject = self.createCacheObject(
181 res.statusCode,
182 headers,
183 res.locals.apicache.content,
184 encoding
185 )
186 self.cacheResponse(key, cacheObject, duration)
187 }
188 }
189
190 res.locals.apicache.end.apply(this, arguments as any)
191 } as any
192
193 next()
194 }
195
196 private sendCachedResponse (request: express.Request, response: express.Response, cacheObject: CacheObject, duration: number) {
197 const headers = response.getHeaders()
198
199 if (isTestInstance()) {
200 Object.assign(headers, {
201 'x-api-cache-cached': 'true'
202 })
203 }
204
205 Object.assign(headers, this.filterBlacklistedHeaders(cacheObject.headers || {}), {
206 // Set properly decremented max-age header
207 // This ensures that max-age is in sync with the cache expiration
208 'cache-control':
209 'max-age=' +
210 Math.max(
211 0,
212 (duration / 1000 - (new Date().getTime() / 1000 - cacheObject.timestamp))
213 ).toFixed(0)
214 })
215
216 // unstringify buffers
217 let data = cacheObject.data
218 if (data && data.type === 'Buffer') {
219 data = typeof data.data === 'number'
220 ? Buffer.alloc(data.data)
221 : Buffer.from(data.data)
222 }
223
224 // Test Etag against If-None-Match for 304
225 const cachedEtag = cacheObject.headers.etag
226 const requestEtag = request.headers['if-none-match']
227
228 if (requestEtag && cachedEtag === requestEtag) {
229 response.writeHead(304, headers)
230 return response.end()
231 }
232
233 response.writeHead(cacheObject.status || 200, headers)
234
235 return response.end(data, cacheObject.encoding)
236 }
237
238 private clear (target: string) {
239 const redis = Redis.Instance.getClient()
240
241 if (target) {
242 clearTimeout(this.timers[target])
243 delete this.timers[target]
244
245 try {
246 redis.del(target)
247 } catch (err) {
248 logger.error('Cannot delete %s in redis cache.', target, { err })
249 }
250
251 this.index.all = this.index.all.filter(key => key !== target)
252 } else {
253 for (const key of this.index.all) {
254 clearTimeout(this.timers[key])
255 delete this.timers[key]
256
257 try {
258 redis.del(key)
259 } catch (err) {
260 logger.error('Cannot delete %s in redis cache.', key, { err })
261 }
262 }
263
264 this.index.all = []
265 }
266
267 return this.index
268 }
269}
diff --git a/server/middlewares/cache/shared/index.ts b/server/middlewares/cache/shared/index.ts
new file mode 100644
index 000000000..c707eaf7a
--- /dev/null
+++ b/server/middlewares/cache/shared/index.ts
@@ -0,0 +1 @@
export * from './api-cache'
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts
index 413653dac..a0035f623 100644
--- a/server/middlewares/index.ts
+++ b/server/middlewares/index.ts
@@ -1,4 +1,5 @@
1export * from './validators' 1export * from './validators'
2export * from './cache'
2export * from './activitypub' 3export * from './activitypub'
3export * from './async' 4export * from './async'
4export * from './auth' 5export * from './auth'
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts
index 5667207c0..55b434846 100644
--- a/server/tests/feeds/feeds.ts
+++ b/server/tests/feeds/feeds.ts
@@ -8,6 +8,7 @@ import {
8 createMultipleServers, 8 createMultipleServers,
9 createSingleServer, 9 createSingleServer,
10 doubleFollow, 10 doubleFollow,
11 makeGetRequest,
11 PeerTubeServer, 12 PeerTubeServer,
12 setAccessTokensToServers, 13 setAccessTokensToServers,
13 waitJobs 14 waitJobs
@@ -52,9 +53,7 @@ describe('Test syndication feeds', () => {
52 } 53 }
53 54
54 { 55 {
55 const attr = { username: 'john', password: 'password' } 56 userAccessToken = await servers[0].users.generateUserAndToken('john')
56 await servers[0].users.create({ username: attr.username, password: attr.password })
57 userAccessToken = await servers[0].login.getAccessToken(attr)
58 57
59 const user = await servers[0].users.getMyInfo({ token: userAccessToken }) 58 const user = await servers[0].users.getMyInfo({ token: userAccessToken })
60 userAccountId = user.account.id 59 userAccountId = user.account.id
@@ -108,6 +107,41 @@ describe('Test syndication feeds', () => {
108 expect(JSON.parse(jsonText)).to.be.jsonSchema({ type: 'object' }) 107 expect(JSON.parse(jsonText)).to.be.jsonSchema({ type: 'object' })
109 } 108 }
110 }) 109 })
110
111 it('Should serve the endpoint with a classic request', async function () {
112 await makeGetRequest({
113 url: servers[0].url,
114 path: '/feeds/videos.xml',
115 accept: 'application/xml',
116 expectedStatus: HttpStatusCode.OK_200
117 })
118 })
119
120 it('Should serve the endpoint as a cached request', async function () {
121 const res = await makeGetRequest({
122 url: servers[0].url,
123 path: '/feeds/videos.xml',
124 accept: 'application/xml',
125 expectedStatus: HttpStatusCode.OK_200
126 })
127
128 expect(res.headers['x-api-cache-cached']).to.equal('true')
129 })
130
131 it('Should not serve the endpoint as a cached request', async function () {
132 const res = await makeGetRequest({
133 url: servers[0].url,
134 path: '/feeds/videos.xml?v=186',
135 accept: 'application/xml',
136 expectedStatus: HttpStatusCode.OK_200
137 })
138
139 expect(res.headers['x-api-cache-cached']).to.not.exist
140 })
141
142 it('Should refuse to serve the endpoint without accept header', async function () {
143 await makeGetRequest({ url: servers[0].url, path: '/feeds/videos.xml', expectedStatus: HttpStatusCode.NOT_ACCEPTABLE_406 })
144 })
111 }) 145 })
112 146
113 describe('Videos feed', function () { 147 describe('Videos feed', function () {
diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts
index 5469f3b83..1a99b598a 100644
--- a/server/typings/express/index.d.ts
+++ b/server/typings/express/index.d.ts
@@ -1,4 +1,5 @@
1 1
2import { OutgoingHttpHeaders } from 'http'
2import { RegisterServerAuthExternalOptions } from '@server/types' 3import { RegisterServerAuthExternalOptions } from '@server/types'
3import { 4import {
4 MAbuseMessage, 5 MAbuseMessage,
@@ -40,6 +41,7 @@ import {
40 MVideoShareActor, 41 MVideoShareActor,
41 MVideoThumbnail 42 MVideoThumbnail
42} from '../../types/models' 43} from '../../types/models'
44import { Writable } from 'stream'
43 45
44declare module 'express' { 46declare module 'express' {
45 export interface Request { 47 export interface Request {
@@ -98,6 +100,15 @@ declare module 'express' {
98 }) => void 100 }) => void
99 101
100 locals: { 102 locals: {
103 apicache: {
104 content: string | Buffer
105 write: Writable['write']
106 writeHead: Response['writeHead']
107 end: Response['end']
108 cacheable: boolean
109 headers: OutgoingHttpHeaders
110 }
111
101 docUrl?: string 112 docUrl?: string
102 113
103 videoAPI?: MVideoFormattableDetails 114 videoAPI?: MVideoFormattableDetails
diff --git a/yarn.lock b/yarn.lock
index f68741038..f0cf2d449 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1243,11 +1243,6 @@ anymatch@~3.1.1, anymatch@~3.1.2:
1243 normalize-path "^3.0.0" 1243 normalize-path "^3.0.0"
1244 picomatch "^2.0.4" 1244 picomatch "^2.0.4"
1245 1245
1246apicache@1.6.2:
1247 version "1.6.2"
1248 resolved "https://registry.yarnpkg.com/apicache/-/apicache-1.6.2.tgz#a0a3d51024fa2814c4ace7e9e7053ebcb0920ee6"
1249 integrity sha512-3z5e+1E2qwZoqzFVgdx5l9nGhSG0kHv3v2G170vnJSz5uj4mCLVZfRw0o37aWwV8pTPXSkB8OBZz3TIur4H26g==
1250
1251append-field@^1.0.0: 1246append-field@^1.0.0:
1252 version "1.0.0" 1247 version "1.0.0"
1253 resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" 1248 resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56"