shrine 2.3.1 → 2.4.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.

@@ -11,9 +11,9 @@ class Shrine
11
11
  #
12
12
  # Now the attachment module will add additional callbacks to the model:
13
13
  #
14
- # * `before_save` -- Used by the recache plugin.
15
- # * `after_commit on: [:create, :update]` -- Promotes the attachment, deletes replaced ones.
16
- # * `after_commit on: [:destroy]` -- Deletes the attachment.
14
+ # * "before save" -- Used by the `recache` plugin.
15
+ # * "after commit" (save) -- Promotes the attachment, deletes replaced ones.
16
+ # * "after commit" (destroy) -- Deletes the attachment.
17
17
  #
18
18
  # Note that ActiveRecord versions 3.x and 4.x have errors automatically
19
19
  # silenced in hooks, which can make debugging more difficult, so it's
@@ -22,10 +22,6 @@ class Shrine
22
22
  # # This is the default in ActiveRecord 5
23
23
  # ActiveRecord::Base.raise_in_transactional_callbacks = true
24
24
  #
25
- # Also note that if your tests are wrapped in transactions, the
26
- # `after_commit` callbacks won't get called, so in order to test uploading
27
- # you should first disable transactions for those tests.
28
- #
29
25
  # If you want to put promoting/deleting into a background job, see the
30
26
  # `backgrounding` plugin.
31
27
  #
@@ -38,16 +34,15 @@ class Shrine
38
34
  # before_save do
39
35
  # if avatar_data_changed? && avatar_attacher.cached?
40
36
  # # cached
41
- # end
42
- #
43
- # if avatar_data_changed? && avatar_attacher.stored?
37
+ # elsif avatar_data_changed? && avatar_attacher.stored?
44
38
  # # promoted
45
39
  # end
46
40
  # end
47
41
  # end
48
42
  #
49
- # If you don't want callbacks (e.g. you want to use the attacher object
50
- # directly), you can turn them off:
43
+ # If you don't want the attachment module to add any callbacks to the
44
+ # model, and would instead prefer to call these actions manually, you can
45
+ # disable callbacks:
51
46
  #
52
47
  # plugin :activerecord, callbacks: false
53
48
  #
@@ -62,8 +57,8 @@ class Shrine
62
57
  # validates_presence_of :avatar
63
58
  # end
64
59
  #
65
- # If you're doing validation separately from your models, you can turn off
66
- # validations for your models:
60
+ # If don't want the attachment module to merge file validations errors into
61
+ # model errors, you can disable it:
67
62
  #
68
63
  # plugin :activerecord, validations: false
69
64
  module Activerecord
@@ -119,6 +114,20 @@ class Shrine
119
114
  super
120
115
  record.save(validate: false)
121
116
  end
117
+
118
+ # If the data attribute represents a JSON column, it needs to receive a
119
+ # Hash.
120
+ def convert_before_write(value)
121
+ activerecord_json_column? ? value : super
122
+ end
123
+
124
+ # Returns true if the data attribute represents a JSON or JSONB column.
125
+ def activerecord_json_column?
126
+ return false unless record.is_a?(ActiveRecord::Base)
127
+ return false unless column = record.class.columns_hash[data_attribute.to_s]
128
+
129
+ [:json, :jsonb].include?(column.type)
130
+ end
122
131
  end
123
132
  end
124
133
 
@@ -1,8 +1,8 @@
1
1
  class Shrine
2
2
  module Plugins
3
- # The `backgrounding` plugin enables you to remove processing/storing/deleting
4
- # of files from record's lifecycle, and put them into background jobs.
5
- # This is generally useful if you're doing processing and/or your store is
3
+ # The `backgrounding` plugin enables you to move processing, storing and
4
+ # deleting of files from record's lifecycle into background jobs. This is
5
+ # generally useful if you're doing processing and/or your store is
6
6
  # something other than Storage::FileSystem.
7
7
  #
8
8
  # Shrine.plugin :backgrounding
