shrine 1.4.2 → 2.0.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.

Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +236 -234
  3. data/doc/changing_location.md +6 -4
  4. data/doc/creating_storages.md +4 -4
  5. data/doc/design.md +223 -0
  6. data/doc/migrating_storage.md +6 -11
  7. data/doc/regenerating_versions.md +22 -40
  8. data/lib/shrine.rb +60 -77
  9. data/lib/shrine/plugins/activerecord.rb +37 -14
  10. data/lib/shrine/plugins/background_helpers.rb +1 -0
  11. data/lib/shrine/plugins/backgrounding.rb +49 -37
  12. data/lib/shrine/plugins/backup.rb +6 -4
  13. data/lib/shrine/plugins/cached_attachment_data.rb +5 -5
  14. data/lib/shrine/plugins/data_uri.rb +9 -9
  15. data/lib/shrine/plugins/default_storage.rb +4 -4
  16. data/lib/shrine/plugins/default_url.rb +7 -1
  17. data/lib/shrine/plugins/default_url_options.rb +1 -1
  18. data/lib/shrine/plugins/delete_promoted.rb +2 -2
  19. data/lib/shrine/plugins/delete_raw.rb +4 -4
  20. data/lib/shrine/plugins/determine_mime_type.rb +50 -43
  21. data/lib/shrine/plugins/direct_upload.rb +10 -20
  22. data/lib/shrine/plugins/download_endpoint.rb +16 -13
  23. data/lib/shrine/plugins/dynamic_storage.rb +4 -12
  24. data/lib/shrine/plugins/included.rb +6 -19
  25. data/lib/shrine/plugins/keep_files.rb +4 -4
  26. data/lib/shrine/plugins/logging.rb +4 -4
  27. data/lib/shrine/plugins/migration_helpers.rb +37 -34
  28. data/lib/shrine/plugins/moving.rb +19 -32
  29. data/lib/shrine/plugins/parallelize.rb +5 -5
  30. data/lib/shrine/plugins/pretty_location.rb +2 -6
  31. data/lib/shrine/plugins/remote_url.rb +31 -43
  32. data/lib/shrine/plugins/remove_attachment.rb +5 -5
  33. data/lib/shrine/plugins/remove_invalid.rb +1 -1
  34. data/lib/shrine/plugins/restore_cached_data.rb +4 -10
  35. data/lib/shrine/plugins/sequel.rb +46 -21
  36. data/lib/shrine/plugins/store_dimensions.rb +19 -20
  37. data/lib/shrine/plugins/upload_options.rb +11 -9
  38. data/lib/shrine/plugins/validation_helpers.rb +3 -3
  39. data/lib/shrine/plugins/versions.rb +18 -3
  40. data/lib/shrine/storage/file_system.rb +9 -11
  41. data/lib/shrine/storage/linter.rb +1 -7
  42. data/lib/shrine/storage/s3.rb +25 -19
  43. data/lib/shrine/version.rb +3 -3
  44. data/shrine.gemspec +13 -3
  45. metadata +28 -9
  46. data/lib/shrine/plugins/delete_uploaded.rb +0 -3
  47. data/lib/shrine/plugins/keep_location.rb +0 -46
  48. data/lib/shrine/plugins/restore_cached.rb +0 -3
@@ -32,6 +32,9 @@ class Shrine
32
32
  # }
33
33
  # }
34
34
  #
35
+ # Note that the endpoint uploads the file standalone, without any knowledge
36
+ # of the record, so `context[:record]` and `context[:name]` will be nil.
37
+ #
35
38
  # Once you've uploaded the file, you need to assign the result to the
36
39
  # hidden attachment field in the form. There are many great JavaScript
37
40
  # libraries for file uploads, most popular being [jQuery-File-Upload].
@@ -156,18 +159,12 @@ class Shrine
156
159
  uploader.plugin :rack_file
157
160
  end
158
161
 
