Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
"r": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
"ecmaVersion": 2020
}
}
50 changes: 37 additions & 13 deletions lib/remote-storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,23 +149,39 @@ module.exports = class RemoteStorage {
}
const content = await fs.readFile(file)
const mimeType = mime.lookup(path.extname(file))
// first we will grab it from the global config: htmlCacheDuration, etc.
const cacheControlString = this._getCacheControlConfig(mimeType, appConfig.app)
const uploadParams = {
Bucket: this.bucket,
Key: urlJoin(prefix, path.basename(file)),
Body: content,
CacheControl: cacheControlString
Body: content
}
// if we found it in the global config, we will use it ( for now )
if (cacheControlString) {
uploadParams.CacheControl = cacheControlString
}
// add response headers if specified in manifest
const responseHeaders = this.getResponseHeadersForFile(file, distRoot, appConfig)
if (responseHeaders) {
const responseHeaders = this.getResponseHeadersForFile(file, distRoot, appConfig) ?? {}
// here we allow overriding the cache control if specified in response headers
// this is considered more specific than the general cache control config
// ideally we deprecate cache control config in favor of response headers directly
if (responseHeaders?.['adp-cache-control']) {
uploadParams.CacheControl = responseHeaders['adp-cache-control']
delete responseHeaders['adp-cache-control']
}

if (appConfig.auditUserId) {
responseHeaders['adp-AuditUserId'] = appConfig.auditUserId
}
// we only set metadata if we have added anything to responseHeaders object
// it is not null, but could be empty
if (Object.keys(responseHeaders).length > 0) {
uploadParams.Metadata = responseHeaders
}
// s3 misses some mime types like for css files
if (mimeType) {
uploadParams.ContentType = mimeType
}

// Note: putObject is recommended for files < 100MB and has a limit of 5GB, which is ok for our use case of storing static web assets
// if we intend to store larger files, we should use multipart upload and https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_lib_storage.html
return this.s3.putObject(uploadParams)
Expand All @@ -190,6 +206,13 @@ module.exports = class RemoteStorage {
return responseHeaders
}

/**
* Checks if a header can be added to a file based on the rule
* @param {string} file - file path
* @param {string} distRoot - distribution root
* @param {string} rule - rule to check
* @returns {boolean} true if header can be added, false otherwise
*/
canAddHeader (file, distRoot, rule) {
const filePath = path.parse(file)
const normalisedRule = rule.replace(/\//g, path.sep)
Expand All @@ -198,7 +221,6 @@ module.exports = class RemoteStorage {
if (folderPathToMatch.endsWith(path.sep)) {
folderPathToMatch = folderPathToMatch.substring(0, folderPathToMatch.length - 1) // remove any trailing path separator
}

if (rule === '/*') { // all content
return true
} else if (rule.endsWith('/*')) { // all content in a folder ex. /test/*
Expand Down Expand Up @@ -293,17 +315,19 @@ module.exports = class RemoteStorage {
* @param {Object} appConfig - application config
*/
_getCacheControlConfig (mimeType, appConfig) {
const cacheControlStr = 's-maxage=0'
const cacheControlStr = 's-maxage=60'
if (!mimeType) {
return cacheControlStr
return null
} else if (mimeType === mime.lookup('html')) {
return cacheControlStr + ', max-age=' + appConfig.htmlCacheDuration
return `${cacheControlStr}, max-age=${appConfig.htmlCacheDuration}`
} else if (mimeType === mime.lookup('js')) {
return cacheControlStr + ', max-age=' + appConfig.jsCacheDuration
return `${cacheControlStr}, max-age=${appConfig.jsCacheDuration}`
} else if (mimeType === mime.lookup('css')) {
return cacheControlStr + ', max-age=' + appConfig.cssCacheDuration
return `${cacheControlStr}, max-age=${appConfig.cssCacheDuration}`
} else if (mimeType.startsWith('image')) {
return cacheControlStr + ', max-age=' + appConfig.imageCacheDuration
} else { return cacheControlStr }
return `${cacheControlStr}, max-age=${appConfig.imageCacheDuration}`
} else {
return null
}
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"klaw": "^4",
"lodash.clonedeep": "^4.5.0",
"mime-types": "^2.1.24",
"parcel": "^2.7.0",
"parcel": "^2.15.4",
"proxy-agent": "^6.3.0",
"regenerator-runtime": "^0.13.7"
},
Expand Down
99 changes: 94 additions & 5 deletions test/lib/remote-storage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,31 +300,31 @@ describe('RemoteStorage', () => {
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')
expect(response).toBe('s-maxage=60, 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')
expect(response).toBe('s-maxage=60, 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')
expect(response).toBe('s-maxage=60, 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')
expect(response).toBe('s-maxage=60, 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')
expect(response).toBe(null)
})

// response header tests
Expand Down Expand Up @@ -573,4 +573,93 @@ describe('RemoteStorage', () => {
}
expect(mockS3.putObject).toHaveBeenCalledWith(expect.objectContaining(expected))
})

test('Cache control override from response headers', async () => {
global.addFakeFiles(vol, 'fakeDir', { 'index.html': 'fake content' })
const rs = new RemoteStorage(global.fakeTVMResponse)
const files = await rs.walkDir('fakeDir')
const fakeDistRoot = path.parse(files[0]).dir
const filePath = files[0] // Use absolute path from walkDir
const newConfig = global.configWithModifiedWeb(global.fakeConfig, {
'response-headers': {
'/*.html': {
'cache-control': 'max-age=3600, s-maxage=7200',
testHeader: 'generic-header'
}
}
})
await rs.uploadFile(filePath, 'fakeprefix', newConfig, fakeDistRoot)
const body = Buffer.from('fake content', 'utf8')
const expected = {
Bucket: 'fake-bucket',
Key: 'fakeprefix/index.html',
Body: body,
ContentType: 'text/html',
CacheControl: 'max-age=3600, s-maxage=7200',
Metadata: {
'adp-testHeader': 'generic-header'
}
}
expect(mockS3.putObject).toHaveBeenCalledWith(expect.objectContaining(expected))
// Verify that adp-cache-control was removed from metadata
const putObjectCall = mockS3.putObject.mock.calls[0][0]
expect(putObjectCall.Metadata).not.toHaveProperty('adp-cache-control')
})

test('uploadFile includes auditUserId in metadata when set', async () => {
global.addFakeFiles(vol, 'fakeDir', { 'index.js': 'fake content' })
const rs = new RemoteStorage(global.fakeTVMResponse)
const fakeConfig = { ...global.fakeConfig, auditUserId: 'test-user-123' }
await rs.uploadFile('fakeDir/index.js', 'fakeprefix', fakeConfig, 'fakeDir')
const body = Buffer.from('fake content', 'utf8')
const expected = {
Bucket: 'fake-bucket',
Key: 'fakeprefix/index.js',
Body: body,
ContentType: 'application/javascript',
Metadata: expect.objectContaining({
'adp-AuditUserId': 'test-user-123'
})
}
expect(mockS3.putObject).toHaveBeenCalledWith(expect.objectContaining(expected))
})

test('uploadFile does not set Metadata when responseHeaders is empty and auditUserId is not set', async () => {
global.addFakeFiles(vol, 'fakeDir', { 'index.js': 'fake content' })
const rs = new RemoteStorage(global.fakeTVMResponse)
const fakeConfig = {
app: global.fakeConfig.app
// No web.response-headers and no auditUserId
}
await rs.uploadFile('fakeDir/index.js', 'fakeprefix', fakeConfig, 'fakeDir')
const body = Buffer.from('fake content', 'utf8')
const putObjectCall = mockS3.putObject.mock.calls[0][0]
expect(putObjectCall).not.toHaveProperty('Metadata')
expect(putObjectCall).toMatchObject({
Bucket: 'fake-bucket',
Key: 'fakeprefix/index.js',
Body: body,
ContentType: 'application/javascript'
})
})

test('uploadFile sets CacheControl even when responseHeaders is empty and auditUserId is not set', async () => {
global.addFakeFiles(vol, 'fakeDir', { 'index.html': 'fake content' })
const rs = new RemoteStorage(global.fakeTVMResponse)
const fakeConfig = {
app: global.fakeConfig.app
// No web.response-headers and no auditUserId
}
await rs.uploadFile('fakeDir/index.html', 'fakeprefix', fakeConfig, 'fakeDir')
const body = Buffer.from('fake content', 'utf8')
const putObjectCall = mockS3.putObject.mock.calls[0][0]
expect(putObjectCall).not.toHaveProperty('Metadata')
expect(putObjectCall).toMatchObject({
Bucket: 'fake-bucket',
Key: 'fakeprefix/index.html',
Body: body,
ContentType: 'text/html',
CacheControl: 's-maxage=60, max-age=60'
})
})
})
Loading