@@ -8,12 +8,13 @@ class Shrine
8
8
  #
9
9
  # The plugin adds `#cached_<attachment>_data` to the model, which returns
10
10
  # the cached file as JSON, and should be used to set the value of the
11
- # hidden form field:
11
+ # hidden form field.
12
12
  #
13
- # <%= form_for @user do |f| %>
14
- # <%= f.hidden_field :avatar, value: @user.cached_avatar_data %>
15
- # <%= f.field_field :avatar %>
16
- # <% end %>
13
+ # @user.cached_avatar_data #=> '{"storage":"cache","id":"...","metadata":{...}}'
14
+ #
15
+ # This method delegates to `Attacher#read_cached`:
16
+ #
17
+ # attacher.read_cached #=> '{"storage":"cache","id":"...","metadata":{...}}'
17
18
  module CachedAttachmentData
18
19
  module AttachmentMethods
19
20
  def initialize(*)
@@ -23,7 +23,7 @@ class Shrine
23
23
  def initialize_copy(record)
24
24
  super
25
25
  @#{@name}_attacher = nil # reload the attacher
26
- self.#{@name}_data = nil # remove original attachment
26
+ #{@name}_attacher.send(:write, nil) # remove original attachment
27
27
  #{@name}_attacher.copy(record.#{@name}_attacher)
28
28
  end
29
29
  RUBY
@@ -34,15 +34,16 @@ class Shrine
34
34
  def copy(attacher)
35
35
  options = {action: :copy, move: false}
36
36
 
37
- if attacher.cached?
38
- copied_attachment = cache!(attacher.get, **options)
39
- elsif attacher.stored?
40
- copied_attachment = store!(attacher.get, **options)
41
- else
42
- copied_attachment = nil
43
- end
37
+ copied_attachment = if attacher.cached?
38
+ cache!(attacher.get, **options)
39
+ elsif attacher.stored?
40
+ store!(attacher.get, **options)
41
+ else
42
+ nil
43
+ end
44
44
 
45
- set(copied_attachment)
45
+ @old = get
46
+ _set(copied_attachment)
46
47
  end
47
48
  end
48
49
  end
@@ -18,6 +18,11 @@ class Shrine
18
18
  # user.avatar.mime_type #=> "image/png"
19
19
  # user.avatar.size #=> 43423
20
20
  #
21
+ # You can also use `#data_uri=` and `#data_uri` methods directly on the
22
+ # `Shrine::Attacher`:
23
+ #
24
+ # attacher.data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"
25
+ #
21
26
  # If you want the uploaded file to have an extension, you can generate a
22
27
  # filename based on the content type of the data URI:
23
28
  #
@@ -5,8 +5,17 @@ class Shrine
5
5
  #
6
6
  # plugin :default_url_options, store: {download: true}
7
7
  #
8
- # The default options are merged with options passed to `UploadedFile#url`,
9
- # and the latter will always have precedence over default options.
8
+ # You can also generate the default URL options dynamically by using a
9
+ # block, which will receive the UploadedFile object along with any options
10
+ # that were passed to `UploadedFile#url`.
11
+ #
12
+ # plugin :default_url_options, store: ->(io, **options) do
13
+ # {response_content_disposition: "attachment; filename=\"#{io.original_filename}\""}
14
+ # end
15
+ #
16
+ # In both cases the default options are merged with options passed to
17
+ # `UploadedFile#url`, and the latter will always have precedence over
18
+ # default options.
10
19
  module DefaultUrlOptions
11
20
  def self.configure(uploader, options = {})
12
21
  uploader.opts[:default_url_options] = (uploader.opts[:default_url_options] || {}).merge(options)
@@ -14,14 +23,18 @@ class Shrine
14
23
 
15
24
  module FileMethods
16
25
  def url(**options)
26
+ default_options = default_url_options
27
+ default_options = default_options.call(self, **options) if default_options.respond_to?(:call)
28
+ default_options ||= {}
29
+
17
30
  super(default_options.merge(options))