159
- def self.configure(uploader, allowed_storages: [:cache], presign: nil, presign_options: {}, presign_location: nil, max_size: nil)
160
- if presign.respond_to?(:call)
161
- warn "Passing a block to :presign in direct_upload plugin is deprecated and will be removed in Shrine 2. Use :presign_options instead."
162
- presign_options = presign
163
- presign = true
164
- end
165
-
166
- uploader.opts[:direct_upload_allowed_storages] = allowed_storages
167
- uploader.opts[:direct_upload_presign] = presign
168
- uploader.opts[:direct_upload_presign_options] = presign_options
169
- uploader.opts[:direct_upload_presign_location] = presign_location
170
- uploader.opts[:direct_upload_max_size] = max_size
162
+ def self.configure(uploader, opts = {})
163
+ uploader.opts[:direct_upload_allowed_storages] = opts.fetch(:allowed_storages, uploader.opts.fetch(:direct_upload_allowed_storages, [:cache]))
164
+ uploader.opts[:direct_upload_presign] = opts.fetch(:presign, uploader.opts[:direct_upload_presign])
165
+ uploader.opts[:direct_upload_presign_options] = opts.fetch(:presign_options, uploader.opts.fetch(:direct_upload_presign_options, {}))
166
+ uploader.opts[:direct_upload_presign_location] = opts.fetch(:presign_location, uploader.opts[:direct_upload_presign_location])
167
+ uploader.opts[:direct_upload_max_size] = opts.fetch(:max_size, uploader.opts[:direct_upload_max_size])
171
168
 
172
169
  uploader.assign_upload_endpoint(App) unless uploader.const_defined?(:UploadEndpoint)
173
170
  end
@@ -185,12 +182,6 @@ class Shrine
185
182
  endpoint_class.opts[:shrine_class] = self
186
183
  const_set(:UploadEndpoint, endpoint_class)
187
184
  end
188
-
189
- # Returns the Roda direct upload endpoint.
190
- def direct_endpoint
191
- warn "Shrine.direct_endpoint is deprecated and will be removed in Shrine 2, you should use Shrine::UploadEndpoint instead."
192
- self::UploadEndpoint
193
- end
194
185
  end
195
186
 
196
187
  # Routes incoming requests. It first asserts that the storage is existent
@@ -198,7 +189,6 @@ class Shrine
198
189
  # with the file upload and returns the uploaded file as JSON.
199
190
  class App < Roda
200
191
  plugin :default_headers, "Content-Type"=>"application/json"
201
- plugin :json_parser
202
192
 
203
193
  route do |r|
204
194
  r.on ":storage" do |storage_key|
@@ -260,7 +250,7 @@ class Shrine
260
250
  if presign_location
261
251
  presign_location.call(request)
262
252
  else
263
- SecureRandom.hex(30) + request.params["extension"].to_s
253
+ uploader.send(:generate_uid, nil) + request.params["extension"].to_s
264
254
  end
265
255
  end
266
256
 
@@ -47,11 +47,13 @@ class Shrine
47
47
  #
48
48
  # [Roda]: https://github.com/jeremyevans/roda
49
49
  module DownloadEndpoint
50
- def self.configure(uploader, storages:, prefix:, disposition: "inline", host: nil)
51
- uploader.opts[:download_endpoint_storages] = storages
52
- uploader.opts[:download_endpoint_prefix] = prefix
53
- uploader.opts[:download_endpoint_disposition] = disposition
54
- uploader.opts[:download_endpoint_host] = host
50
+ def self.configure(uploader, opts = {})
51
+ uploader.opts[:download_endpoint_storages] = opts.fetch(:storages, uploader.opts[:download_endpoint_storages])
52
+ uploader.opts[:download_endpoint_prefix] = opts.fetch(:prefix, uploader.opts[:download_endpoint_prefix])
53
+ uploader.opts[:download_endpoint_disposition] = opts.fetch(:disposition, uploader.opts.fetch(:download_endpoint_disposition, "inline"))
54
+ uploader.opts[:download_endpoint_host] = opts.fetch(:host, uploader.opts[:download_endpoint_host])
55
+
56
+ raise Error, "The :storages option is required for download_endpoint plugin" if uploader.opts[:download_endpoint_storages].nil?
55
57
 
