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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2156db4347bf54e372dd3865a117b38e14638419
4
- data.tar.gz: 6fc29c5c7837f3355240b85ff313579b35953a62
3
+ metadata.gz: b6ffc195c54c2afa0b36d184a94e44f5e8e110bb
4
+ data.tar.gz: '026928b32cef34807152c6999cee6e5f2f94ef81'
5
5
  SHA512:
6
- metadata.gz: 634e5d0bec22737521cef1b2b7eab82871e3be1d13988a171a6686e3f3885d2514531ede958ca757dc9a84c526b049f9d2bb5bccb4119f4c8c090a1272ba88c7
7
- data.tar.gz: bfa00d5f2ae6ca28e66cb0e26872cfb127fe00e13b12bf0eddd9965b295a5ea4b7418bf1d74f4e3deb059aa8a66d3262b120f968b369d71f675a3c5b9d265c4f
6
+ metadata.gz: b520588785bf8fb1145500fe7a5819685d3a1ec00773c9bf124a7e4b8c0a6c4f5400e0389910741db02aaf58a591df263ba8e305f9112ed83bda997d6b6dbed5
7
+ data.tar.gz: a993d56594c2ea0b8c31f5c8bf84416358da9bdda4ede5c5353ba8be89d8cb5058f05dc97eedaf9a63efcf592c9354006fd4b402bc65399d989933aceb6e4a5c
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015-2016 Janko Marohnić
3
+ Copyright (c) 2015-2017 Janko Marohnić
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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
@@ -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: avatar, thumb: thumb})
46
+ attacher.swap({original: attachment, thumb: thumb})
47
47
  end
48
48
  end
49
49
  ```
@@ -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
- :read => [:length, :outbuf],
32
- :eof? => [],
33
- :rewind => [],
34
- :size => [],
35
- :close => [],
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." unless opts.key?(:mime_type_analyzer)
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
- # class MyUploader < Shrine
62
- # plugin :validation_helpers
61
+ # class MyUploader < Shrine
62
+ # plugin :validation_helpers
63
63
  #
64
- # Attacher.validate do
65
- # validate_max_size 256 * 1024**2, message: ->(max) { [:max_size, max: max] }
66
- # end
67
- # end
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
- analyzer = opts[:mime_type_analyzer]
84
- analyzer = mime_type_analyzers[analyzer] if analyzer.is_a?(Symbol)
85
- args = [io, mime_type_analyzers].take(analyzer.arity.abs)
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
- mime_type = analyzer.call(*args)
88
- io.rewind
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
- cmd = %W[file --mime-type --brief -]
148
- options = { stdin_data: io.read(MAGIC_NUMBER), binmode: true }
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
- begin
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
- raise Error, stderr unless status.success?
157
- $stderr.print(stderr)
160
+ raise Error, stderr.read unless status.success?
161
+ $stderr.print(stderr.read)
158
162
 
159
- stdout.strip
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
- filemagic = FileMagic.new(FileMagic::MAGIC_MIME_TYPE)
166
- mime_type = filemagic.buffer(io.read(MAGIC_NUMBER))
167
- filemagic.close
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
- # Rails.appliations.routes.draw do
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.to_json
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
- next unless record.respond_to?(:"#{name}_#{destination}=")
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(:"#{name}_#{destination}=", cached_file.metadata[source.to_s])
61
+ record.send(:"#{attribute_name}=", cached_file.metadata[source.to_s])
51
62
  else
52
- record.send(:"#{name}_#{destination}=", nil)
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 accepts a
17
- # storage identifier and returns a Rack application that accepts GET
18
- # requests and generates a presign for the specified storage.
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
- # Shrine.presign_endpoint(:cache) # rack app
21
- #
22
- # Asynchronous upload is typically meant to replace the caching phase in
23
- # the default synchronous workflow, so we want to generate parameters for
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
- # We can mount the returned Rack application inside our application:
26
+ # # OR
27
27
  #
28
+ # # config/routes.rb (Rails)
28
29
  # Rails.application.routes.draw do
29
- # mount Shrine.presign_endpoint(:cache) => "/presign"
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 #=> {"Content-Length" => "100", "Content-Type" => "text/plain", "Content-Disposition" => "inline; filename=\"file.txt\""}
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
- status = 200
43
- headers = rack_headers(disposition: disposition)
44
- body = rack_body
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", and
75
+ # Returns a hash of "Content-Length", "Content-Type" and
52
76
  # "Content-Disposition" headers, whose values are extracted from
53
- # metadata.
54
- def rack_headers(disposition:)
55
- length = size || io.size
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
- chunks = Enumerator.new do |yielder|
71
- if io.respond_to?(:each_chunk) # Down::ChunkedIO
72
- io.each_chunk { |chunk| yielder << chunk }
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
- yielder << io.read(16*1024) until io.eof?
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
- Rack::BodyProxy.new(chunks) { io.close }
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, :md5, format: :hex)
44
- #
45
- # The following encoding formats are supported:
37
+ # Shrine.calculate_signature(io, :sha256, format: :hex)
46
38
  #
47
- # * `none`
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 accepts a storage
15
- # identifier and returns a Rack application that accepts multipart POST
16
- # requests, and uploads received files to the specified storage.
17
- #
18
- # Shrine.upload_endpoint(:cache) # rack app
19
- #
20
- # Asynchronous upload is typically meant to replace the caching phase in
21
- # the default synchronous workflow, so we want the uploads to go to
22
- # temporary (`:cache`) storage.
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
- # When we want to mount the Rack application to our app, it's recommended
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
@@ -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
- # storage = Shrine::Storage::S3.new(
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
- # storage.client #=> #<Aws::S3::Client>
33
- # storage.client.access_key_id #=> "abc"
34
- # storage.client.secret_access_key #=> "xyz"
35
- # storage.client.region #=> "eu-west-1"
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
- # storage.bucket #=> #<Aws::S3::Bucket>
38
- # storage.bucket.name #=> "my-app"
39
+ # s3.bucket #=> #<Aws::S3::Bucket>
40
+ # s3.bucket.name #=> "my-app"
39
41
  #
40
- # storage.object("key") #=> #<Aws::S3::Object>
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
- # uploaded_file.url(public: true) # public URL without signed parameters
88
- # uploaded_file.url(download: true) # forced download URL
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
- # uploaded_file.url(expires_in: 15)
93
- # uploaded_file.url(virtual_host: true)
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
- # uploaded_file.url("image.jpg", host: "http://abc123.cloudfront.net")
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
- def download(id)
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
- def open(id)
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 S3.
330
+ # Deletes the file from the storage.
313
331
  def delete(id)
314
332
  object(id).delete
315
333
  end
316
334
 
317
- # This is called when multiple files are being deleted at once. Issues a
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.each_slice(1000) do |ids_batch|
321
- delete_params = {objects: ids_batch.map { |id| {key: object(id).key} }}
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
- # Deletes all files from the storage.
327
- def clear!
328
- objects = bucket.object_versions(prefix: prefix)
329
- objects.respond_to?(:batch_delete!) ? objects.batch_delete! : objects.delete
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.
@@ -5,10 +5,10 @@ class Shrine
5
5
 
6
6
  module VERSION
7
7
  MAJOR = 2
8
- MINOR = 7
8
+ MINOR = 8
9
9
  TINY = 0
10
10
  PRE = nil
11
11
 
12
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
12
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
13
13
  end
14
14
  end
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.7.0
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-09-11 00:00:00.000000000 Z
11
+ date: 2017-10-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: down