18
31
  end
19
32
 
20
33
  private
21
34
 
22
- def default_options
35
+ def default_url_options
23
36
  options = shrine_class.opts[:default_url_options]
24
- options[storage_key.to_sym] || {}
37
+ options[storage_key.to_sym]
25
38
  end
26
39
  end
27
40
  end
@@ -60,7 +60,9 @@ class Shrine
60
60
  # plugin :direct_upload, max_size: 5*1024*1024 # 5 MB
61
61
  #
62
62
  # Note that this option doesn't affect presigned uploads, there you can
63
- # apply filesize limit when generating a presign.
63
+ # apply filesize limit when generating a presign. The filesize constraint
64
+ # here is for security purposes, you should still perform file validations
65
+ # on attaching.
64
66
  #
65
67
  # ## Presigns
66
68
  #
@@ -96,11 +98,11 @@ class Shrine
96
98
  # plugin :direct_upload, presign_options: {acl: "public-read"}
97
99
  #
98
100
  # plugin :direct_upload, presign_options: ->(request) do
99
- # filename = request.params["filename"].inspect
101
+ # filename = request.params["filename"]
100
102
  #
101
103
  # {
102
- # content_length_range: 0..(10*1024*1024), # limit filesize to 10MB
103
- # content_disposition: "attachment; filename=#{filename}", # download with original filename
104
+ # content_length_range: 0..(10*1024*1024), # limit filesize to 10MB
105
+ # content_disposition: "attachment; filename=\"#{filename}\"", # download with original filename
104
106
  # }
105
107
  # end
106
108
  #
@@ -111,13 +113,6 @@ class Shrine
111
113
  # See the [Direct Uploads to S3] guide for further instructions on how to
112
114
  # hook the presigned uploads to a form.
113
115
  #
114
- # ### Testing presigns
115
- #
116
- # If you want to test presigned uploads, but don't want to use Amazon S3 in
117
- # tests for performance reasons, you can simply swap out S3 with a storage
118
- # like FileSystem. The presigns will still get generated, but will simply
119
- # point to this endpoint's upload route instead.
120
- #
121
116
  # ## Allowed storages
122
117
  #
123
118
  # By default only uploads to `:cache` are allowed, to prevent the
@@ -97,6 +97,8 @@ class Shrine
97
97
  # and allowed. Afterwards it proceeds with the file download using
98
98
  # streaming.
99
99
  class App < Roda
100
+ plugin :streaming
101
+
100
102
  route do |r|
101
103
  r.on ":storage" do |storage_key|
102
104
  @storage = get_storage(storage_key)
@@ -105,26 +107,19 @@ class Shrine
105
107
  filename = request.path.split("/").last
106
108
  extname = File.extname(filename)
107
109
 
108
- response["Content-Disposition"] = "#{disposition}; filename=#{filename.inspect}"
110
+ response["Content-Disposition"] = "#{disposition}; filename=\"#{filename}\""
109
111
  response["Content-Type"] = Rack::Mime.mime_type(extname)
110
112
 
111
- io = get_stream_io(id)
113
+ io = storage.open(id)
112
114
  response["Content-Length"] = io.size.to_s if io.size
113
115
 
114
- body = Enumerator.new do |y|
115
- if io.respond_to?(:each_chunk)
116
- io.each_chunk { |chunk| y.yield(chunk) }
116
+ stream(callback: ->{io.close}) do |out|
117
+ if io.respond_to?(:each_chunk) # Down::ChunkedIO
118
+ io.each_chunk { |chunk| out << chunk }
117
119
  else
118
- y.yield io.read(16*1024, buffer ||= "") until io.eof?
120
+ out << io.read(16*1024, buffer ||= "") until io.eof?
119
121
  end
120
122
  end
121
-
122
- proxy = Rack::BodyProxy.new(body) do
123
- io.close
124
- io.delete if io.class.name == "Tempfile"
125
- end
126
-
127
- r.halt response.finish_with_body(proxy)
128
123
  end