56
58
  uploader.assign_download_endpoint(App) unless uploader.const_defined?(:DownloadEndpoint)
57
59
  end
@@ -84,7 +86,7 @@ class Shrine
84
86
  id,
85
87
  ].join("/")
86
88
  else
87
- super(options)
89
+ super
88
90
  end
89
91
  end
90
92
  end
@@ -93,8 +95,6 @@ class Shrine
93
95
  # and allowed. Afterwards it proceeds with the file download using
94
96
  # streaming.
95
97
  class App < Roda
96
- plugin :streaming
97
-
98
98
  route do |r|
99
99
  r.on ":storage" do |storage_key|
100
100
  @storage = get_storage(storage_key)
@@ -106,15 +106,18 @@ class Shrine
106
106
  response["Content-Disposition"] = "#{disposition}; filename=#{filename.inspect}"
107
107
  response["Content-Type"] = Rack::Mime.mime_type(extname)
108
108
 
109
- chunks = get_stream(id)
110
- _, content_length = chunks.peek
109
+ stream = get_stream(id)
110
+ _, content_length = stream.peek
111
111
  response['Content-Length'] = content_length.to_s if content_length
112
112
 
113
- stream do |out|
114
- chunks.each do |chunk|
115
- out << chunk
113
+ chunks = Enumerator.new do |y|
114
+ loop do
115
+ chunk, * = stream.next
116
+ y << chunk
116
117
  end
117
118
  end
119
+
120
+ r.halt response.finish_with_body(chunks)
118
121
  end
119
122
  end
120
123
  end
@@ -34,20 +34,12 @@ class Shrine
34
34
  private
35
35
 
36
36
  def resolve_dynamic_storage(name)
37
- dynamic_storage_cache.fetch(name) do
38
- dynamic_storages.each do |regex, block|
39
- if match = name.to_s.match(regex)
40
- dynamic_storage_cache[name] = block.call(match)
41
- break
42
- end
37
+ dynamic_storages.each do |regex, block|
38
+ if match = name.to_s.match(regex)
39
+ return block.call(match)
43
40
  end
44
-
45
- dynamic_storage_cache[name]
46
41
  end
47
- end
48
-
49
- def dynamic_storage_cache
50
- @dynamic_storage_cache ||= {}
42
+ nil
51
43
  end
52
44
  end
53
45
  end
@@ -1,8 +1,8 @@
1
1
  class Shrine
2
2
  module Plugins
3
- # The included plugin allows you to hook up to the `.included` hook when
4
- # of the "attachment" module. This allows you to add additonal methods to
5
- # the model whenever an attachment is included.
3
+ # The included plugin allows you to hook up to the `.included` hook of the
4
+ # attachment module, and do additional action on the model which includes
5
+ # it.
6
6
  #
7
7
  # plugin :included do |name|
8
8
  # define_method("#{name}_width") do
@@ -14,22 +14,9 @@ class Shrine
14
14
  # end
15
15
  # end
16
16
  #
17
- # The block is evaluated in the context of the model, but note that you
18
- # cannot use keywords like `def`, instead you should use the
19
- # metaprogramming equivalents like `define_method`. Now when an attachment
20
- # is included to a model, it will receive the appropriate methods:
21
- #
22
- # class User
23
- # include ImageUploader[:avatar]
24
- # end
25
- #
26
- # user = User.new
27
- # user.avatar_width #=> nil
28
- # user.avatar_height #=> nil
29
- #
30
- # user.avatar = File.open("avatar.jpg")
31
- # user.avatar_width #=> 300
32
- # user.avatar_height #=> 500
17
+ # The block is evaluated in the context of the model via `instance_exec`.
18
+ # This means you cannot use keywords like `def`, instead you should use the
19
+ # metaprogramming equivalents like `define_method`.
33
20
  module Included
34
21
  def self.configure(uploader, &block)
35
22
  uploader.opts[:included_block] = block
