From 51ad5f9adea717a49202baca767a743d5a4806cb Mon Sep 17 00:00:00 2001 From: Richard Petersen <richard.petersen@open-xchange.com> Date: Thu, 25 Aug 2022 16:15:18 +0200 Subject: [PATCH] Add: ui-files are compressed before stored in cache or delivered to the client --- integration/caching_test.js | 7 +++++-- spec/file_caching_test.js | 10 ++++++++++ src/files.js | 32 +++++++++++++++++++------------- src/middlewares/serve-files.js | 3 +-- 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/integration/caching_test.js b/integration/caching_test.js index b3915a8..a4b831e 100644 --- a/integration/caching_test.js +++ b/integration/caching_test.js @@ -4,6 +4,7 @@ import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from '../s import { client } from '../src/redis.js' import * as td from 'testdouble' import { getRedisKey } from '../src/util.js' +import { gunzipSync } from 'node:zlib' describe('File caching service', function () { let app @@ -40,8 +41,10 @@ describe('File caching service', function () { expect(response.statusCode).to.equal(200) const version = response.headers.version - expect(await client.get(getRedisKey({ version, name: '/index.html:meta' }))).to.equal('{"sha256Sum":"iFSuC3aK6EN/ASUamuZ+j3xZXI9eBdIlxtVDFjn7y1I=","headers":{"content-type":"text/html","dependencies":false}}') - expect(await client.get(getRedisKey({ version, name: '/index.html:body' }))).to.equal('<html><head></head><body>it\'s me</body></html>') + const body = await client.getBuffer(getRedisKey({ version, name: '/index.html:body' })) + expect(gunzipSync(body).toString()).to.equal('<html><head></head><body>it\'s me</body></html>') + const meta = await client.get(getRedisKey({ version, name: '/index.html:meta' })) + expect(meta).to.equal('{"headers":{"content-type":"text/html","dependencies":false,"content-encoding":"gzip"}}') }) it('serves files from redis and stores them in local cache', async function () { diff --git a/spec/file_caching_test.js b/spec/file_caching_test.js index 2037f3a..3bbf8f2 100644 --- a/spec/file_caching_test.js +++ b/spec/file_caching_test.js @@ -5,6 +5,7 @@ import { expect } from 'chai' import * as td from 'testdouble' import RedisMock from 'ioredis-mock' import sinon from 'sinon' +import zlib from 'node:zlib' const image = fs.readFileSync('./spec/media/image.png') const imageStat = fs.statSync('./spec/media/image.png') @@ -347,4 +348,13 @@ describe('File caching service', function () { expect(spy.callCount).to.equal(1) }) }) + + it('serves files as gzip', async function () { + const response = await request(app).get('/example.js') + expect(response.statusCode).to.equal(200) + expect(response.headers['content-encoding']).to.equal('gzip') + + // check for files in redis + expect(zlib.gunzipSync(await redis.client.getBuffer('ui-middleware:554855300:/example.js:body')).toString()).to.equal(response.text) + }) }) diff --git a/src/files.js b/src/files.js index 7fe9bdb..e7bd62a 100644 --- a/src/files.js +++ b/src/files.js @@ -1,4 +1,3 @@ -import crypto from 'crypto' import { config } from './config.js' import { getRedisKey, isJSFile } from './util.js' import { getCSSDependenciesFor, getViteManifests } from './manifests.js' @@ -6,12 +5,28 @@ import * as cache from './cache.js' import { logger } from './logger.js' import { NotFoundError } from './errors.js' import * as redis from './redis.js' +import zlib from 'node:zlib' +import { promisify } from 'node:util' +const gzip = promisify(zlib.gzip) export function createWritable (body) { if (typeof body !== 'string' && !(body instanceof Buffer)) return JSON.stringify(body) return body } +async function createFileBuffer (response, dependencies) { + const cssString = dependencies && dependencies.map(file => `"${file}"`).join(',') + const appendix = cssString && `\n/*injected by ui-middleware*/document.dispatchEvent(new CustomEvent("load-css",{detail:{css:[${cssString}]}}))` + const resBuffer = await response.arrayBuffer() + const appendixLength = appendix?.length || 0 + const buffer = Buffer.alloc(resBuffer.byteLength + appendixLength) + + buffer.fill(Buffer.from(resBuffer), 0, resBuffer.byteLength) + if (appendix) buffer.write(appendix, resBuffer.byteLength) + + return await gzip(buffer) +} + export async function fetchFileWithHeadersFromBaseUrl (path, baseUrl, version) { const [response, dependencies] = await Promise.all([ fetch(new URL(path, baseUrl)), @@ -20,23 +35,14 @@ export async function fetchFileWithHeadersFromBaseUrl (path, baseUrl, version) { if (!response.ok) throw new NotFoundError(`Error fetching file: ${path}`) - const cssString = dependencies && dependencies.map(file => `"${file}"`).join(',') - const appendix = cssString && `\n/*injected by ui-middleware*/document.dispatchEvent(new CustomEvent("load-css",{detail:{css:[${cssString}]}}))` - const resBuffer = await response.arrayBuffer() - const appendixLength = appendix?.length || 0 - const body = Buffer.alloc(resBuffer.byteLength + appendixLength) - - body.fill(Buffer.from(resBuffer), 0, resBuffer.byteLength) - if (appendix) body.write(appendix, resBuffer.byteLength) - - const sha256Sum = crypto.createHash('sha256').update(body).digest('base64') + const body = await createFileBuffer(response, dependencies) return { body, - sha256Sum, headers: { 'content-type': response.headers.get('content-type'), - dependencies + dependencies, + 'content-encoding': 'gzip' } } } diff --git a/src/middlewares/serve-files.js b/src/middlewares/serve-files.js index 1ef2cb4..4bae733 100644 --- a/src/middlewares/serve-files.js +++ b/src/middlewares/serve-files.js @@ -7,10 +7,9 @@ export default async function (req, res, next) { if (req.method !== 'GET') return next() const version = res.version const path = req.path === '/' ? '/index.html' : req.path - const { body, headers, sha256Sum } = await getFile({ version, path }) + const { body, headers } = await getFile({ version, path }) res.set(headers) - res.locals.sha256Sum = sha256Sum res.send(body) } catch (err) { const errors = err.errors || [err] -- GitLab