DEV: Add optional s3_stale_while_revalidate and s3_max_age for uploads (#36298)

With this change, S3's `stale-while-revalidate` and `max-age` response
directives can be controlled.

By default, off for `stale-while-revalidate`, and both are hidden site
settings.
This commit is contained in:
Gabriel Grubba
2025-12-01 14:57:50 -03:00
committed by GitHub
parent cc7f4743a5
commit 568afbae47
3 changed files with 108 additions and 1 deletions
+10
View File
@@ -2212,6 +2212,16 @@ files:
s3_access_key_id:
default: ""
secret: true
s3_max_age:
default: 31556952
min: 60
max: 31556952
hidden: true
s3_stale_while_revalidate:
default: 31556952
min: 60
max: 31556952
hidden: true
s3_secret_access_key:
default: ""
secret: true
+7 -1
View File
@@ -90,8 +90,14 @@ module FileStore
# cache file locally when needed
cache_file(file, File.basename(path)) if opts[:cache_locally]
cache_control = "max-age=#{SiteSetting.s3_max_age}, public, immutable"
if SiteSetting.s3_stale_while_revalidate != SiteSetting.defaults[:s3_stale_while_revalidate]
cache_control =
"#{cache_control}, stale-while-revalidate=#{SiteSetting.s3_stale_while_revalidate}"
end
options = {
cache_control: "max-age=31556952, public, immutable",
cache_control: cache_control,
content_type:
opts[:content_type].presence || MiniMime.lookup_by_filename(filename)&.content_type,
}.merge(default_s3_options(secure: opts[:private]))
+91
View File
@@ -54,6 +54,97 @@ RSpec.describe FileStore::S3Store do
expect(upload.etag).to eq(etag)
end
it "adds `stale-while-revalidate` response directive when `s3_stale_while_revalidate` site setting is set" do
SiteSetting.s3_stale_while_revalidate = 3600
expected_cache_control = "max-age=31556952, public, immutable, stale-while-revalidate=3600"
s3_helper.expects(:s3_bucket).returns(s3_bucket).at_least_once
s3_bucket
.expects(:object)
.with(regexp_matches(%r{original/\d+X.*/#{upload.sha1}\.png}))
.returns(s3_object)
s3_object
.expects(:put)
.with(
{
acl: FileStore::S3Store::CANNED_ACL_PUBLIC_READ,
cache_control: expected_cache_control,
content_type: "image/png",
content_disposition: "inline; filename=\"logo.png\"; filename*=UTF-8''logo.png",
body: uploaded_file,
},
)
.returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\""))
expect(store.store_upload(uploaded_file, upload)).to match(
%r{//s3-upload-bucket\.s3\.dualstack\.us-west-1\.amazonaws\.com/original/\d+X.*/#{upload.sha1}\.png},
)
expect(upload.etag).to eq(etag)
end
it "respects `s3_max_age` site setting for cache_control" do
SiteSetting.s3_max_age = 60
expected_cache_control = "max-age=60, public, immutable"
s3_helper.expects(:s3_bucket).returns(s3_bucket).at_least_once
s3_bucket
.expects(:object)
.with(regexp_matches(%r{original/\d+X.*/#{upload.sha1}\.png}))
.returns(s3_object)
s3_object
.expects(:put)
.with(
{
acl: FileStore::S3Store::CANNED_ACL_PUBLIC_READ,
cache_control: expected_cache_control,
content_type: "image/png",
content_disposition: "inline; filename=\"logo.png\"; filename*=UTF-8''logo.png",
body: uploaded_file,
},
)
.returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\""))
expect(store.store_upload(uploaded_file, upload)).to match(
%r{//s3-upload-bucket\.s3\.dualstack\.us-west-1\.amazonaws\.com/original/\d+X.*/#{upload.sha1}\.png},
)
expect(upload.etag).to eq(etag)
end
describe "when default site settings are set" do
it "cache_control is `max-age=31556952, public, immutable`" do
default_cache_control = "max-age=31556952, public, immutable"
s3_helper.expects(:s3_bucket).returns(s3_bucket).at_least_once
s3_bucket
.expects(:object)
.with(regexp_matches(%r{original/\d+X.*/#{upload.sha1}\.png}))
.returns(s3_object)
s3_object
.expects(:put)
.with(
{
acl: FileStore::S3Store::CANNED_ACL_PUBLIC_READ,
cache_control: default_cache_control,
content_type: "image/png",
content_disposition: "inline; filename=\"logo.png\"; filename*=UTF-8''logo.png",
body: uploaded_file,
},
)
.returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\""))
expect(store.store_upload(uploaded_file, upload)).to match(
%r{//s3-upload-bucket\.s3\.dualstack\.us-west-1\.amazonaws\.com/original/\d+X.*/#{upload.sha1}\.png},
)
expect(upload.etag).to eq(etag)
end
end
describe "when s3_upload_bucket includes folders path" do
before do
s3_object.stubs(:put).returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\""))