@@ -20,10 +20,10 @@ class Shrine
20
20
  #
21
21
  # [event store]: http://docs.geteventstore.com/introduction/event-sourcing-basics/
22
22
  module KeepFiles
23
- def self.configure(uploader, destroyed: nil, replaced: nil, **)
24
- uploader.opts[:keep_files] = []
25
- uploader.opts[:keep_files] << :destroyed if destroyed
26
- uploader.opts[:keep_files] << :replaced if replaced
23
+ def self.configure(uploader, opts = {})
24
+ keep_files = (uploader.opts[:keep_files] ||= [])
25
+ opts[:destroyed] ? keep_files << :destroyed : keep_files.delete(:destroyed) if opts.key?(:destroyed)
26
+ opts[:replaced] ? keep_files << :replaced : keep_files.delete(:replaced) if opts.key?(:replaced)
27
27
  end
28
28
 
29
29
  module AttacherMethods
@@ -48,10 +48,10 @@ class Shrine
48
48
  uploader.plugin :hooks
49
49
  end
50
50
 
51
- def self.configure(uploader, logger: nil, stream: $stdout, format: :human)
52
- uploader.opts[:logging_logger] = logger
53
- uploader.opts[:logging_stream] = stream
54
- uploader.opts[:logging_format] = format
51
+ def self.configure(uploader, opts = {})
52
+ uploader.opts[:logging_logger] = opts.fetch(:logger, uploader.opts[:logging_logger])
53
+ uploader.opts[:logging_stream] = opts.fetch(:stream, uploader.opts.fetch(:logging_stream, $stdout))
54
+ uploader.opts[:logging_format] = opts.fetch(:format, uploader.opts.fetch(:logging_format, :human))
55
55
  end
56
56
 
57
57
  module ClassMethods
@@ -1,57 +1,60 @@
1
+ warn "The migration_helpers Shrine plugin is deprecated and will be removed in Shrine 3. Attacher#cached? and Attacher#stored? have been moved to base."
2
+
1
3
  class Shrine
2
4
  module Plugins
3
5
  # The migration_helpers plugin gives the attacher additional helper methods
4
6
  # which are convenient when doing file migrations.
5
7
  #
6
- # By default additional methods are also added to the model which delegate
7
- # to the underlying attacher. If you want to disable that, you can load the
8
- # plugin with `delegate: false`:
9
- #
10
- # plugin :migration_helpers, delegate: false
11
- #
12
- # ## `attachment_cache` and `attachment_store`
13
- #
14
- # These methods return cache and store uploaders used by the underlying
15
- # attacher:
16
- #
17
- # user.avatar_cache #=> #<Shrine @storage_key=:cache @storage=#<Shrine::Storage::FileSystem @directory=public/uploads>>
18
- # user.avatar_store #=> #<Shrine @storage_key=:store @storage=#<Shrine::Storage::S3:0x007fb8343397c8 @bucket=#<Aws::S3::Bucket name="foo">>>
19
- #
20
- # # attacher equivalents
21
- # user.avatar_attacher.cache
22
- # user.avatar_attacher.store
8
+ # The plugin also allows convenient delegating to these methods through the
9
+ # model, by setting `:delegate`:
23
10
  #
24
- # ## `attachment_cached?` and `attachment_stored?`
25
- #
26
- # These methods return true if attachment exists and is cached/stored:
11
+ # plugin :migration_helpers, delegate: true
27
12
  #
28
- # user.avatar_cached? # user.avatar && user.avatar_cache.uploaded?(user.avatar)
29
- # user.avatar_stored? # user.avatar && user.avatar_store.uploaded?(user.avatar)
30
- #
31
- # # attacher equivalents
32
- # user.avatar_attacher.cached?
33
- # user.avatar_attacher.stored?
34
- #
35
- # ## `update_attachment`
13
+ # ## `update_stored`
36
14
  #
37
15
  # This method updates the record's attachment with the result of the given
38
16
  # block.
39
17
  #
