shrine 2.7.0 → 2.8.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of shrine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -1
- data/README.md +8 -0
- data/doc/direct_s3.md +8 -0
- data/doc/regenerating_versions.md +1 -1
- data/lib/shrine.rb +14 -13
- data/lib/shrine/plugins/activerecord.rb +6 -6
- data/lib/shrine/plugins/default_url.rb +1 -1
- data/lib/shrine/plugins/determine_mime_type.rb +25 -20
- data/lib/shrine/plugins/direct_upload.rb +2 -1
- data/lib/shrine/plugins/download_endpoint.rb +11 -2
- data/lib/shrine/plugins/logging.rb +3 -3
- data/lib/shrine/plugins/metadata_attributes.rb +22 -11
- data/lib/shrine/plugins/presign_endpoint.rb +20 -12
- data/lib/shrine/plugins/rack_response.rb +83 -15
- data/lib/shrine/plugins/signature.rb +4 -14
- data/lib/shrine/plugins/upload_endpoint.rb +15 -13
- data/lib/shrine/storage/file_system.rb +2 -2
- data/lib/shrine/storage/s3.rb +65 -30
- data/lib/shrine/version.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b6ffc195c54c2afa0b36d184a94e44f5e8e110bb
|
4
|
+
data.tar.gz: '026928b32cef34807152c6999cee6e5f2f94ef81'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b520588785bf8fb1145500fe7a5819685d3a1ec00773c9bf124a7e4b8c0a6c4f5400e0389910741db02aaf58a591df263ba8e305f9112ed83bda997d6b6dbed5
|
7
|
+
data.tar.gz: a993d56594c2ea0b8c31f5c8bf84416358da9bdda4ede5c5353ba8be89d8cb5058f05dc97eedaf9a63efcf592c9354006fd4b402bc65399d989933aceb6e4a5c
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -762,6 +762,14 @@ step in the default synchronous workflow.
|
|
762
762
|
Shrine.plugin :upload_endpoint
|
763
763
|
```
|
764
764
|
```rb
|
765
|
+
# config.ru (Rack)
|
766
|
+
map "/images/upload" do
|
767
|
+
run ImageUploader.upload_endpoint(:cache)
|
768
|
+
end
|
769
|
+
|
770
|
+
# OR
|
771
|
+
|
772
|
+
# config/routes.rb (Rails)
|
765
773
|
Rails.application.routes.draw do
|
766
774
|
mount ImageUploader.upload_endpoint(:cache) => "/images/upload"
|
767
775
|
end
|
data/doc/direct_s3.md
CHANGED
@@ -94,6 +94,14 @@ route, so we just need to mount it in our application:
|
|
94
94
|
Shrine.plugin :presign_endpoint
|
95
95
|
```
|
96
96
|
```rb
|
97
|
+
# config.ru (Rack)
|
98
|
+
map "/presign" do
|
99
|
+
run Shrine.presign_endpoint(:cache)
|
100
|
+
end
|
101
|
+
|
102
|
+
# OR
|
103
|
+
|
104
|
+
# config/routes.rb (Rails)
|
97
105
|
Rails.application.routes.draw do
|
98
106
|
mount Shrine.presign_endpoint(:cache) => "/presign"
|
99
107
|
end
|
@@ -43,7 +43,7 @@ User.paged_each do |user|
|
|
43
43
|
if attacher.stored? && !attachment.is_a?(Hash)
|
44
44
|
file = some_processing(attachment.download)
|
45
45
|
thumb = attacher.store!(file, version: :thumb)
|
46
|
-
attacher.swap({original:
|
46
|
+
attacher.swap({original: attachment, thumb: thumb})
|
47
47
|
end
|
48
48
|
end
|
49
49
|
```
|
data/lib/shrine.rb
CHANGED
@@ -28,11 +28,11 @@ class Shrine
|
|
28
28
|
# Methods which an object has to respond to in order to be considered
|
29
29
|
# an IO object, along with their arguments.
|
30
30
|
IO_METHODS = {
|
31
|
-
:
|
32
|
-
|
33
|
-
:
|
34
|
-
:
|
35
|
-
:
|
31
|
+
read: [:length, :outbuf],
|
32
|
+
eof?: [],
|
33
|
+
rewind: [],
|
34
|
+
size: [],
|
35
|
+
close: [],
|
36
36
|
}
|
37
37
|
|
38
38
|
# Core class that represents a file uploaded to a storage. The instance
|
@@ -250,7 +250,7 @@ class Shrine
|
|
250
250
|
# location.
|
251
251
|
def generate_location(io, context = {})
|
252
252
|
extension = ".#{io.extension}" if io.is_a?(UploadedFile) && io.extension
|
253
|
-
extension ||= File.extname(extract_filename(io).to_s)
|
253
|
+
extension ||= File.extname(extract_filename(io).to_s).downcase
|
254
254
|
basename = generate_uid(io)
|
255
255
|
|
256
256
|
basename + extension.to_s
|
@@ -280,7 +280,7 @@ class Shrine
|
|
280
280
|
# Attempts to extract the MIME type from the IO object.
|
281
281
|
def extract_mime_type(io)
|
282
282
|
if io.respond_to?(:content_type)
|
283
|
-
warn "The \"mime_type\" Shrine metadata field will be set from the \"Content-Type\" request header, which might not hold the actual MIME type of the file. It is recommended to load the determine_mime_type plugin which determines MIME type from file content."
|
283
|
+
warn "The \"mime_type\" Shrine metadata field will be set from the \"Content-Type\" request header, which might not hold the actual MIME type of the file. It is recommended to load the determine_mime_type plugin which determines MIME type from file content."
|
284
284
|
io.content_type
|
285
285
|
end
|
286
286
|
end
|
@@ -760,7 +760,8 @@ class Shrine
|
|
760
760
|
# The extension derived from #id if present, otherwise from
|
761
761
|
# #original_filename.
|
762
762
|
def extension
|
763
|
-
File.extname(id)[1..-1] || File.extname(original_filename.to_s)[1..-1]
|
763
|
+
result = File.extname(id)[1..-1] || File.extname(original_filename.to_s)[1..-1]
|
764
|
+
result.downcase if result
|
764
765
|
end
|
765
766
|
|
766
767
|
# The filesize of the uploaded file.
|
@@ -781,8 +782,8 @@ class Shrine
|
|
781
782
|
# uploaded_file.open do |io|
|
782
783
|
# puts io.read # prints the content of the file
|
783
784
|
# end
|
784
|
-
def open
|
785
|
-
@io = storage.open(id)
|
785
|
+
def open(*args)
|
786
|
+
@io = storage.open(id, *args)
|
786
787
|
yield @io
|
787
788
|
ensure
|
788
789
|
@io.close if @io
|
@@ -791,12 +792,12 @@ class Shrine
|
|
791
792
|
|
792
793
|
# Calls `#download` on the storage if the storage implements it,
|
793
794
|
# otherwise uses #open to stream the underlying IO to a Tempfile.
|
794
|
-
def download
|
795
|
+
def download(*args)
|
795
796
|
if storage.respond_to?(:download)
|
796
|
-
storage.download(id)
|
797
|
+
storage.download(id, *args)
|
797
798
|
else
|
798
799
|
tempfile = Tempfile.new(["shrine", ".#{extension}"], binmode: true)
|
799
|
-
open { |io| IO.copy_stream(io, tempfile.path) }
|
800
|
+
open(*args) { |io| IO.copy_stream(io, tempfile.path) }
|
800
801
|
tempfile.tap(&:open)
|
801
802
|
end
|
802
803
|
end
|
@@ -58,13 +58,13 @@ class Shrine
|
|
58
58
|
# and options, which allows them to be internationalized together with
|
59
59
|
# other ActiveRecord validation messages.
|
60
60
|
#
|
61
|
-
#
|
62
|
-
#
|
61
|
+
# class MyUploader < Shrine
|
62
|
+
# plugin :validation_helpers
|
63
63
|
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
64
|
+
# Attacher.validate do
|
65
|
+
# validate_max_size 256 * 1024**2, message: ->(max) { [:max_size, max: max] }
|
66
|
+
# end
|
67
|
+
# end
|
68
68
|
#
|
69
69
|
# If you want to validate presence of the attachment, you can do it
|
70
70
|
# directly on the model.
|
@@ -50,7 +50,7 @@ class Shrine
|
|
50
50
|
if default_url_block
|
51
51
|
instance_exec(options, &default_url_block)
|
52
52
|
elsif shrine_class.opts[:default_url]
|
53
|
-
shrine_class.opts[:default_url].call(context.merge(options){|k,old,new|old})
|
53
|
+
shrine_class.opts[:default_url].call(context.merge(options){|k, old, new| old})
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
@@ -80,12 +80,16 @@ class Shrine
|
|
80
80
|
# Determines the MIME type of the IO object by calling the specified
|
81
81
|
# analyzer.
|
82
82
|
def determine_mime_type(io)
|
83
|
-
|
84
|
-
|
85
|
-
|
83
|
+
if opts[:mime_type_analyzer] == :default
|
84
|
+
mime_type = io.content_type if io.respond_to?(:content_type)
|
85
|
+
else
|
86
|
+
analyzer = opts[:mime_type_analyzer]
|
87
|
+
analyzer = mime_type_analyzers[analyzer] if analyzer.is_a?(Symbol)
|
88
|
+
args = [io, mime_type_analyzers].take(analyzer.arity.abs)
|
86
89
|
|
87
|
-
|
88
|
-
|
90
|
+
mime_type = analyzer.call(*args)
|
91
|
+
io.rewind
|
92
|
+
end
|
89
93
|
|
90
94
|
mime_type
|
91
95
|
end
|
@@ -144,29 +148,30 @@ class Shrine
|
|
144
148
|
def extract_with_file(io)
|
145
149
|
require "open3"
|
146
150
|
|
147
|
-
|
148
|
-
|
151
|
+
Open3.popen3(*%W[file --mime-type --brief -]) do |stdin, stdout, stderr, thread|
|
152
|
+
begin
|
153
|
+
IO.copy_stream(io, stdin.binmode)
|
154
|
+
rescue Errno::EPIPE
|
155
|
+
end
|
156
|
+
stdin.close
|
149
157
|
|
150
|
-
|
151
|
-
stdout, stderr, status = Open3.capture3(*cmd, options)
|
152
|
-
rescue Errno::ENOENT
|
153
|
-
raise Error, "The `file` command-line tool is not installed"
|
154
|
-
end
|
158
|
+
status = thread.value
|
155
159
|
|
156
|
-
|
157
|
-
|
160
|
+
raise Error, stderr.read unless status.success?
|
161
|
+
$stderr.print(stderr.read)
|
158
162
|
|
159
|
-
|
163
|
+
stdout.read.strip
|
164
|
+
end
|
165
|
+
rescue Errno::ENOENT
|
166
|
+
raise Error, "The `file` command-line tool is not installed"
|
160
167
|
end
|
161
168
|
|
162
169
|
def extract_with_filemagic(io)
|
163
170
|
require "filemagic"
|
164
171
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
mime_type
|
172
|
+
FileMagic.open(FileMagic::MAGIC_MIME_TYPE) do |filemagic|
|
173
|
+
filemagic.buffer(io.read(MAGIC_NUMBER))
|
174
|
+
end
|
170
175
|
end
|
171
176
|
|
172
177
|
def extract_with_mimemagic(io)
|
@@ -194,6 +194,7 @@ class Shrine
|
|
194
194
|
# with the file upload and returns the uploaded file as JSON.
|
195
195
|
class App < Roda
|
196
196
|
plugin :default_headers, "Content-Type"=>"application/json"
|
197
|
+
plugin :placeholder_string_matchers if Gem::Version.new(Roda::RodaVersion) >= Gem::Version.new("3.0.0")
|
197
198
|
|
198
199
|
route do |r|
|
199
200
|
r.on ":storage" do |storage_key|
|
@@ -257,7 +258,7 @@ class Shrine
|
|
257
258
|
presign_location.call(request)
|
258
259
|
else
|
259
260
|
extension = request.params["extension"]
|
260
|
-
extension.prepend(".") if extension && !extension.start_with?(
|
261
|
+
extension.prepend(".") if extension && !extension.start_with?(".")
|
261
262
|
uploader.send(:generate_uid, nil) + extension.to_s
|
262
263
|
end
|
263
264
|
end
|
@@ -15,7 +15,15 @@ class Shrine
|
|
15
15
|
# After loading the plugin the endpoint should be mounted on the specified
|
16
16
|
# prefix:
|
17
17
|
#
|
18
|
-
#
|
18
|
+
# # config.ru (Rack)
|
19
|
+
# map "/attachments" do
|
20
|
+
# run Shrine.download_endpoint
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# # OR
|
24
|
+
#
|
25
|
+
# # config/routes.rb (Rails)
|
26
|
+
# Rails.application.routes.draw do
|
19
27
|
# mount Shrine.download_endpoint => "/attachments"
|
20
28
|
# end
|
21
29
|
#
|
@@ -160,8 +168,9 @@ class Shrine
|
|
160
168
|
|
161
169
|
def stream_file(data)
|
162
170
|
uploaded_file = get_uploaded_file(data)
|
171
|
+
range = env["HTTP_RANGE"]
|
163
172
|
|
164
|
-
status, headers, body = uploaded_file.to_rack_response(disposition: disposition)
|
173
|
+
status, headers, body = uploaded_file.to_rack_response(disposition: disposition, range: range)
|
165
174
|
headers["Cache-Control"] = "max-age=#{365*24*60*60}" # cache for a year
|
166
175
|
|
167
176
|
request.halt [status, headers, body]
|
@@ -103,9 +103,9 @@ class Shrine
|
|
103
103
|
_log(
|
104
104
|
action: action,
|
105
105
|
phase: context[:action],
|
106
|
-
uploader: self.class,
|
106
|
+
uploader: self.class.to_s,
|
107
107
|
attachment: context[:name],
|
108
|
-
record_class: (context[:record].class if context[:record]),
|
108
|
+
record_class: (context[:record].class.to_s if context[:record]),
|
109
109
|
record_id: (context[:record].id if context[:record].respond_to?(:id)),
|
110
110
|
files: (action == "process" ? [count(input), count(result)] : count(result)),
|
111
111
|
duration: ("%.2f" % duration).to_f,
|
@@ -135,7 +135,7 @@ class Shrine
|
|
135
135
|
|
136
136
|
def _log_message_json(data)
|
137
137
|
data[:files] = Array(data[:files]).join("-")
|
138
|
-
data
|
138
|
+
JSON.generate(data)
|
139
139
|
end
|
140
140
|
|
141
141
|
def _log_message_heroku(data)
|
@@ -1,15 +1,13 @@
|
|
1
1
|
class Shrine
|
2
2
|
module Plugins
|
3
3
|
# The `metadata_attributes` plugin allows you to sync attachment metadata
|
4
|
-
# to additional record attributes.
|
4
|
+
# to additional record attributes. You can provide a hash of mappings to
|
5
|
+
# the plugin call itself or the `Attacher.metadata_attributes` method:
|
5
6
|
#
|
7
|
+
# plugin :metadata_attributes, :size => :size, :mime_type => :type
|
8
|
+
# # or
|
6
9
|
# plugin :metadata_attributes
|
7
|
-
#
|
8
|
-
# It provides `Attacher.metadata_attributes` method which allows you to
|
9
|
-
# specify mappings between metadata fields on the attachment and attribute
|
10
|
-
# names on the record.
|
11
|
-
#
|
12
|
-
# Attacher.metadata_attributes :size => :size, :mime_type => :type
|
10
|
+
# Attacher.metadata_attributes, :size => :size, :mime_type => :type
|
13
11
|
#
|
14
12
|
# The above configuration will sync `size` metadata field to
|
15
13
|
# `<attachment>_size` record attribute, and `mime_type` metadata field to
|
@@ -25,11 +23,22 @@ class Shrine
|
|
25
23
|
# user.avatar_size #=> nil
|
26
24
|
# user.avatar_type #=> nil
|
27
25
|
#
|
26
|
+
# If you want to specify the full record attribute name, pass the record
|
27
|
+
# attribute name as a string instead of a symbol.
|
28
|
+
#
|
29
|
+
# Attacher.metadata_attributes, :filename => "original_filename"
|
30
|
+
#
|
31
|
+
# # ...
|
32
|
+
#
|
33
|
+
# photo.image = image
|
34
|
+
# photo.original_filename #=> "nature.jpg"
|
35
|
+
#
|
28
36
|
# If any corresponding metadata attribute doesn't exist on the record, that
|
29
37
|
# metadata sync will be silently skipped.
|
30
38
|
module MetadataAttributes
|
31
|
-
def self.configure(uploader)
|
39
|
+
def self.configure(uploader, mappings = {})
|
32
40
|
uploader.opts[:metadata_attributes_mappings] ||= {}
|
41
|
+
uploader.opts[:metadata_attributes_mappings].merge!(mappings)
|
33
42
|
end
|
34
43
|
|
35
44
|
module AttacherClassMethods
|
@@ -44,12 +53,14 @@ class Shrine
|
|
44
53
|
cached_file = get
|
45
54
|
|
46
55
|
shrine_class.opts[:metadata_attributes_mappings].each do |source, destination|
|
47
|
-
|
56
|
+
attribute_name = destination.is_a?(Symbol) ? :"#{name}_#{destination}" : :"#{destination}"
|
57
|
+
|
58
|
+
next unless record.respond_to?(:"#{attribute_name}=")
|
48
59
|
|
49
60
|
if cached_file
|
50
|
-
record.send(:"#{
|
61
|
+
record.send(:"#{attribute_name}=", cached_file.metadata[source.to_s])
|
51
62
|
else
|
52
|
-
record.send(:"#{
|
63
|
+
record.send(:"#{attribute_name}=", nil)
|
53
64
|
end
|
54
65
|
end
|
55
66
|
end
|
@@ -13,27 +13,32 @@ class Shrine
|
|
13
13
|
#
|
14
14
|
# plugin :presign_endpoint
|
15
15
|
#
|
16
|
-
# The plugin adds a `Shrine.presign_endpoint` method which
|
17
|
-
#
|
18
|
-
#
|
16
|
+
# The plugin adds a `Shrine.presign_endpoint` method which, given a storage
|
17
|
+
# identifier, returns a Rack application that accepts GET requests and
|
18
|
+
# generates a presign for the specified storage. You can run this Rack
|
19
|
+
# application inside your app:
|
19
20
|
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
# uploads to the temporary (`:cache`) storage.
|
21
|
+
# # config.ru (Rack)
|
22
|
+
# map "/images/presign" do
|
23
|
+
# run ImageUploader.presign_endpoint(:cache)
|
24
|
+
# end
|
25
25
|
#
|
26
|
-
#
|
26
|
+
# # OR
|
27
27
|
#
|
28
|
+
# # config/routes.rb (Rails)
|
28
29
|
# Rails.application.routes.draw do
|
29
|
-
# mount
|
30
|
+
# mount ImageUploader.presign_endpoint(:cache) => "/images/presign"
|
30
31
|
# end
|
31
32
|
#
|
33
|
+
# Asynchronous upload is typically meant to replace the caching phase in
|
34
|
+
# the default synchronous workflow, so we want to generate parameters for
|
35
|
+
# uploads to the temporary (`:cache`) storage.
|
36
|
+
#
|
32
37
|
# The above will create a `GET /presign` endpoint, which generates presign
|
33
38
|
# URL, fields, and headers using the specified storage, and returns it in
|
34
39
|
# JSON format.
|
35
40
|
#
|
36
|
-
# # GET /presign
|
41
|
+
# # GET /images/presign
|
37
42
|
# {
|
38
43
|
# "url": "https://my-bucket.s3-eu-west-1.amazonaws.com",
|
39
44
|
# "fields": {
|
@@ -57,7 +62,7 @@ class Shrine
|
|
57
62
|
# By default the generated location won't have any file extension, but you
|
58
63
|
# can specify one by sending the `filename` query parameter:
|
59
64
|
#
|
60
|
-
# GET /presign?filename=nature.jpg
|
65
|
+
# GET /images/presign?filename=nature.jpg
|
61
66
|
#
|
62
67
|
# It's also possible to customize how the presign location is generated:
|
63
68
|
#
|
@@ -107,6 +112,9 @@ class Shrine
|
|
107
112
|
#
|
108
113
|
# Shrine.presign_endpoint(:cache, presign_location: "${filename}")
|
109
114
|
#
|
115
|
+
# [FineUploader]: https://github.com/FineUploader/fine-uploader
|
116
|
+
# [Dropzone]: https://github.com/enyo/dropzone
|
117
|
+
# [jQuery-File-Upload]: https://github.com/blueimp/jQuery-File-Upload
|
110
118
|
# [Amazon S3]: https://aws.amazon.com/s3/
|
111
119
|
# [Google Cloud Storage]: https://cloud.google.com/storage/
|
112
120
|
# [Microsoft Azure Storage]: https://azure.microsoft.com/en-us/services/storage/
|
@@ -13,7 +13,13 @@ class Shrine
|
|
13
13
|
#
|
14
14
|
# status, headers, body = uploaded_file.to_rack_response
|
15
15
|
# status #=> 200
|
16
|
-
# headers #=>
|
16
|
+
# headers #=>
|
17
|
+
# # {
|
18
|
+
# # "Content-Length" => "100",
|
19
|
+
# # "Content-Type" => "text/plain",
|
20
|
+
# # "Content-Disposition" => "inline; filename=\"file.txt\"",
|
21
|
+
# # "Accept-Ranges" => "bytes"
|
22
|
+
# # }
|
17
23
|
# body # object that responds to #each and #close
|
18
24
|
#
|
19
25
|
# An example how this can be used in a Rails controller:
|
@@ -29,30 +35,49 @@ class Shrine
|
|
29
35
|
# end
|
30
36
|
# end
|
31
37
|
#
|
38
|
+
# ## Disposition
|
39
|
+
#
|
32
40
|
# By default the "Content-Disposition" header will use the `inline`
|
33
41
|
# disposition, but you can change it to `attachment` if you don't want the
|
34
42
|
# file to be rendered inside the browser:
|
35
43
|
#
|
36
44
|
# status, headers, body = uploaded_file.to_rack_response(disposition: "attachment")
|
37
45
|
# headers["Content-Disposition"] #=> "attachment; filename=\"file.txt\""
|
46
|
+
#
|
47
|
+
# ## Range
|
48
|
+
#
|
49
|
+
# [Partial responses][range requests] are also supported via the `:range`
|
50
|
+
# parameter, which accepts a value of the `Range` request header.
|
51
|
+
#
|
52
|
+
# env["HTTP_RANGE"] #=> "bytes=100-200"
|
53
|
+
# status, headers, body = uploaded_file.to_rack_response(range: env["HTTP_RANGE"])
|
54
|
+
# status #=> 206
|
55
|
+
# headers["Content-Length"] #=> "101"
|
56
|
+
# headers["Content-Range"] #=> "bytes 100-200/1000"
|
57
|
+
# body # partial content
|
58
|
+
#
|
59
|
+
# [range requests]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
|
38
60
|
module RackResponse
|
39
61
|
module FileMethods
|
40
62
|
# Returns a Rack response triple for the uploaded file.
|
41
|
-
def to_rack_response(disposition: "inline")
|
42
|
-
|
43
|
-
|
44
|
-
|
63
|
+
def to_rack_response(disposition: "inline", range: false)
|
64
|
+
range = parse_http_range(range) if range
|
65
|
+
|
66
|
+
status = range ? 206 : 200
|
67
|
+
headers = rack_headers(disposition: disposition, range: range)
|
68
|
+
body = rack_body(range: range)
|
45
69
|
|
46
70
|
[status, headers, body]
|
47
71
|
end
|
48
72
|
|
49
73
|
private
|
50
74
|
|
51
|
-
# Returns a hash of "Content-Length", "Content-Type"
|
75
|
+
# Returns a hash of "Content-Length", "Content-Type" and
|
52
76
|
# "Content-Disposition" headers, whose values are extracted from
|
53
|
-
# metadata.
|
54
|
-
|
55
|
-
|
77
|
+
# metadata. Also returns the correct "Content-Range" header on ranged
|
78
|
+
# requests.
|
79
|
+
def rack_headers(disposition:, range: false)
|
80
|
+
length = range ? range.end - range.begin + 1 : size || io.size
|
56
81
|
type = mime_type || Rack::Mime.mime_type(".#{extension}")
|
57
82
|
filename = original_filename || id.split("/").last
|
58
83
|
|
@@ -60,22 +85,65 @@ class Shrine
|
|
60
85
|
headers["Content-Length"] = length.to_s if length
|
61
86
|
headers["Content-Type"] = type
|
62
87
|
headers["Content-Disposition"] = "#{disposition}; filename=\"#{filename}\""
|
88
|
+
headers["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size||io.size}" if range
|
89
|
+
headers["Accept-Ranges"] = "bytes" unless range == false
|
63
90
|
|
64
91
|
headers
|
65
92
|
end
|
66
93
|
|
67
94
|
# Returns an object that responds to #each and #close, which yields
|
68
95
|
# contents of the file.
|
69
|
-
def rack_body
|
70
|
-
|
71
|
-
|
72
|
-
|
96
|
+
def rack_body(range: nil)
|
97
|
+
if range
|
98
|
+
body = enum_for(:read_partial_chunks, range)
|
99
|
+
else
|
100
|
+
body = enum_for(:read_chunks)
|
101
|
+
end
|
102
|
+
|
103
|
+
Rack::BodyProxy.new(body) { io.close }
|
104
|
+
end
|
105
|
+
|
106
|
+
# Yields reasonably sized chunks of uploaded file's partial content
|
107
|
+
# specified by the given index range.
|
108
|
+
def read_partial_chunks(range)
|
109
|
+
bytes_read = 0
|
110
|
+
|
111
|
+
read_chunks do |chunk|
|
112
|
+
chunk_range = bytes_read..(bytes_read + chunk.bytesize - 1)
|
113
|
+
|
114
|
+
if chunk_range.begin >= range.begin && chunk_range.end <= range.end
|
115
|
+
yield chunk
|
116
|
+
elsif chunk_range.end >= range.begin || chunk_range.end <= range.end
|
117
|
+
requested_range_begin = [chunk_range.begin, range.begin].max - bytes_read
|
118
|
+
requested_range_end = [chunk_range.end, range.end].min - bytes_read
|
119
|
+
|
120
|
+
yield chunk.byteslice(requested_range_begin..requested_range_end)
|
73
121
|
else
|
74
|
-
|
122
|
+
# skip chunk
|
75
123
|
end
|
124
|
+
|
125
|
+
bytes_read += chunk.bytesize
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Yields reasonably sized chunks of uploaded file's content.
|
130
|
+
def read_chunks
|
131
|
+
if io.respond_to?(:each_chunk) # Down::ChunkedIO
|
132
|
+
io.each_chunk { |chunk| yield chunk }
|
133
|
+
else
|
134
|
+
yield io.read(16*1024) until io.eof?
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Parses the value of a "Range" HTTP header.
|
139
|
+
def parse_http_range(range_header)
|
140
|
+
if Rack.release >= "2.0"
|
141
|
+
ranges = Rack::Utils.get_byte_ranges(range_header, size || io.size)
|
142
|
+
else
|
143
|
+
ranges = Rack::Utils.byte_ranges({"HTTP_RANGE" => range_header}, size || io.size)
|
76
144
|
end
|
77
145
|
|
78
|
-
|
146
|
+
ranges.first if ranges && ranges.one?
|
79
147
|
end
|
80
148
|
end
|
81
149
|
end
|
@@ -29,24 +29,14 @@ class Shrine
|
|
29
29
|
# calculate_signature(io, :md5) if context[:action] == :cache
|
30
30
|
# end
|
31
31
|
#
|
32
|
-
# The following hashing algorithms are supported:
|
33
|
-
#
|
34
|
-
# * `sha1`
|
35
|
-
# * `sha256`
|
36
|
-
# * `sha384`
|
37
|
-
# * `sha512`
|
38
|
-
# * `md5`
|
39
|
-
# * `crc32`
|
32
|
+
# The following hashing algorithms are supported: SHA1, SHA256, SHA384,
|
33
|
+
# SHA512, MD5, and CRC32.
|
40
34
|
#
|
41
35
|
# You can also choose which format will the calculated hash be encoded in:
|
42
36
|
#
|
43
|
-
# Shrine.calculate_signature(io, :
|
44
|
-
#
|
45
|
-
# The following encoding formats are supported:
|
37
|
+
# Shrine.calculate_signature(io, :sha256, format: :hex)
|
46
38
|
#
|
47
|
-
#
|
48
|
-
# * `hex` (default)
|
49
|
-
# * `base64`
|
39
|
+
# The supported encoding formats are `hex` (default), `base64`, and `none`.
|
50
40
|
module Signature
|
51
41
|
module ClassMethods
|
52
42
|
# Calculates `algorithm` hash of the contents of the IO object, and
|
@@ -11,25 +11,27 @@ class Shrine
|
|
11
11
|
#
|
12
12
|
# plugin :upload_endpoint
|
13
13
|
#
|
14
|
-
# The plugin adds a `Shrine.upload_endpoint` method which
|
15
|
-
# identifier
|
16
|
-
# requests, and uploads received files to the specified storage.
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
14
|
+
# The plugin adds a `Shrine.upload_endpoint` method which, given a storage
|
15
|
+
# identifier, returns a Rack application that accepts multipart POST
|
16
|
+
# requests, and uploads received files to the specified storage. You can
|
17
|
+
# run this Rack application inside your app:
|
18
|
+
#
|
19
|
+
# # config.ru (Rack)
|
20
|
+
# map "/images/upload" do
|
21
|
+
# run ImageUploader.upload_endpoint(:cache)
|
22
|
+
# end
|
23
23
|
#
|
24
|
-
#
|
25
|
-
# to generate endpoints for specific uploaders. This is because different
|
26
|
-
# uploaders may have different uploading logic, and this also allows
|
27
|
-
# customizing the upload endpoint per uploader.
|
24
|
+
# # OR
|
28
25
|
#
|
26
|
+
# # config/routes.rb (Rails)
|
29
27
|
# Rails.application.routes.draw do
|
30
28
|
# mount ImageUploader.upload_endpoint(:cache) => "/images/upload"
|
31
29
|
# end
|
32
30
|
#
|
31
|
+
# Asynchronous upload is typically meant to replace the caching phase in
|
32
|
+
# the default synchronous workflow, so we want the uploads to go to
|
33
|
+
# temporary (`:cache`) storage.
|
34
|
+
#
|
33
35
|
# The above will create a `POST /images/upload` endpoint, which uploads the
|
34
36
|
# file received in the `file` param using `ImageUploader`, and returns a
|
35
37
|
# JSON representation of the uploaded file.
|
@@ -106,9 +106,9 @@ class Shrine
|
|
106
106
|
|
107
107
|
if prefix
|
108
108
|
@prefix = Pathname(relative(prefix))
|
109
|
-
@directory = Pathname(directory).join(@prefix)
|
109
|
+
@directory = Pathname(directory).join(@prefix).expand_path
|
110
110
|
else
|
111
|
-
@directory = Pathname(directory)
|
111
|
+
@directory = Pathname(directory).expand_path
|
112
112
|
end
|
113
113
|
|
114
114
|
@host = host
|
data/lib/shrine/storage/s3.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
require "shrine"
|
1
2
|
begin
|
2
3
|
require "aws-sdk-s3"
|
3
4
|
if Gem::Version.new(Aws::S3::GEM_VERSION) < Gem::Version.new("1.2.0")
|
4
5
|
raise "Shrine::Storage::S3 requires aws-sdk-s3 version 1.2.0 or above"
|
5
6
|
end
|
6
7
|
rescue LoadError
|
8
|
+
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.")
|
7
9
|
require "aws-sdk"
|
8
10
|
Aws.eager_autoload!(services: ["S3"])
|
9
11
|
end
|
@@ -20,7 +22,7 @@ class Shrine
|
|
20
22
|
#
|
21
23
|
# It is initialized with the following 4 required options:
|
22
24
|
#
|
23
|
-
#
|
25
|
+
# s3 = Shrine::Storage::S3.new(
|
24
26
|
# access_key_id: "abc",
|
25
27
|
# secret_access_key: "xyz",
|
26
28
|
# region: "eu-west-1",
|
@@ -29,15 +31,15 @@ class Shrine
|
|
29
31
|
#
|
30
32
|
# The storage exposes the underlying Aws objects:
|
31
33
|
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
34
|
+
# s3.client #=> #<Aws::S3::Client>
|
35
|
+
# s3.client.access_key_id #=> "abc"
|
36
|
+
# s3.client.secret_access_key #=> "xyz"
|
37
|
+
# s3.client.region #=> "eu-west-1"
|
36
38
|
#
|
37
|
-
#
|
38
|
-
#
|
39
|
+
# s3.bucket #=> #<Aws::S3::Bucket>
|
40
|
+
# s3.bucket.name #=> "my-app"
|
39
41
|
#
|
40
|
-
#
|
42
|
+
# s3.object("key") #=> #<Aws::S3::Object>
|
41
43
|
#
|
42
44
|
# ## Prefix
|
43
45
|
#
|
@@ -84,22 +86,27 @@ class Shrine
|
|
84
86
|
# This storage supports various URL options that will be forwarded from
|
85
87
|
# uploaded file.
|
86
88
|
#
|
87
|
-
#
|
88
|
-
#
|
89
|
+
# s3.url(public: true) # public URL without signed parameters
|
90
|
+
# s3.url(download: true) # forced download URL
|
89
91
|
#
|
90
92
|
# All other options are forwarded to the aws-sdk-s3 gem:
|
91
93
|
#
|
92
|
-
#
|
93
|
-
#
|
94
|
+
# s3.url(expires_in: 15)
|
95
|
+
# s3.url(virtual_host: true)
|
94
96
|
#
|
95
97
|
# ## CDN
|
96
98
|
#
|
97
99
|
# If you're using a CDN with S3 like Amazon CloudFront, you can specify
|
98
100
|
# the `:host` option to `#url`:
|
99
101
|
#
|
100
|
-
#
|
102
|
+
# s3.url("image.jpg", host: "http://abc123.cloudfront.net")
|
101
103
|
# #=> "http://abc123.cloudfront.net/image.jpg"
|
102
104
|
#
|
105
|
+
# You have the `:host` option passed automatically for every URL with the
|
106
|
+
# `default_url_options` plugin.
|
107
|
+
#
|
108
|
+
# plugin :default_url_options, store: { host: "http://abc123.cloudfront.net" }
|
109
|
+
#
|
103
110
|
# ## Accelerate endpoint
|
104
111
|
#
|
105
112
|
# To use Amazon S3's [Transfer Acceleration] feature, you can change the
|
@@ -147,6 +154,11 @@ class Shrine
|
|
147
154
|
# delete old files which aren't used anymore. S3 has a built-in way to do
|
148
155
|
# this, read [this article][object lifecycle] for instructions.
|
149
156
|
#
|
157
|
+
# Alternatively you can periodically call the `#clear!` method:
|
158
|
+
#
|
159
|
+
# # deletes all objects that were uploaded more than 7 days ago
|
160
|
+
# s3.clear! { |object| object.last_modified < Time.now - 7*24*60*60 }
|
161
|
+
#
|
150
162
|
# [uploading]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#put-instance_method
|
151
163
|
# [copying]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#copy_from-instance_method
|
152
164
|
# [presigning]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_post-instance_method
|
@@ -234,19 +246,25 @@ class Shrine
|
|
234
246
|
end
|
235
247
|
end
|
236
248
|
|
237
|
-
# Downloads the file from S3, and returns a `Tempfile`.
|
238
|
-
|
249
|
+
# Downloads the file from S3, and returns a `Tempfile`. And additional
|
250
|
+
# options are forwarded to [`Aws::S3::Object#get`].
|
251
|
+
#
|
252
|
+
# [`Aws::S3::Object#get`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#get-instance_method
|
253
|
+
def download(id, **options)
|
239
254
|
tempfile = Tempfile.new(["shrine-s3", File.extname(id)], binmode: true)
|
240
|
-
(object = object(id)).get(response_target: tempfile)
|
255
|
+
(object = object(id)).get(response_target: tempfile, **options)
|
241
256
|
tempfile.singleton_class.instance_eval { attr_accessor :content_type }
|
242
257
|
tempfile.content_type = object.content_type
|
243
258
|
tempfile.tap(&:open)
|
244
259
|
end
|
245
260
|
|
246
|
-
# Returns a `Down::ChunkedIO` object representing the S3 object.
|
247
|
-
|
261
|
+
# Returns a `Down::ChunkedIO` object representing the S3 object. Any
|
262
|
+
# additional options are forwarded to [`Aws::S3::Object#get`].
|
263
|
+
#
|
264
|
+
# [`Aws::S3::Object#get`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#get-instance_method
|
265
|
+
def open(id, **options)
|
248
266
|
object = object(id)
|
249
|
-
io = Down::ChunkedIO.new(chunks: object.enum_for(:get), data: {object: object})
|
267
|
+
io = Down::ChunkedIO.new(chunks: object.enum_for(:get, **options), data: { object: object })
|
250
268
|
io.size = object.content_length
|
251
269
|
io
|
252
270
|
end
|
@@ -309,24 +327,32 @@ class Shrine
|
|
309
327
|
object(id).presigned_post(options)
|
310
328
|
end
|
311
329
|
|
312
|
-
# Deletes the file from
|
330
|
+
# Deletes the file from the storage.
|
313
331
|
def delete(id)
|
314
332
|
object(id).delete
|
315
333
|
end
|
316
334
|
|
317
|
-
#
|
318
|
-
# single MULTI DELETE command for each 1000 objects (S3 delete limit).
|
335
|
+
# Deletes multiple files at once from the storage.
|
319
336
|
def multi_delete(ids)
|
320
|
-
ids.
|
321
|
-
|
322
|
-
bucket.delete_objects(delete: delete_params)
|
323
|
-
end
|
337
|
+
objects_to_delete = ids.map { |id| object(id) }
|
338
|
+
delete_objects(objects_to_delete)
|
324
339
|
end
|
325
340
|
|
326
|
-
#
|
327
|
-
|
328
|
-
|
329
|
-
|
341
|
+
# If block is given, deletes all objects from the storage for which the
|
342
|
+
# block evaluates to true. Otherwise deletes all objects from the storage.
|
343
|
+
#
|
344
|
+
# s3.clear!
|
345
|
+
# # or
|
346
|
+
# s3.clear! { |object| object.last_modified < Time.now - 7*24*60*60 }
|
347
|
+
def clear!(&block)
|
348
|
+
objects_to_delete = Enumerator.new do |yielder|
|
349
|
+
bucket.objects(prefix: prefix).each do |object|
|
350
|
+
condition = block.call(object) if block
|
351
|
+
yielder << object unless condition == false
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
delete_objects(objects_to_delete)
|
330
356
|
end
|
331
357
|
|
332
358
|
# Returns an `Aws::S3::Object` for the given id.
|
@@ -379,6 +405,15 @@ class Shrine
|
|
379
405
|
io.storage.client.config.access_key_id == client.config.access_key_id
|
380
406
|
end
|
381
407
|
|
408
|
+
# Deletes all objects in fewest requests possible (S3 only allows 1000
|
409
|
+
# objects to be deleted at once).
|
410
|
+
def delete_objects(objects)
|
411
|
+
objects.each_slice(1000) do |objects_batch|
|
412
|
+
delete_params = { objects: objects_batch.map { |object| { key: object.key } } }
|
413
|
+
bucket.delete_objects(delete: delete_params)
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
382
417
|
# Upload requests will fail if filename has non-ASCII characters, because
|
383
418
|
# of how S3 generates signatures, so we URI-encode them. Most browsers
|
384
419
|
# should automatically URI-decode filenames when downloading.
|
data/lib/shrine/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shrine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Janko Marohnić
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-10-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: down
|