129
124
  end
130
125
  end
@@ -138,17 +133,6 @@ class Shrine
138
133
  shrine_class.find_storage(storage_key)
139
134
  end
140
135
 
141
- def get_stream_io(id)
142
- if storage.respond_to?(:stream)
143
- warn "Storage#stream is deprecated, in Shrine 3 the download_endpoint plugin will use only Storage#open. You should update your storage library."
144
- stream = storage.enum_for(:stream, id)
145
- chunks = Enumerator.new { |y| y << Array(stream.next)[0] }
146
- Down::ChunkedIO.new(size: Array(stream.peek)[1], chunks: chunks)
147
- else
148
- storage.open(id)
149
- end
150
- end
151
-
152
136
  # Halts the request if storage is not allowed.
153
137
  def allow_storage!(storage_key)
154
138
  if !allowed_storages.map(&:to_s).include?(storage_key)
@@ -7,7 +7,7 @@ class Shrine
7
7
  #
8
8
  # This plugin allows you pass an array of files to `Shrine#delete`.
9
9
  #
10
- # Shrine.new(:storage).delete([file1, file2, file3])
10
+ # uploader.delete([file1, file2, file3])
11
11
  #
12
12
  # Now if you're using Storage::S3, deleting an array of files will issue a
13
13
  # single HTTP request. Some other storages may support multi deletes as
@@ -20,6 +20,14 @@ class Shrine
20
20
  # in any block to signal that no processing was performed and that the
21
21
  # original file should be used.
22
22
  #
23
+ # The `.process` call is just a shorthand for
24
+ #
25
+ # def process(io, context)
26
+ # if context[:action] == :store
27
+ # # ...
28
+ # end
29
+ # end
30
+ #
23
31
  # If you want the result of processing to be multiple files, use the
24
32
  # `versions` plugin.
25
33
  module Processing
@@ -15,6 +15,11 @@ class Shrine
15
15
  # user.avatar.size #=> 43423
16
16
  # user.avatar.original_filename #=> "cool-image.png"
17
17
  #
18
+ # You can also use `#remote_url=` and `#remote_url` methods directly on the
19
+ # `Shrine::Attacher`:
20
+ #
21
+ # attacher.remote_url = "http://example.com/cool-image.png"
22
+ #
18
23
  # The file will by default be downloaded using [Down], which is a wrapper
19
24
  # around the open-uri standard library. It's a good practice to limit the
20
25
  # maximum filesize of the remote file:
@@ -7,16 +7,9 @@ class Shrine
7
7
  #
8
8
  # If for example your attachment is called "avatar", this plugin will add
9
9
  # `#remove_avatar` and `#remove_avatar=` methods to your model. This allows
10
- # you to easily delete attached files through the form:
10
+ # you to add a form field for removing attachments:
11
11
  #
12
- # <%= form_for @user do |f| %>
13
- # <%= f.hidden_field :avatar, value: @user.avatar_data %>
14
- # <%= f.file_field :avatar %>
15
- # Remove attachment: <%= f.check_box :remove_avatar %>
16
- # <% end %>
17
- #
18
- # Now when the checkbox is ticked and the form is submitted, the attached
19
- # file will be removed.
12
+ # form.check_box :remove_avatar
20
13
  module RemoveAttachment
21
14
  module AttachmentMethods
22
15
  def initialize(*)
@@ -49,7 +42,7 @@ class Shrine
49
42
 
50
43
  # Rails sends "0" or "false" if the checkbox hasn't been ticked.
51
44
  def remove?
52
- remove && remove != "" && remove !~ /\A0|false$\z/
45
+ remove && remove != "" && remove !~ /\A(0|false)\z/
53
46
  end
54
47
  end
55
48
  end
@@ -11,13 +11,9 @@ class Shrine
11
11
  #
12
12
  # Now the attachment module will add additional callbacks to the model:
13
13
  #
