shrine 2.19.3 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +523 -41
- data/LICENSE.txt +1 -1
- data/README.md +83 -979
- data/doc/advantages.md +231 -204
- data/doc/attacher.md +304 -153
- data/doc/carrierwave.md +297 -226
- data/doc/changing_derivatives.md +308 -0
- data/doc/changing_location.md +103 -21
- data/doc/changing_storage.md +110 -0
- data/doc/creating_persistence_plugins.md +132 -0
- data/doc/creating_plugins.md +43 -23
- data/doc/creating_storages.md +19 -5
- data/doc/design.md +147 -97
- data/doc/direct_s3.md +38 -28
- data/doc/external/articles.md +63 -0
- data/doc/external/extensions.md +53 -0
- data/doc/external/misc.md +32 -0
- data/doc/getting_started.md +1156 -0
- data/doc/metadata.md +190 -109
- data/doc/multiple_files.md +93 -30
- data/doc/paperclip.md +384 -262
- data/doc/plugins/activerecord.md +177 -46
- data/doc/plugins/add_metadata.md +139 -38
- data/doc/plugins/atomic_helpers.md +217 -0
- data/doc/plugins/backgrounding.md +156 -98
- data/doc/plugins/cached_attachment_data.md +7 -5
- data/doc/plugins/column.md +121 -0
- data/doc/plugins/data_uri.md +23 -22
- data/doc/plugins/default_storage.md +36 -10
- data/doc/plugins/default_url.md +30 -13
- data/doc/plugins/delete_raw.md +4 -2
- data/doc/plugins/derivation_endpoint.md +186 -101
- data/doc/plugins/derivatives.md +839 -0
- data/doc/plugins/determine_mime_type.md +4 -2
- data/doc/plugins/download_endpoint.md +64 -8
- data/doc/plugins/dynamic_storage.md +5 -3
- data/doc/plugins/entity.md +263 -0
- data/doc/plugins/form_assign.md +55 -0
- data/doc/plugins/included.md +31 -8
- data/doc/plugins/infer_extension.md +21 -10
- data/doc/plugins/instrumentation.md +38 -16
- data/doc/plugins/keep_files.md +16 -17
- data/doc/plugins/metadata_attributes.md +42 -13
- data/doc/plugins/mirroring.md +118 -0
- data/doc/plugins/model.md +210 -0
- data/doc/plugins/module_include.md +4 -2
- data/doc/plugins/multi_cache.md +24 -0
- data/doc/plugins/persistence.md +101 -0
- data/doc/plugins/presign_endpoint.md +9 -4
- data/doc/plugins/pretty_location.md +16 -3
- data/doc/plugins/processing.md +4 -2
- data/doc/plugins/rack_file.md +8 -2
- data/doc/plugins/rack_response.md +6 -2
- data/doc/plugins/recache.md +4 -2
- data/doc/plugins/refresh_metadata.md +49 -9
- data/doc/plugins/remote_url.md +84 -47
- data/doc/plugins/remove_attachment.md +27 -6
- data/doc/plugins/remove_invalid.md +21 -6
- data/doc/plugins/restore_cached_data.md +11 -3
- data/doc/plugins/sequel.md +159 -35
- data/doc/plugins/signature.md +16 -5
- data/doc/plugins/store_dimensions.md +14 -2
- data/doc/plugins/tempfile.md +4 -2
- data/doc/plugins/type_predicates.md +96 -0
- data/doc/plugins/upload_endpoint.md +13 -13
- data/doc/plugins/upload_options.md +6 -4
- data/doc/plugins/{default_url_options.md → url_options.md} +9 -7
- data/doc/plugins/validation.md +97 -0
- data/doc/plugins/validation_helpers.md +16 -13
- data/doc/plugins/versions.md +15 -19
- data/doc/processing.md +438 -221
- data/doc/refile.md +188 -170
- data/doc/release_notes/1.0.0.md +4 -0
- data/doc/release_notes/1.1.0.md +6 -2
- data/doc/release_notes/1.2.0.md +4 -0
- data/doc/release_notes/1.3.0.md +4 -0
- data/doc/release_notes/1.4.0.md +4 -0
- data/doc/release_notes/1.4.1.md +4 -0
- data/doc/release_notes/1.4.2.md +4 -0
- data/doc/release_notes/2.0.0.md +4 -0
- data/doc/release_notes/2.0.1.md +4 -0
- data/doc/release_notes/2.1.0.md +5 -1
- data/doc/release_notes/2.1.1.md +4 -0
- data/doc/release_notes/2.10.0.md +4 -0
- data/doc/release_notes/2.10.1.md +4 -0
- data/doc/release_notes/2.11.0.md +4 -0
- data/doc/release_notes/2.12.0.md +4 -0
- data/doc/release_notes/2.13.0.md +4 -0
- data/doc/release_notes/2.14.0.md +5 -1
- data/doc/release_notes/2.15.0.md +11 -7
- data/doc/release_notes/2.16.0.md +4 -0
- data/doc/release_notes/2.17.0.md +4 -0
- data/doc/release_notes/2.18.0.md +4 -0
- data/doc/release_notes/2.19.0.md +6 -3
- data/doc/release_notes/2.2.0.md +4 -0
- data/doc/release_notes/2.3.0.md +4 -0
- data/doc/release_notes/2.3.1.md +4 -0
- data/doc/release_notes/2.4.0.md +4 -0
- data/doc/release_notes/2.4.1.md +4 -0
- data/doc/release_notes/2.5.0.md +4 -0
- data/doc/release_notes/2.6.0.md +4 -0
- data/doc/release_notes/2.6.1.md +4 -0
- data/doc/release_notes/2.7.0.md +4 -0
- data/doc/release_notes/2.8.0.md +4 -0
- data/doc/release_notes/2.9.0.md +4 -0
- data/doc/release_notes/3.0.0.md +981 -0
- data/doc/release_notes/3.0.1.md +22 -0
- data/doc/release_notes/3.1.0.md +73 -0
- data/doc/release_notes/3.2.0.md +96 -0
- data/doc/release_notes/3.2.1.md +31 -0
- data/doc/release_notes/3.2.2.md +14 -0
- data/doc/release_notes/3.3.0.md +105 -0
- data/doc/release_notes/3.4.0.md +35 -0
- data/doc/release_notes/3.5.0.md +63 -0
- data/doc/release_notes/3.6.0.md +23 -0
- data/doc/retrieving_uploads.md +5 -2
- data/doc/securing_uploads.md +60 -37
- data/doc/storage/file_system.md +20 -3
- data/doc/storage/memory.md +19 -0
- data/doc/storage/s3.md +122 -78
- data/doc/testing.md +141 -133
- data/doc/upgrading_to_3.md +708 -0
- data/doc/validation.md +54 -90
- data/lib/shrine/attacher.rb +292 -169
- data/lib/shrine/attachment.rb +13 -46
- data/lib/shrine/plugins/_persistence.rb +93 -0
- data/lib/shrine/plugins/activerecord.rb +77 -34
- data/lib/shrine/plugins/add_metadata.rb +25 -17
- data/lib/shrine/plugins/atomic_helpers.rb +119 -0
- data/lib/shrine/plugins/backgrounding.rb +77 -113
- data/lib/shrine/plugins/cached_attachment_data.rb +6 -15
- data/lib/shrine/plugins/column.rb +102 -0
- data/lib/shrine/plugins/data_uri.rb +38 -36
- data/lib/shrine/plugins/default_storage.rb +45 -15
- data/lib/shrine/plugins/default_url.rb +12 -24
- data/lib/shrine/plugins/default_url_options.rb +3 -30
- data/lib/shrine/plugins/delete_raw.rb +10 -16
- data/lib/shrine/plugins/derivation_endpoint.rb +130 -171
- data/lib/shrine/plugins/derivatives.rb +645 -0
- data/lib/shrine/plugins/determine_mime_type.rb +9 -21
- data/lib/shrine/plugins/download_endpoint.rb +118 -133
- data/lib/shrine/plugins/dynamic_storage.rb +5 -11
- data/lib/shrine/plugins/entity.rb +158 -0
- data/lib/shrine/plugins/form_assign.rb +108 -0
- data/lib/shrine/plugins/included.rb +6 -6
- data/lib/shrine/plugins/infer_extension.rb +17 -20
- data/lib/shrine/plugins/instrumentation.rb +59 -43
- data/lib/shrine/plugins/keep_files.rb +3 -15
- data/lib/shrine/plugins/metadata_attributes.rb +28 -19
- data/lib/shrine/plugins/mirroring.rb +142 -0
- data/lib/shrine/plugins/model.rb +160 -0
- data/lib/shrine/plugins/module_include.rb +3 -3
- data/lib/shrine/plugins/multi_cache.rb +27 -0
- data/lib/shrine/plugins/presign_endpoint.rb +27 -28
- data/lib/shrine/plugins/pretty_location.rb +15 -9
- data/lib/shrine/plugins/processing.rb +22 -9
- data/lib/shrine/plugins/rack_file.rb +2 -42
- data/lib/shrine/plugins/rack_response.rb +21 -10
- data/lib/shrine/plugins/recache.rb +6 -5
- data/lib/shrine/plugins/refresh_metadata.rb +13 -11
- data/lib/shrine/plugins/remote_url.rb +49 -49
- data/lib/shrine/plugins/remove_attachment.rb +12 -6
- data/lib/shrine/plugins/remove_invalid.rb +19 -8
- data/lib/shrine/plugins/restore_cached_data.rb +13 -7
- data/lib/shrine/plugins/sequel.rb +86 -36
- data/lib/shrine/plugins/signature.rb +10 -16
- data/lib/shrine/plugins/store_dimensions.rb +35 -40
- data/lib/shrine/plugins/tempfile.rb +1 -3
- data/lib/shrine/plugins/type_predicates.rb +113 -0
- data/lib/shrine/plugins/upload_endpoint.rb +28 -24
- data/lib/shrine/plugins/upload_options.rb +14 -15
- data/lib/shrine/plugins/url_options.rb +31 -0
- data/lib/shrine/plugins/validation.rb +80 -0
- data/lib/shrine/plugins/validation_helpers.rb +35 -58
- data/lib/shrine/plugins/versions.rb +107 -87
- data/lib/shrine/plugins.rb +22 -0
- data/lib/shrine/storage/file_system.rb +46 -64
- data/lib/shrine/storage/linter.rb +42 -7
- data/lib/shrine/storage/memory.rb +49 -0
- data/lib/shrine/storage/s3.rb +173 -160
- data/lib/shrine/uploaded_file.rb +32 -32
- data/lib/shrine/version.rb +3 -3
- data/lib/shrine.rb +87 -150
- data/shrine.gemspec +11 -12
- metadata +92 -82
- data/doc/migrating_storage.md +0 -76
- data/doc/plugins/backup.md +0 -31
- data/doc/plugins/copy.md +0 -24
- data/doc/plugins/delete_promoted.md +0 -12
- data/doc/plugins/direct_upload.md +0 -172
- data/doc/plugins/hooks.md +0 -58
- data/doc/plugins/logging.md +0 -42
- data/doc/plugins/migration_helpers.md +0 -60
- data/doc/plugins/moving.md +0 -19
- data/doc/plugins/multi_delete.md +0 -20
- data/doc/plugins/parallelize.md +0 -16
- data/doc/plugins/parsed_json.md +0 -23
- data/doc/regenerating_versions.md +0 -143
- data/lib/shrine/plugins/background_helpers.rb +0 -5
- data/lib/shrine/plugins/backup.rb +0 -90
- data/lib/shrine/plugins/copy.rb +0 -50
- data/lib/shrine/plugins/delete_promoted.rb +0 -20
- data/lib/shrine/plugins/direct_upload.rb +0 -217
- data/lib/shrine/plugins/hooks.rb +0 -90
- data/lib/shrine/plugins/logging.rb +0 -142
- data/lib/shrine/plugins/migration_helpers.rb +0 -70
- data/lib/shrine/plugins/moving.rb +0 -57
- data/lib/shrine/plugins/multi_delete.rb +0 -32
- data/lib/shrine/plugins/parallelize.rb +0 -78
- data/lib/shrine/plugins/parsed_json.rb +0 -29
data/lib/shrine/storage/s3.rb
CHANGED
|
@@ -1,34 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
gem "aws-sdk-s3", "~> 1.14"
|
|
4
|
+
|
|
3
5
|
require "shrine"
|
|
4
|
-
|
|
5
|
-
require "aws-sdk-s3"
|
|
6
|
-
if Gem::Version.new(Aws::S3::GEM_VERSION) < Gem::Version.new("1.2.0")
|
|
7
|
-
raise "Shrine::Storage::S3 requires aws-sdk-s3 version 1.2.0 or above"
|
|
8
|
-
elsif Gem::Version.new(Aws::S3::GEM_VERSION) < Gem::Version.new("1.14.0")
|
|
9
|
-
Shrine.deprecation("Using aws-sdk-s3 < 1.14 is deprecated and support for it will be removed in Shrine 3. Update to aws-sdk-s3 version 1.14 or higher")
|
|
10
|
-
end
|
|
11
|
-
rescue LoadError => exception
|
|
12
|
-
begin
|
|
13
|
-
require "aws-sdk"
|
|
14
|
-
Shrine.deprecation("Using aws-sdk 2.x is deprecated and support for it will be removed in Shrine 3. Use the new aws-sdk-s3 gem instead.")
|
|
15
|
-
Aws.eager_autoload!(services: ["S3"])
|
|
16
|
-
rescue LoadError
|
|
17
|
-
raise exception
|
|
18
|
-
end
|
|
19
|
-
end
|
|
6
|
+
require "aws-sdk-s3"
|
|
20
7
|
|
|
21
8
|
require "down/chunked_io"
|
|
22
9
|
require "content_disposition"
|
|
23
10
|
|
|
24
11
|
require "uri"
|
|
25
|
-
require "cgi"
|
|
26
12
|
require "tempfile"
|
|
27
13
|
|
|
28
14
|
class Shrine
|
|
29
15
|
module Storage
|
|
30
16
|
class S3
|
|
31
|
-
attr_reader :client, :bucket, :prefix, :
|
|
17
|
+
attr_reader :client, :bucket, :prefix, :upload_options, :copy_options, :signer, :public
|
|
18
|
+
|
|
19
|
+
MAX_MULTIPART_PARTS = 10_000
|
|
20
|
+
MIN_PART_SIZE = 5*1024*1024
|
|
21
|
+
|
|
22
|
+
MULTIPART_THRESHOLD = { upload: 15*1024*1024, copy: 100*1024*1024 }
|
|
23
|
+
|
|
24
|
+
COPY_OPTIONS = { tagging_directive: "REPLACE" }
|
|
32
25
|
|
|
33
26
|
# Initializes a storage for uploading to S3. All options are forwarded to
|
|
34
27
|
# [`Aws::S3::Client#initialize`], except the following:
|
|
@@ -50,12 +43,20 @@ class Shrine
|
|
|
50
43
|
# be passed to [`Aws::S3::Object#put`], [`Aws::S3::Object#copy_from`]
|
|
51
44
|
# and [`Aws::S3::Bucket#presigned_post`].
|
|
52
45
|
#
|
|
46
|
+
# :copy_options
|
|
47
|
+
# : Additional options that will be used for copying files, they will
|
|
48
|
+
# be passed to [`Aws::S3::Object#copy_from`].
|
|
49
|
+
#
|
|
53
50
|
# :multipart_threshold
|
|
54
51
|
# : If the input file is larger than the specified size, a parallelized
|
|
55
52
|
# multipart will be used for the upload/copy. Defaults to
|
|
56
53
|
# `{upload: 15*1024*1024, copy: 100*1024*1024}` (15MB for upload
|
|
57
54
|
# requests, 100MB for copy requests).
|
|
58
55
|
#
|
|
56
|
+
# :max_multipart_parts
|
|
57
|
+
# : Limits the number of parts if parellized multipart upload/copy is used.
|
|
58
|
+
# Defaults to 10_000.
|
|
59
|
+
#
|
|
59
60
|
# In addition to specifying the `:bucket`, you'll also need to provide
|
|
60
61
|
# AWS credentials. The most common way is to provide them directly via
|
|
61
62
|
# `:access_key_id`, `:secret_access_key`, and `:region` options. But you
|
|
@@ -67,33 +68,20 @@ class Shrine
|
|
|
67
68
|
# [`Aws::S3::Bucket#presigned_post`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_post-instance_method
|
|
68
69
|
# [`Aws::S3::Client#initialize`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#initialize-instance_method
|
|
69
70
|
# [configuring AWS SDK]: https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/setup-config.html
|
|
70
|
-
def initialize(bucket:, client: nil, prefix: nil,
|
|
71
|
+
def initialize(bucket:, client: nil, prefix: nil, upload_options: {}, multipart_threshold: {}, max_multipart_parts: nil, signer: nil, public: nil, copy_options: COPY_OPTIONS, **s3_options)
|
|
71
72
|
raise ArgumentError, "the :bucket option is nil" unless bucket
|
|
72
73
|
|
|
73
|
-
Shrine.deprecation("The :host option to Shrine::Storage::S3#initialize is deprecated and will be removed in Shrine 3. Pass :host to S3#url instead, you can also use default_url_options plugin.") if host
|
|
74
|
-
|
|
75
|
-
if multipart_threshold.is_a?(Integer)
|
|
76
|
-
Shrine.deprecation("Accepting the :multipart_threshold S3 option as an integer is deprecated, use a hash with :upload and :copy keys instead, e.g. {upload: 15*1024*1024, copy: 150*1024*1024}")
|
|
77
|
-
multipart_threshold = { upload: multipart_threshold }
|
|
78
|
-
end
|
|
79
|
-
multipart_threshold = { upload: 15*1024*1024, copy: 100*1024*1024 }.merge(multipart_threshold)
|
|
80
|
-
|
|
81
74
|
@client = client || Aws::S3::Client.new(**s3_options)
|
|
82
75
|
@bucket = Aws::S3::Bucket.new(name: bucket, client: @client)
|
|
83
76
|
@prefix = prefix
|
|
84
|
-
@host = host
|
|
85
77
|
@upload_options = upload_options
|
|
86
|
-
@
|
|
78
|
+
@copy_options = copy_options
|
|
79
|
+
@multipart_threshold = MULTIPART_THRESHOLD.merge(multipart_threshold)
|
|
80
|
+
@max_multipart_parts = max_multipart_parts || MAX_MULTIPART_PARTS
|
|
87
81
|
@signer = signer
|
|
88
82
|
@public = public
|
|
89
83
|
end
|
|
90
84
|
|
|
91
|
-
# Returns an `Aws::S3::Resource` object.
|
|
92
|
-
def s3
|
|
93
|
-
Shrine.deprecation("Shrine::Storage::S3#s3 that returns an Aws::S3::Resource is deprecated, use Shrine::Storage::S3#client which returns an Aws::S3::Client object.")
|
|
94
|
-
Aws::S3::Resource.new(client: @client)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
85
|
# If the file is an UploadedFile from S3, issues a COPY command, otherwise
|
|
98
86
|
# uploads the file. For files larger than `:multipart_threshold` a
|
|
99
87
|
# multipart upload/copy will be used for better performance and more
|
|
@@ -112,8 +100,6 @@ class Shrine
|
|
|
112
100
|
options.merge!(@upload_options)
|
|
113
101
|
options.merge!(upload_options)
|
|
114
102
|
|
|
115
|
-
options[:content_disposition] = encode_content_disposition(options[:content_disposition]) if options[:content_disposition]
|
|
116
|
-
|
|
117
103
|
if copyable?(io)
|
|
118
104
|
copy(io, id, **options)
|
|
119
105
|
else
|
|
@@ -124,22 +110,18 @@ class Shrine
|
|
|
124
110
|
# Returns a `Down::ChunkedIO` object that downloads S3 object content
|
|
125
111
|
# on-demand. By default, read content will be cached onto disk so that
|
|
126
112
|
# it can be rewinded, but if you don't need that you can pass
|
|
127
|
-
# `rewindable: false`.
|
|
113
|
+
# `rewindable: false`. A required character encoding can be passed in
|
|
114
|
+
# `encoding`; the default is `Encoding::BINARY` via `Down::ChunkedIO`.
|
|
128
115
|
#
|
|
129
116
|
# Any additional options are forwarded to [`Aws::S3::Object#get`].
|
|
130
117
|
#
|
|
131
118
|
# [`Aws::S3::Object#get`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#get-instance_method
|
|
132
|
-
def open(id, rewindable: true, **options)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
load_data(object, **options)
|
|
119
|
+
def open(id, rewindable: true, encoding: nil, **options)
|
|
120
|
+
chunks, length = get(id, **options)
|
|
136
121
|
|
|
137
|
-
Down::ChunkedIO.new(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
size: object.content_length,
|
|
141
|
-
data: { object: object },
|
|
142
|
-
)
|
|
122
|
+
Down::ChunkedIO.new(chunks: chunks, rewindable: rewindable, size: length, encoding: encoding)
|
|
123
|
+
rescue Aws::S3::Errors::NoSuchKey
|
|
124
|
+
raise Shrine::FileNotFound, "file #{id.inspect} not found on storage"
|
|
143
125
|
end
|
|
144
126
|
|
|
145
127
|
# Returns true file exists on S3.
|
|
@@ -163,13 +145,7 @@ class Shrine
|
|
|
163
145
|
#
|
|
164
146
|
# [`Aws::S3::Object#presigned_url`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_url-instance_method
|
|
165
147
|
# [`Aws::S3::Object#public_url`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#public_url-instance_method
|
|
166
|
-
def url(id,
|
|
167
|
-
if download
|
|
168
|
-
Shrine.deprecation("The :download option in Shrine::Storage::S3#url is deprecated and will be removed in Shrine 3. Use the :response_content_disposition option directly, e.g. `response_content_disposition: \"attachment\"`.")
|
|
169
|
-
options[:response_content_disposition] ||= "attachment"
|
|
170
|
-
end
|
|
171
|
-
options[:response_content_disposition] = encode_content_disposition(options[:response_content_disposition]) if options[:response_content_disposition]
|
|
172
|
-
|
|
148
|
+
def url(id, public: self.public, host: nil, **options)
|
|
173
149
|
if public || signer
|
|
174
150
|
url = object(id).public_url(**options)
|
|
175
151
|
else
|
|
@@ -217,27 +193,7 @@ class Shrine
|
|
|
217
193
|
options.merge!(@upload_options)
|
|
218
194
|
options.merge!(presign_options)
|
|
219
195
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if method == :post
|
|
223
|
-
presigned_post = object(id).presigned_post(options)
|
|
224
|
-
|
|
225
|
-
Struct.new(:method, :url, :fields).new(method, presigned_post.url, presigned_post.fields)
|
|
226
|
-
else
|
|
227
|
-
url = object(id).presigned_url(method, options)
|
|
228
|
-
|
|
229
|
-
# When any of these options are specified, the corresponding request
|
|
230
|
-
# headers must be included in the upload request.
|
|
231
|
-
headers = {}
|
|
232
|
-
headers["Content-Length"] = options[:content_length] if options[:content_length]
|
|
233
|
-
headers["Content-Type"] = options[:content_type] if options[:content_type]
|
|
234
|
-
headers["Content-Disposition"] = options[:content_disposition] if options[:content_disposition]
|
|
235
|
-
headers["Content-Encoding"] = options[:content_encoding] if options[:content_encoding]
|
|
236
|
-
headers["Content-Language"] = options[:content_language] if options[:content_language]
|
|
237
|
-
headers["Content-MD5"] = options[:content_md5] if options[:content_md5]
|
|
238
|
-
|
|
239
|
-
{ method: method, url: url, headers: headers }
|
|
240
|
-
end
|
|
196
|
+
send(:"presign_#{method}", id, options)
|
|
241
197
|
end
|
|
242
198
|
|
|
243
199
|
# Deletes the file from the storage.
|
|
@@ -245,6 +201,16 @@ class Shrine
|
|
|
245
201
|
object(id).delete
|
|
246
202
|
end
|
|
247
203
|
|
|
204
|
+
# Deletes objects at keys starting with the specified prefix.
|
|
205
|
+
#
|
|
206
|
+
# s3.delete_prefixed("somekey/derivatives/")
|
|
207
|
+
def delete_prefixed(delete_prefix)
|
|
208
|
+
# We need to make sure to combine with storage prefix, and
|
|
209
|
+
# that it ends in '/' cause S3 can be squirrely about matching interior.
|
|
210
|
+
delete_prefix = delete_prefix.chomp("/") + "/"
|
|
211
|
+
bucket.objects(prefix: [*prefix, delete_prefix].join("/")).batch_delete!
|
|
212
|
+
end
|
|
213
|
+
|
|
248
214
|
# If block is given, deletes all objects from the storage for which the
|
|
249
215
|
# block evaluates to true. Otherwise deletes all objects from the storage.
|
|
250
216
|
#
|
|
@@ -260,76 +226,109 @@ class Shrine
|
|
|
260
226
|
|
|
261
227
|
# Returns an `Aws::S3::Object` for the given id.
|
|
262
228
|
def object(id)
|
|
263
|
-
bucket.object(
|
|
229
|
+
bucket.object(object_key(id))
|
|
264
230
|
end
|
|
265
231
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
232
|
+
private
|
|
233
|
+
|
|
234
|
+
# Uploads the file to S3. Uses multipart upload for large files.
|
|
235
|
+
def put(io, id, **options)
|
|
236
|
+
if io.respond_to?(:size) && io.size && io.size <= @multipart_threshold[:upload]
|
|
237
|
+
object(id).put(body: io, **options)
|
|
238
|
+
else # multipart upload
|
|
239
|
+
object(id).upload_stream(part_size: part_size(io), **options) do |write_stream|
|
|
240
|
+
IO.copy_stream(io, write_stream)
|
|
241
|
+
end
|
|
273
242
|
end
|
|
274
243
|
end
|
|
275
244
|
|
|
276
|
-
private
|
|
277
|
-
|
|
278
245
|
# Copies an existing S3 object to a new location. Uses multipart copy for
|
|
279
246
|
# large files.
|
|
280
|
-
def copy(io, id, **
|
|
281
|
-
#
|
|
282
|
-
options = {
|
|
247
|
+
def copy(io, id, **copy_options)
|
|
248
|
+
# don't inherit source object metadata or AWS tags
|
|
249
|
+
options = {
|
|
250
|
+
metadata_directive: "REPLACE",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if io.size && io.size >= @multipart_threshold[:copy]
|
|
254
|
+
# pass :content_length on multipart copy to avoid an additional HEAD request
|
|
255
|
+
options.merge!(multipart_copy: true, content_length: io.size)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
options.merge!(@copy_options)
|
|
259
|
+
options.merge!(copy_options)
|
|
260
|
+
|
|
283
261
|
object(id).copy_from(io.storage.object(io.id), **options)
|
|
284
262
|
end
|
|
285
263
|
|
|
286
|
-
#
|
|
287
|
-
def
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
object(id).upload_file(path, **options)
|
|
292
|
-
else
|
|
293
|
-
io.to_io if io.is_a?(UploadedFile) # open if not already opened
|
|
294
|
-
|
|
295
|
-
if io.respond_to?(:size) && io.size && (io.size <= @multipart_threshold[:upload] || !object(id).respond_to?(:upload_stream))
|
|
296
|
-
object(id).put(body: io, **options)
|
|
297
|
-
elsif object(id).respond_to?(:upload_stream)
|
|
298
|
-
# `upload_stream` uses multipart upload
|
|
299
|
-
object(id).upload_stream(tempfile: true, **options) do |write_stream|
|
|
300
|
-
IO.copy_stream(io, write_stream)
|
|
301
|
-
end
|
|
302
|
-
else
|
|
303
|
-
Tempfile.create("shrine-s3", binmode: true) do |file|
|
|
304
|
-
IO.copy_stream(io, file.path)
|
|
305
|
-
object(id).upload_file(file.path, **options)
|
|
306
|
-
end
|
|
307
|
-
end
|
|
308
|
-
end
|
|
264
|
+
# Generates parameters for a POST upload request.
|
|
265
|
+
def presign_post(id, options)
|
|
266
|
+
presigned_post = object(id).presigned_post(options)
|
|
267
|
+
|
|
268
|
+
{ method: :post, url: presigned_post.url, fields: presigned_post.fields }
|
|
309
269
|
end
|
|
310
270
|
|
|
311
|
-
#
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
271
|
+
# Generates parameters for a PUT upload request.
|
|
272
|
+
def presign_put(id, options)
|
|
273
|
+
url = object(id).presigned_url(:put, options)
|
|
274
|
+
|
|
275
|
+
# When any of these options are specified, the corresponding request
|
|
276
|
+
# headers must be included in the upload request.
|
|
277
|
+
headers = {}
|
|
278
|
+
headers["Content-Length"] = options[:content_length] if options[:content_length]
|
|
279
|
+
headers["Content-Type"] = options[:content_type] if options[:content_type]
|
|
280
|
+
headers["Content-Disposition"] = options[:content_disposition] if options[:content_disposition]
|
|
281
|
+
headers["Content-Encoding"] = options[:content_encoding] if options[:content_encoding]
|
|
282
|
+
headers["Content-Language"] = options[:content_language] if options[:content_language]
|
|
283
|
+
headers["Content-MD5"] = options[:content_md5] if options[:content_md5]
|
|
284
|
+
|
|
285
|
+
{ method: :put, url: url, headers: headers }
|
|
286
|
+
end
|
|
318
287
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
)
|
|
288
|
+
# Determins the part size that should be used when uploading the given IO
|
|
289
|
+
# object via multipart upload.
|
|
290
|
+
def part_size(io)
|
|
291
|
+
return unless io.respond_to?(:size) && io.size
|
|
324
292
|
|
|
325
|
-
|
|
293
|
+
if io.size <= MIN_PART_SIZE * @max_multipart_parts # <= 50 GB
|
|
294
|
+
MIN_PART_SIZE
|
|
295
|
+
else # > 50 GB
|
|
296
|
+
(io.size.to_f / @max_multipart_parts).ceil
|
|
297
|
+
end
|
|
326
298
|
end
|
|
327
299
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
300
|
+
# Aws::S3::Object#get doesn't allow us to get the content length of the
|
|
301
|
+
# object before all content is downloaded, so we hack our way around it.
|
|
302
|
+
# This way get the content length without an additional HEAD request.
|
|
303
|
+
if Gem::Version.new(Aws::CORE_GEM_VERSION) >= Gem::Version.new("3.104.0")
|
|
304
|
+
def get(id, **params)
|
|
305
|
+
enum = object(id).enum_for(:get, **params)
|
|
306
|
+
|
|
307
|
+
begin
|
|
308
|
+
content_length = Integer(enum.peek.last["content-length"])
|
|
309
|
+
rescue StopIteration
|
|
310
|
+
content_length = 0
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
chunks = Enumerator.new { |y| loop { y << enum.next.first } }
|
|
314
|
+
|
|
315
|
+
[chunks, content_length]
|
|
316
|
+
end
|
|
317
|
+
else
|
|
318
|
+
def get(id, **params)
|
|
319
|
+
req = client.build_request(:get_object, bucket: bucket.name, key: object_key(id), **params)
|
|
320
|
+
|
|
321
|
+
body = req.enum_for(:send_request)
|
|
322
|
+
begin
|
|
323
|
+
body.peek # start the request
|
|
324
|
+
rescue StopIteration
|
|
325
|
+
# the S3 object is empty
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
content_length = Integer(req.context.http_response.headers["Content-Length"])
|
|
329
|
+
chunks = Enumerator.new { |y| loop { y << body.next } }
|
|
330
|
+
|
|
331
|
+
[chunks, content_length]
|
|
333
332
|
end
|
|
334
333
|
end
|
|
335
334
|
|
|
@@ -349,42 +348,56 @@ class Shrine
|
|
|
349
348
|
end
|
|
350
349
|
end
|
|
351
350
|
|
|
352
|
-
#
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
def encode_content_disposition(content_disposition)
|
|
356
|
-
content_disposition.sub(/(?<=filename=").+(?=")/) do |filename|
|
|
357
|
-
if filename =~ /[^[:ascii:]]/
|
|
358
|
-
Shrine.deprecation("Shrine::Storage::S3 will not escape characters in the filename for Content-Disposition header in Shrine 3. Use the content_disposition gem, for example `ContentDisposition.format(disposition: 'inline', filename: '...')`.")
|
|
359
|
-
CGI.escape(filename).gsub("+", " ")
|
|
360
|
-
else
|
|
361
|
-
filename
|
|
362
|
-
end
|
|
363
|
-
end
|
|
351
|
+
# Returns object key with potential prefix.
|
|
352
|
+
def object_key(id)
|
|
353
|
+
[*prefix, id].join("/")
|
|
364
354
|
end
|
|
365
355
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
object.get { |chunk| yield chunk, object.content_length }
|
|
370
|
-
end
|
|
356
|
+
# Adds support for Aws::S3::Encryption::Client.
|
|
357
|
+
module ClientSideEncryption
|
|
358
|
+
attr_reader :encryption_client
|
|
371
359
|
|
|
372
|
-
|
|
373
|
-
|
|
360
|
+
# Save the encryption client and continue initialization with normal
|
|
361
|
+
# client.
|
|
362
|
+
def initialize(client: nil, **options)
|
|
363
|
+
return super unless client.class.name.start_with?("Aws::S3::Encryption")
|
|
374
364
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
tempfile.close! if tempfile
|
|
381
|
-
raise
|
|
382
|
-
end
|
|
365
|
+
super(client: client.client, **options)
|
|
366
|
+
@encryption_client = client
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
private
|
|
383
370
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
371
|
+
# Encryption client doesn't support multipart uploads, so we always use
|
|
372
|
+
# #put_object.
|
|
373
|
+
def put(io, id, **options)
|
|
374
|
+
return super unless encryption_client
|
|
375
|
+
|
|
376
|
+
encryption_client.put_object(body: io, bucket: bucket.name, key: object_key(id), **options)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def get(id, **options)
|
|
380
|
+
return super unless encryption_client
|
|
381
|
+
|
|
382
|
+
# Encryption client v2 warns against streaming download, so we first
|
|
383
|
+
# download all content into a file.
|
|
384
|
+
tempfile = Tempfile.new("shrine-s3", binmode: true)
|
|
385
|
+
response = encryption_client.get_object(response_target: tempfile, bucket: bucket.name, key: object_key(id), **options)
|
|
386
|
+
tempfile.rewind
|
|
387
|
+
|
|
388
|
+
chunks = Enumerator.new do |yielder|
|
|
389
|
+
begin
|
|
390
|
+
yielder << tempfile.read(16*1024) until tempfile.eof?
|
|
391
|
+
ensure
|
|
392
|
+
tempfile.close!
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
[chunks, tempfile.size]
|
|
397
|
+
end
|
|
387
398
|
end
|
|
399
|
+
|
|
400
|
+
prepend ClientSideEncryption
|
|
388
401
|
end
|
|
389
402
|
end
|
|
390
403
|
end
|
data/lib/shrine/uploaded_file.rb
CHANGED
|
@@ -6,7 +6,6 @@ require "uri"
|
|
|
6
6
|
|
|
7
7
|
class Shrine
|
|
8
8
|
# Core class that represents a file uploaded to a storage.
|
|
9
|
-
# Base implementation is defined in InstanceMethods and ClassMethods.
|
|
10
9
|
class UploadedFile
|
|
11
10
|
@shrine_class = ::Shrine
|
|
12
11
|
|
|
@@ -23,31 +22,24 @@ class Shrine
|
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
module InstanceMethods
|
|
26
|
-
# The
|
|
27
|
-
attr_reader :
|
|
25
|
+
# The location where the file was uploaded to the storage.
|
|
26
|
+
attr_reader :id
|
|
28
27
|
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
raise Error, "#{data.inspect} isn't valid uploaded file data" unless data["id"] && data["storage"]
|
|
28
|
+
# The identifier of the storage the file is uploaded to.
|
|
29
|
+
attr_reader :storage_key
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
storage # ensure storage is registered
|
|
36
|
-
end
|
|
31
|
+
# A hash of file metadata that was extracted during upload.
|
|
32
|
+
attr_reader :metadata
|
|
37
33
|
|
|
38
|
-
#
|
|
39
|
-
def
|
|
40
|
-
@data
|
|
41
|
-
|
|
34
|
+
# Initializes the uploaded file with the given data hash.
|
|
35
|
+
def initialize(data)
|
|
36
|
+
@id = data[:id] || data["id"]
|
|
37
|
+
@storage_key = data[:storage]&.to_sym || data["storage"]&.to_sym
|
|
38
|
+
@metadata = data[:metadata] || data["metadata"] || {}
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
def storage_key
|
|
45
|
-
@data.fetch("storage")
|
|
46
|
-
end
|
|
40
|
+
fail Error, "#{data.inspect} isn't valid uploaded file data" unless @id && @storage_key
|
|
47
41
|
|
|
48
|
-
|
|
49
|
-
def metadata
|
|
50
|
-
@data.fetch("metadata")
|
|
42
|
+
storage # ensure storage is registered
|
|
51
43
|
end
|
|
52
44
|
|
|
53
45
|
# The filename that was extracted from the uploaded file.
|
|
@@ -58,8 +50,9 @@ class Shrine
|
|
|
58
50
|
# The extension derived from #id if present, otherwise it's derived
|
|
59
51
|
# from #original_filename.
|
|
60
52
|
def extension
|
|
61
|
-
|
|
62
|
-
result.
|
|
53
|
+
identifier = id =~ URI::DEFAULT_PARSER.make_regexp ? id.sub(/\?.+$/, "") : id # strip query params for shrine-url
|
|
54
|
+
result = File.extname(identifier)[1..-1]
|
|
55
|
+
result ||= File.extname(original_filename.to_s)[1..-1]
|
|
63
56
|
result.downcase if result
|
|
64
57
|
end
|
|
65
58
|
|
|
@@ -177,6 +170,7 @@ class Shrine
|
|
|
177
170
|
# opened IO object.
|
|
178
171
|
def close
|
|
179
172
|
io.close if opened?
|
|
173
|
+
@io = nil
|
|
180
174
|
end
|
|
181
175
|
|
|
182
176
|
# Returns whether the file has already been opened.
|
|
@@ -196,8 +190,8 @@ class Shrine
|
|
|
196
190
|
end
|
|
197
191
|
|
|
198
192
|
# Uploads a new file to this file's location and returns it.
|
|
199
|
-
def replace(io,
|
|
200
|
-
uploader.upload(io,
|
|
193
|
+
def replace(io, **options)
|
|
194
|
+
uploader.upload(io, **options, location: id)
|
|
201
195
|
end
|
|
202
196
|
|
|
203
197
|
# Calls `#delete` on the storage, which deletes the file from the
|
|
@@ -222,11 +216,16 @@ class Shrine
|
|
|
222
216
|
data
|
|
223
217
|
end
|
|
224
218
|
|
|
219
|
+
# Returns serializable hash representation of the uploaded file.
|
|
220
|
+
def data
|
|
221
|
+
{ "id" => id, "storage" => storage_key.to_s, "metadata" => metadata }
|
|
222
|
+
end
|
|
223
|
+
|
|
225
224
|
# Returns true if the other UploadedFile is uploaded to the same
|
|
226
225
|
# storage and it has the same #id.
|
|
227
226
|
def ==(other)
|
|
228
|
-
|
|
229
|
-
self.id
|
|
227
|
+
self.class == other.class &&
|
|
228
|
+
self.id == other.id &&
|
|
230
229
|
self.storage_key == other.storage_key
|
|
231
230
|
end
|
|
232
231
|
alias eql? ==
|
|
@@ -251,6 +250,11 @@ class Shrine
|
|
|
251
250
|
self.class.shrine_class
|
|
252
251
|
end
|
|
253
252
|
|
|
253
|
+
# Returns simplified inspect output.
|
|
254
|
+
def inspect
|
|
255
|
+
"#<#{self.class.inspect} storage=#{storage_key.inspect} id=#{id.inspect} metadata=#{metadata.inspect}>"
|
|
256
|
+
end
|
|
257
|
+
|
|
254
258
|
private
|
|
255
259
|
|
|
256
260
|
# Returns an opened IO object for the uploaded file by calling `#open`
|
|
@@ -260,11 +264,7 @@ class Shrine
|
|
|
260
264
|
end
|
|
261
265
|
|
|
262
266
|
def _open(**options)
|
|
263
|
-
|
|
264
|
-
storage.open(id, **options)
|
|
265
|
-
else
|
|
266
|
-
storage.open(id) # some storage implementations might not accept additional arguments
|
|
267
|
-
end
|
|
267
|
+
storage.open(id, **options)
|
|
268
268
|
end
|
|
269
269
|
end
|
|
270
270
|
|