18
+ # user.avatar_attacher.update_stored do |avatar|
19
+ # user.avatar_attacher.store.upload(avatar) # saved to the record
20
+ # end
21
+ #
22
+ # # with model delegation
40
23
  # user.update_avatar do |avatar|
41
24
  # user.avatar_store.upload(avatar) # saved to the record
42
25
  # end
43
26
  #
44
- # # attacher equivalent
45
- # user.avatar_attacher.update_stored { |avatar| }
46
- #
47
27
  # The block will get triggered _only_ if the attachment is present and not
48
28
  # cached, *and* will save the record only if the record's attachment
49
29
  # hasn't changed in the time it took to execute the block. This method is
50
30
  # most useful for adding/removing versions and changing locations of files.
31
+ #
32
+ # ## `cached?` and `stored?`
33
+ #
34
+ # These methods return true if attachment exists and is cached/stored:
35
+ #
36
+ # user.avatar_attacher.cached? # user.avatar && user.avatar_attacher.cache.uploaded?(user.avatar)
37
+ # user.avatar_attacher.stored? # user.avatar && user.avatar_attacher.store.uploaded?(user.avatar)
38
+ #
39
+ # # with model delegation
40
+ # user.avatar_cached?
41
+ # user.avatar_stored?
42
+ #
43
+ # ## `attachment_cache` and `attachment_store`
44
+ #
45
+ # These methods return cache and store uploaders used by the underlying
46
+ # attacher:
47
+ #
48
+ # # these methods already exist without migration_helpers
49
+ # user.avatar_attacher.cache #=> #<Shrine @storage_key=:cache @storage=#<Shrine::Storage::FileSystem @directory=public/uploads>>
50
+ # user.avatar_attacher.store #=> #<Shrine @storage_key=:store @storage=#<Shrine::Storage::S3:0x007fb8343397c8 @bucket=#<Aws::S3::Bucket name="foo">>>
51
+ #
52
+ # # with model delegation
53
+ # user.avatar_cache
54
+ # user.avatar_store
51
55
  module MigrationHelpers
52
- def self.configure(uploader, options = {})
53
- warn "The :delegate option in migration_helpers Shrine plugin will default to false in Shrine 2. To remove this warning, set :delegate explicitly." if !options.key?(:delegate)
54
- uploader.opts[:migration_helpers_delegate] = options.fetch(:delegate, true)
56
+ def self.configure(uploader, delegate: false)
57
+ uploader.opts[:migration_helpers_delegate] = delegate
55
58
  end
56
59
 
57
60
  module AttachmentMethods
@@ -1,54 +1,36 @@
1
1
  class Shrine
2
2
  module Plugins
3
- # The moving plugin enables you to move files to specified storages. On
4
- # the filesystem moving is istantaneous, since the OS only changes the
5
- # pointer, so this plugin is useful when dealing with large files.
3
+ # The moving plugin makes so that when files are supposed to be uploaded,
4
+ # they are moved instead. For example, on FileSystem moving is
5
+ # instantaneous regardless of the filesize, so it's suitable for speeding
6
+ # up uploads for larger files.
6
7
  #
7
- # This plugin is also recommended if you're doing processing, since by
8
- # default temporary files won't immediately get deleted (Ruby's Tempfiles
9
- # usually get deleted only when the process ends).
8
+ # plugin :moving
10
9
  #
11
- # plugin :moving, storages: [:cache]
12
- #
13
- # The `:storages` option specifies which storages the file will be moved
14
- # to. The above will move raw files to cache (without this plugin it's
15
- # simply copied over). However, you may want to move cached files to
16
- # `:store` as well:
17
- #
18
- # plugin :moving, storages: [:cache, :store]
10
+ # By default files will be moved whenever the storage supports it. If you
11
+ # want moving to happen only for certain storages, you can set `storages`:
19
12
  #
20
- # What exactly means "moving"? If both the file being uploaded and the
21
- # destination are on the filesystem, a `mv` command will be executed
22
- # (making the transfer instantaneous). Some other storages may implement
23
- # moving as well, usually only for files which are on the same storage.
24
- # If moving isn't implemented by the storage, the file will be simply
25
- # deleted after upload.
13
+ # plugin :moving, storages: [:cache]
26
14
  module Moving
