diff --git a/lib/remote-storage.js b/lib/remote-storage.js index d69bbba..78fec39 100644 --- a/lib/remote-storage.js +++ b/lib/remote-storage.js @@ -23,6 +23,8 @@ const { ProxyAgent } = require('proxy-agent') const { codes, logAndThrow } = require('./StorageError') const fileExtensionPattern = /\*\.[0-9a-zA-Z]+$/ +const cacheControlHeader = 'cache-control' +const awsCacheControlMetadata = 'cacheControl' // /** // * Joins url path parts @@ -149,18 +151,21 @@ module.exports = class RemoteStorage { } const content = await fs.readFile(file) const mimeType = mime.lookup(path.extname(file)) - const cacheControlString = this._getCacheControlConfig(mimeType, appConfig.app) const uploadParams = { Bucket: this.bucket, Key: urlJoin(prefix, path.basename(file)), - Body: content, - CacheControl: cacheControlString + Body: content } // add response headers if specified in manifest - const responseHeaders = this.getResponseHeadersForFile(file, distRoot, appConfig) + const { responseHeaders, cacheControl } = this.getResponseHeadersForFile(file, distRoot, appConfig) if (responseHeaders) { - uploadParams.Metadata = responseHeaders + uploadParams.Metadata = { ...responseHeaders } } + + if (cacheControl) { + uploadParams[awsCacheControlMetadata] = cacheControl + } + // s3 misses some mime types like for css files if (mimeType) { uploadParams.ContentType = mimeType @@ -173,21 +178,26 @@ module.exports = class RemoteStorage { getResponseHeadersForFile (file, distRoot, appConfig) { let responseHeaders + let cacheControl if (appConfig.web && appConfig.web['response-headers']) { responseHeaders = {} const cdnConfig = appConfig.web['response-headers'] const headerPrefix = 'adp-' - Object.keys(cdnConfig).forEach(rule => { if (this.canAddHeader(file, distRoot, rule)) { Object.keys(cdnConfig[rule]).forEach(header => { this.validateHTTPHeader(header, cdnConfig[rule][header]) - responseHeaders[headerPrefix + header] = cdnConfig[rule][header] + if (header.toLowerCase() === cacheControlHeader) { + // treat cache control header as a special case and store it in the cacheControl metadata + cacheControl = cdnConfig[rule][header] + } else { + responseHeaders[headerPrefix + header] = cdnConfig[rule][header] + } }) } }) } - return responseHeaders + return { responseHeaders, cacheControl } } canAddHeader (file, distRoot, rule) { @@ -286,24 +296,4 @@ module.exports = class RemoteStorage { } return allResults } - - /** - * Get cache control string based on mime type and config - * @param {string|boolean} mimeType - string if valid mimeType or false for unknown files - * @param {Object} appConfig - application config - */ - _getCacheControlConfig (mimeType, appConfig) { - const cacheControlStr = 's-maxage=0' - if (!mimeType) { - return cacheControlStr - } else if (mimeType === mime.lookup('html')) { - return cacheControlStr + ', max-age=' + appConfig.htmlCacheDuration - } else if (mimeType === mime.lookup('js')) { - return cacheControlStr + ', max-age=' + appConfig.jsCacheDuration - } else if (mimeType === mime.lookup('css')) { - return cacheControlStr + ', max-age=' + appConfig.cssCacheDuration - } else if (mimeType.startsWith('image')) { - return cacheControlStr + ', max-age=' + appConfig.imageCacheDuration - } else { return cacheControlStr } - } } diff --git a/test/lib/remote-storage.test.js b/test/lib/remote-storage.test.js index abae930..a756f44 100644 --- a/test/lib/remote-storage.test.js +++ b/test/lib/remote-storage.test.js @@ -297,36 +297,6 @@ describe('RemoteStorage', () => { expect(cbMock).toHaveBeenCalledTimes(4) }) - test('cachecontrol string for html', async () => { - const rs = new RemoteStorage(global.fakeTVMResponse) - const response = rs._getCacheControlConfig('text/html', global.fakeConfig.app) - expect(response).toBe('s-maxage=0, max-age=60') - }) - - test('cachecontrol string for JS', async () => { - const rs = new RemoteStorage(global.fakeTVMResponse) - const response = rs._getCacheControlConfig('application/javascript', global.fakeConfig.app) - expect(response).toBe('s-maxage=0, max-age=604800') - }) - - test('cachecontrol string for CSS', async () => { - const rs = new RemoteStorage(global.fakeTVMResponse) - const response = rs._getCacheControlConfig('text/css', global.fakeConfig.app) - expect(response).toBe('s-maxage=0, max-age=604800') - }) - - test('cachecontrol string for Image', async () => { - const rs = new RemoteStorage(global.fakeTVMResponse) - const response = rs._getCacheControlConfig('image/jpeg', global.fakeConfig.app) - expect(response).toBe('s-maxage=0, max-age=604800') - }) - - test('cachecontrol string for default', async () => { - const rs = new RemoteStorage(global.fakeTVMResponse) - const response = rs._getCacheControlConfig('application/pdf', global.fakeConfig.app) - expect(response).toBe('s-maxage=0') - }) - // response header tests test('get response header from config with multiple rules', async () => { const rs = new RemoteStorage(global.fakeTVMResponse) @@ -354,11 +324,11 @@ describe('RemoteStorage', () => { const fakeDistRoot = path.parse(files[0]).dir const expectedValMap = { - 'index.html': { 'adp-testHeader': 'generic-header' }, - 'test.js': { 'adp-testHeader': 'specific-file-header' } + 'index.html': { responseHeaders: { 'adp-testHeader': 'generic-header' }, cacheControl: undefined }, + 'test.js': { responseHeaders: { 'adp-testHeader': 'specific-file-header' }, cacheControl: undefined } } - expectedValMap[folderPath1] = { 'adp-testHeader': 'folder-header' } - expectedValMap[folderPath2] = { 'adp-testHeader': 'all-js-file-in-folder-header' } + expectedValMap[folderPath1] = { responseHeaders: { 'adp-testHeader': 'folder-header' }, cacheControl: undefined } + expectedValMap[folderPath2] = { responseHeaders: { 'adp-testHeader': 'all-js-file-in-folder-header' }, cacheControl: undefined } files.forEach(f => { const fileName = f.replace(path.join(fakeDistRoot, path.sep), '') @@ -399,13 +369,13 @@ describe('RemoteStorage', () => { // set the expectation const expectedValMap = { - 'index.html': { 'adp-testHeader': 'generic-header' }, - 'test.js': { 'adp-testHeader': 'generic-header' } + 'index.html': { responseHeaders: { 'adp-testHeader': 'generic-header' }, cacheControl: undefined }, + 'test.js': { responseHeaders: { 'adp-testHeader': 'generic-header' }, cacheControl: undefined } } - expectedValMap[folderPath1] = { 'adp-testHeader': 'all-files-in-css-folder-header' } - expectedValMap[folderPath2] = { 'adp-testHeader': 'all-files-in-js-folder-header' } - expectedValMap[folderPath3] = { 'adp-testHeader': 'all-files-in-images-folder-header' } - expectedValMap[folderPath4] = { 'adp-testHeader': 'all-files-in-images-folder-header' } + expectedValMap[folderPath1] = { responseHeaders: { 'adp-testHeader': 'all-files-in-css-folder-header' }, cacheControl: undefined } + expectedValMap[folderPath2] = { responseHeaders: { 'adp-testHeader': 'all-files-in-js-folder-header' }, cacheControl: undefined } + expectedValMap[folderPath3] = { responseHeaders: { 'adp-testHeader': 'all-files-in-images-folder-header' }, cacheControl: undefined } + expectedValMap[folderPath4] = { responseHeaders: { 'adp-testHeader': 'all-files-in-images-folder-header' }, cacheControl: undefined } // check header application per file files.forEach(f => { @@ -455,12 +425,12 @@ describe('RemoteStorage', () => { // set the expectation const expectedValMap = { - 'index.html': { 'adp-testHeader': 'generic-header' }, - 'test.js': { 'adp-testHeader': 'generic-header' } + 'index.html': { responseHeaders: { 'adp-testHeader': 'generic-header' }, cacheControl: undefined }, + 'test.js': { responseHeaders: { 'adp-testHeader': 'generic-header' }, cacheControl: undefined } } - expectedValMap[folderPath1] = { 'adp-testHeader': 'specific-css-file-header' } - expectedValMap[folderPath2] = { 'adp-testHeader': 'specific-js-file-header' } - expectedValMap[folderPath3] = { 'adp-testHeader': 'specific-image-file-header' } + expectedValMap[folderPath1] = { responseHeaders: { 'adp-testHeader': 'specific-css-file-header' }, cacheControl: undefined } + expectedValMap[folderPath2] = { responseHeaders: { 'adp-testHeader': 'specific-js-file-header' }, cacheControl: undefined } + expectedValMap[folderPath3] = { responseHeaders: { 'adp-testHeader': 'specific-image-file-header' }, cacheControl: undefined } // check header application per file files.forEach(f => { @@ -501,12 +471,12 @@ describe('RemoteStorage', () => { // set the expectation const expectedValMap = { - 'index.html': { 'adp-testHeader': 'generic-header' }, - 'test.js': { 'adp-testHeader': 'all-js-files-header' } + 'index.html': { responseHeaders: { 'adp-testHeader': 'generic-header' }, cacheControl: undefined }, + 'test.js': { responseHeaders: { 'adp-testHeader': 'all-js-files-header' }, cacheControl: undefined } } - expectedValMap[folderPath1] = { 'adp-testHeader': 'all-css-files-header' } - expectedValMap[folderPath2] = { 'adp-testHeader': 'all-js-files-header' } - expectedValMap[folderPath3] = { 'adp-testHeader': 'all-png-files-header' } + expectedValMap[folderPath1] = { responseHeaders: { 'adp-testHeader': 'all-css-files-header' }, cacheControl: undefined } + expectedValMap[folderPath2] = { responseHeaders: { 'adp-testHeader': 'all-js-files-header' }, cacheControl: undefined } + expectedValMap[folderPath3] = { responseHeaders: { 'adp-testHeader': 'all-png-files-header' }, cacheControl: undefined } // check header application per file files.forEach(f => { @@ -555,7 +525,8 @@ describe('RemoteStorage', () => { const newConfig = global.configWithModifiedWeb(global.fakeConfig, { 'response-headers': { '/*': { - testHeader: 'generic-header' + testHeader: 'generic-header', + 'cache-control': 's-maxage=3600, max-age=60' } } }) @@ -569,8 +540,50 @@ describe('RemoteStorage', () => { ContentType: 'application/javascript', Metadata: { 'adp-testHeader': 'generic-header' - } + }, + cacheControl: 's-maxage=3600, max-age=60' } expect(mockS3.putObject).toHaveBeenCalledWith(expect.objectContaining(expected)) }) + + test('set cache control response headers for config with multiple rules', async () => { + // custom cache control are now set via response headers + const rs = new RemoteStorage(global.fakeTVMResponse) + const newConfig = global.configWithModifiedWeb(global.fakeConfig, { + 'response-headers': { + '/*': { + 'cache-control': 's-maxage=0, max-age=60' + }, + '/testFolder/*': { + 'Cache-Control': 's-maxage=500, max-age=0' + }, + '/testFolder/*.js': { + 'Cache-CoNtrol': 's-maxage=0, max-age=604800' + }, + '/test.js': { + 'Cache-COntrol': 's-maxage=604800, max-age=604800' + } + } + }) + + const folderPath1 = 'testFolder' + path.sep + 'index.html' + const folderPath2 = 'testFolder' + path.sep + 'test.js' + await global.addFakeFiles(vol, 'fakeDir', ['index.html', 'test.js', folderPath1, folderPath2]) + const files = await rs.walkDir('fakeDir') + const fakeDistRoot = path.parse(files[0]).dir + + const expectedValMap = { + 'index.html': { responseHeaders: {}, cacheControl: 's-maxage=0, max-age=60' }, + 'test.js': { responseHeaders: {}, cacheControl: 's-maxage=604800, max-age=604800' } + } + expectedValMap[folderPath1] = { responseHeaders: {}, cacheControl: 's-maxage=500, max-age=0' } + expectedValMap[folderPath2] = { responseHeaders: {}, cacheControl: 's-maxage=0, max-age=604800' } + + files.forEach(f => { + const fileName = f.replace(path.join(fakeDistRoot, path.sep), '') + const response = rs.getResponseHeadersForFile(f, fakeDistRoot, newConfig) + const expected = expectedValMap[fileName] + expect(response).toStrictEqual(expected) + }) + }) })