The S3 storage handles uploads to Amazon S3 service, using the aws-sdk-s3 gem:
gem "aws-sdk-s3", "~> 1.14"
It can be initialized by providing the bucket name and credentials:
require "shrine/storage/s3"
s3 = Shrine::Storage::S3.new(
bucket: "my-app", # required
access_key_id: "abc",
secret_access_key: "xyz",
region: "eu-west-1",
)
The core features of this storage require the following AWS permissions:
s3:ListBucket
, s3:PutObject
, s3:GetObject
, and s3:DeleteObject
. If you
have additional upload options configured such as setting object ACLs, then
additional permissions may be required.
The storage exposes the underlying Aws objects:
s3.client #=> #<Aws::S3::Client>
s3.client.access_key_id #=> "abc"
s3.client.secret_access_key #=> "xyz"
s3.client.region #=> "eu-west-1"
s3.bucket #=> #<Aws::S3::Bucket>
s3.bucket.name #=> "my-app"
s3.object("key") #=> #<Aws::S3::Object>
By default, uploaded S3 objects will have private visibility, meaning they can only be accessed via signed expiring URLs generated using your private S3 credentials. If you would like to generate public URLs, you can tell S3 storage to make uploads public:
s3 = Shrine::Storage::S3.new(public: true, **s3_options)
s3.upload(io, "key") # uploads with "public-read" ACL
s3.url("key") # returns public (unsigned) object URL
The :prefix
option can be specified for uploading all files inside a specific
S3 prefix (folder), which is useful when using S3 for both cache and store:
Shrine::Storage::S3.new(prefix: "cache", **s3_options)
Sometimes you'll want to add additional upload options to all S3 uploads. You
can do that by passing the :upload_options
option:
Shrine::Storage::S3.new(upload_options: { acl: "private" }, **s3_options)
These options will be passed to aws-sdk-s3's methods for uploading, copying and presigning.
You can also generate upload options per upload with the upload_options
plugin
class MyUploader < Shrine
plugin :upload_options, store: -> (io, context) do
if context[:version] == :thumb
{ acl: "public-read" }
else
{ acl: "private" }
end
end
end
or when using the uploader directly
uploader.upload(file, upload_options: { acl: "private" })
Note that, unlike the :upload_options
storage option, upload options given on
the uploader level won't be forwarded for generating presigns, since presigns
are generated using the storage directly.
Other than :host
and :public
URL options,
all additional options are forwarded to Aws::S3::Object#presigned_url
.
s3.url(
expires_in: 15,
response_content_disposition: ContentDisposition.attachment("my-filename"),
response_content_type: "foo/bar",
# ...
)
If you want your S3 object URLs to be generated with a different URL host (e.g.
a CDN), you can specify the :host
option to #url
:
s3.url("image.jpg", host: "http://abc123.cloudfront.net")
#=> "http://abc123.cloudfront.net/image.jpg"
The host URL can include a path prefix, but it needs to end with a slash:
s3.url("image.jpg", host: "https://your-s3-host.com/prefix/") # needs to end with a slash
#=> "http://your-s3-host.com/prefix/image.jpg"
To have the :host
option passed automatically for every URL, use the
default_url_options
plugin.
plugin :default_url_options, store: { host: "http://abc123.cloudfront.net" }
If you would like to serve private content via CloudFront, you need to sign
the object URLs with a special signer, such as Aws::CloudFront::UrlSigner
provided by the aws-sdk-cloudfront
gem. The S3 storage initializer accepts a
:signer
block, which you can use to call your signer:
require "aws-sdk-cloudfront"
signer = Aws::CloudFront::UrlSigner.new(
key_pair_id: "cf-keypair-id",
private_key_path: "./cf_private_key.pem"
)
Shrine::Storage::S3.new(signer: signer.method(:signed_url))
# or
Shrine::Storage::S3.new(signer: -> (url, **options) { signer.signed_url(url, **options) })
The #presign
method can be used for generating paramters for direct uploads
to Amazon S3:
s3.presign("/path/to/file") #=>
# {
# url: "https://my-bucket.s3.amazonaws.com/...",
# fields: { ... }, # blank for PUT presigns
# headers: { ... }, # blank for POST presigns
# method: "post",
# }
Additional presign options can be given in three places:
- in
Storage::S3#presign
by forwarding options - in
:upload_options
option on this storage - in
presign_endpoint
plugin through:presign_options
The aws-sdk-s3 gem has the ability to automatically use multipart upload/copy for larger files, splitting the file into multiple chunks and uploading/copying them in parallel.
By default any files that are uploaded will use the multipart upload if they're
larger than 15MB, and any files that are copied will use the multipart copy if
they're larger than 150MB, but you can change the thresholds via
:multipart_threshold
.
thresholds = { upload: 30*1024*1024, copy: 200*1024*1024 }
Shrine::Storage::S3.new(multipart_threshold: thresholds, **s3_options)
If you want to change how many threads aws-sdk-s3 will use for multipart
upload/copy, you can use the upload_options
plugin to specify
:thread_count
.
plugin :upload_options, store: -> (io, context) do
{ thread_count: 5 }
end
The easiest way to use server-side encryption for uploaded S3 objects is to configure default encryption for your S3 bucket. Alternatively, you can pass server-side encryption parameters to the API calls.
The #upload
method accepts :sse_*
options:
s3.upload(io, "key", sse_customer_algorithm: "AES256",
sse_customer_key: "secret_key",
sse_customer_key_md5: "secret_key_md5",
ssekms_key_id: "key_id")
The #presign
method accepts :server_side_encryption_*
options for POST
presigns, and the same :sse_*
options as above for PUT presigns.
s3.presign("key", server_side_encryption_customer_algorithm: "AES256",
server_side_encryption_customer_key: "secret_key",
server_side_encryption_aws_kms_key_id: "key_id")
When downloading encrypted S3 objects, the same server-side encryption parameters need to be passed in.
s3.download("key", sse_customer_algorithm: "AES256",
sse_customer_key: "secret_key",
sse_customer_key_md5: "secret_key_md5")
s3.open("key", sse_customer_algorithm: "AES256",
sse_customer_key: "secret_key",
sse_customer_key_md5: "secret_key_md5")
If you want to use client-side encryption instead, you can instantiate the
storage with an Aws::S3::Encryption::Client
instance.
client = Aws::S3::Encryption::Client.new(
kms_key_id: "alias/my-key"
)
Shrine::Storage::S3(client: client, bucket: "my-bucket")
To use Amazon S3's Transfer Acceleration feature, set
:use_accelerate_endpoint
to true
when initializing the storage:
Shrine::Storage::S3.new(use_accelerate_enpdoint: true, **other_options)
If you're using S3 as a cache, you will probably want to periodically delete old files which aren't used anymore. S3 has a built-in way to do this, read this article for instructions.
Alternatively you can periodically call the #clear!
method:
# deletes all objects that were uploaded more than 7 days ago
s3.clear! { |object| object.last_modified < Time.now - 7*24*60*60 }
Amazon S3 automatically scales to high request rates. For example, your application can achieve at least 3,500 PUT/POST/DELETE and 5,500 GET requests per second per prefix in a bucket (a prefix is a top-level "directory" in the bucket). If your app needs to support higher request rates to S3 than that, you can scale exponentially by using more prefixes.