14
- # * `before_save` -- Used by the recached plugin.
15
- # * `after_commit` -- Promotes the attachment, deletes replaced ones.
16
- # * `after_destroy_commit` -- Deletes the attachment.
17
- #
18
- # Also note that if your tests are wrapped in transactions, the
19
- # `after_commit` callbacks won't get called, so in order to test uploading
20
- # you should first disable transactions for those tests.
14
+ # * "before save" -- Used by the `recache` plugin.
15
+ # * "after commit" (save) -- Promotes the attachment, deletes replaced ones.
16
+ # * "after commit" (destroy) -- Deletes the attachment.
21
17
  #
22
18
  # If you want to put promoting/deleting into a background job, see the
23
19
  # `backgrounding` plugin.
@@ -33,16 +29,15 @@ class Shrine
33
29
  #
34
30
  # if changed_columns.include?(:avatar) && avatar_attacher.cached?
35
31
  # # cached
36
- # end
37
- #
38
- # if changed_columns.include?(:avatar) && avatar_attacher.stored?
32
+ # elsif changed_columns.include?(:avatar) && avatar_attacher.stored?
39
33
  # # promoted
40
34
  # end
41
35
  # end
42
36
  # end
43
37
  #
44
- # If you don't want callbacks (e.g. you want to use the attacher object
45
- # directly), you can turn them off:
38
+ # If you don't want the attachment module to add any callbacks to the
39
+ # model, and would instead prefer to call these actions manually, you can
40
+ # disable callbacks:
46
41
  #
47
42
  # plugin :sequel, callbacks: false
48
43
  #
@@ -57,8 +52,8 @@ class Shrine
57
52
  # validates_presence_of :avatar
58
53
  # end
59
54
  #
60
- # If you're doing validation separately from your models, you can turn off
61
- # validations for your models:
55
+ # If don't want the attachment module to merge file validations errors into
56
+ # model errors, you can disable it:
62
57
  #
63
58
  # plugin :sequel, validations: false
64
59
  module Sequel
@@ -90,14 +85,14 @@ class Shrine
90
85
  #{@name}_attacher.save if #{@name}_attacher.attached?
91
86
  end
92
87
 
93
- def after_commit
88
+ def after_save
94
89
  super
95
- #{@name}_attacher.finalize if #{@name}_attacher.attached?
90
+ db.after_commit{#{@name}_attacher.finalize} if #{@name}_attacher.attached?
96
91
  end
97
92
 
98
- def after_destroy_commit
93
+ def after_destroy
99
94
  super
100
- #{@name}_attacher.destroy
95
+ db.after_commit{#{@name}_attacher.destroy} if #{@name}_attacher.read
101
96
  end
102
97
  RUBY
103
98
  end
@@ -111,13 +106,6 @@ class Shrine
111
106
  end
112
107
 
113
108
  module AttacherMethods
114
- # Support for Postgres JSON columns.
115
- def read
116
- value = super
117
- value = value.to_hash if value.respond_to?(:to_hash)
118
- value
119
- end
120
-
121
109
  private
122
110
 
123
111
  # Saves the record after assignment, skipping validations.
@@ -125,6 +113,21 @@ class Shrine
125
113
  super
126
114
  record.save_changes(validate: false)
127
115
  end
116
+
117
+ # If the data represents a JSON column with `pg_json` Sequel extension
118
+ # loaded, a `Sequel::Postgres::JSONHashBase` object will be returned,
119
+ # which we convert into a Hash.
120
+ def convert_after_read(value)
121
+ sequel_json_column? ? value.to_hash : super
122
+ end
123
+
124
+ # Returns true if the data attribute represents a JSON or JSONB column.
125
+ def sequel_json_column?
126
+ return false unless record.is_a?(::Sequel::Model)
127
+ return false unless column = record.class.db_schema[data_attribute]
128
+
129
+ [:json, :jsonb].include?(column[:type])
130
+ end
128
131
  end
129
132
  end
130
133