27
- def self.configure(uploader, storages:)
28
- uploader.opts[:moving_storages] = storages
15
+ def self.configure(uploader, opts = {})
16
+ uploader.opts[:moving_storages] = opts.fetch(:storages, uploader.opts[:moving_storages])
29
17
  end
30
18
 
31
19
  module InstanceMethods
32
20
  private
33
21
 
34
- # If the storage supports moving we use that, otherwise we do moving by
35
- # copying and deleting.
22
+ # Moves the file if storage supports it, otherwise defaults to copying.
36
23
  def copy(io, context)
37
24
  if move?(io, context)
38
- if movable?(io, context)
39
- move(io, context)
40
- else
41
- warn "The #{storage_key.inspect} Shrine storage doesn't support moving a #{io.inspect}. It is currently still deleted, but it won't be in Shrine 2."
42
- super
43
- io.delete if io.respond_to?(:delete)
44
- end
25
+ move(io, context)
45
26
  else
46
27
  super
47
28
  end
48
29
  end
49
30
 
50
31
  def move(io, context)
51
- storage.move(io, context[:location], context[:metadata])
32
+ context[:upload_options] = (context[:upload_options] || {}).merge(shrine_metadata: context[:metadata])
33
+ storage.move(io, context[:location], context[:upload_options])
52
34
  end
53
35
 
54
36
  def movable?(io, context)
@@ -56,6 +38,11 @@ class Shrine
56
38
  end
57
39
 
58
40
  def move?(io, context)
41
+ moving_storage? && movable?(io, context)
42
+ end
43
+
44
+ def moving_storage?
45
+ opts[:moving_storages].nil? ||
59
46
  opts[:moving_storages].include?(storage_key)
60
47
  end
61
48
  end
@@ -11,8 +11,8 @@ class Shrine
11
11
  #
12
12
  # plugin :parallelize, threads: 5
13
13
  module Parallelize
14
- def self.configure(uploader, threads: 3)
15
- uploader.opts[:parallelize_threads] = threads
14
+ def self.configure(uploader, opts = {})
15
+ uploader.opts[:parallelize_threads] = opts.fetch(:threads, uploader.opts.fetch(:parallelize_threads, 3))
16
16
  end
17
17
 
18
18
  module InstanceMethods
@@ -43,8 +43,8 @@ class Shrine
43
43
  end
44
44
 
45
45
  class ThreadPool
46
- def initialize(thread_count)
47
- @thread_count = thread_count
46
+ def initialize(size)
47
+ @size = size
48
48
  @tasks = Queue.new
49
49
  end
50
50
 
@@ -53,7 +53,7 @@ class Shrine
53
53
  end
54
54
 
55
55
  def perform
56
- threads = @thread_count.times.map { spawn_thread }
56
+ threads = @size.times.map { spawn_thread }
57
57
  threads.each(&:join)
58
58
  end
59
59
 
@@ -23,8 +23,8 @@ class Shrine
23
23
  # plugin :pretty_location, namespace: "/"
24
24
  # # "blog/user/.../493g82jf23.jpg"
25
25
  module PrettyLocation
26
- def self.configure(uploader, namespace: nil)
27
- uploader.opts[:pretty_location_namespace] = namespace
26
+ def self.configure(uploader, opts = {})
27
+ uploader.opts[:pretty_location_namespace] = opts.fetch(:namespace, uploader.opts[:pretty_location_namespace])
28
28
  end
29
29
 
30
30
  module InstanceMethods
@@ -44,10 +44,6 @@ class Shrine
44
44
 
45
45
  private
46
46
 
47
- def generate_uid(io)
48
- SecureRandom.hex(5)
49
- end
50
-
51
47
  def class_location(klass)
52
48
  parts = klass.name.downcase.split("::")
53
49
  if separator = opts[:pretty_location_namespace]