shrine 0.9.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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +663 -0
  4. data/doc/creating_plugins.md +100 -0
  5. data/doc/creating_storages.md +108 -0
  6. data/doc/direct_s3.md +97 -0
  7. data/doc/migrating_storage.md +79 -0
  8. data/doc/regenerating_versions.md +38 -0
  9. data/lib/shrine.rb +806 -0
  10. data/lib/shrine/plugins/activerecord.rb +89 -0
  11. data/lib/shrine/plugins/background_helpers.rb +148 -0
  12. data/lib/shrine/plugins/cached_attachment_data.rb +47 -0
  13. data/lib/shrine/plugins/data_uri.rb +93 -0
  14. data/lib/shrine/plugins/default_storage.rb +39 -0
  15. data/lib/shrine/plugins/delete_invalid.rb +25 -0
  16. data/lib/shrine/plugins/determine_mime_type.rb +119 -0
  17. data/lib/shrine/plugins/direct_upload.rb +274 -0
  18. data/lib/shrine/plugins/dynamic_storage.rb +57 -0
  19. data/lib/shrine/plugins/hooks.rb +123 -0
  20. data/lib/shrine/plugins/included.rb +48 -0
  21. data/lib/shrine/plugins/keep_files.rb +54 -0
  22. data/lib/shrine/plugins/logging.rb +158 -0
  23. data/lib/shrine/plugins/migration_helpers.rb +61 -0
  24. data/lib/shrine/plugins/moving.rb +75 -0
  25. data/lib/shrine/plugins/multi_delete.rb +47 -0
  26. data/lib/shrine/plugins/parallelize.rb +62 -0
  27. data/lib/shrine/plugins/pretty_location.rb +32 -0
  28. data/lib/shrine/plugins/recache.rb +36 -0
  29. data/lib/shrine/plugins/remote_url.rb +127 -0
  30. data/lib/shrine/plugins/remove_attachment.rb +59 -0
  31. data/lib/shrine/plugins/restore_cached.rb +36 -0
  32. data/lib/shrine/plugins/sequel.rb +94 -0
  33. data/lib/shrine/plugins/store_dimensions.rb +82 -0
  34. data/lib/shrine/plugins/validation_helpers.rb +168 -0
  35. data/lib/shrine/plugins/versions.rb +177 -0
  36. data/lib/shrine/storage/file_system.rb +165 -0
  37. data/lib/shrine/storage/linter.rb +94 -0
  38. data/lib/shrine/storage/s3.rb +118 -0
  39. data/lib/shrine/version.rb +14 -0
  40. data/shrine.gemspec +46 -0
  41. metadata +364 -0
@@ -0,0 +1,62 @@
1
+ require "thread/pool"
2
+
3
+ Thread::Pool.abort_on_exception = true
4
+
5
+ class Shrine
6
+ module Plugins
7
+ # The parallelize plugin parallelizes your uploads and deletes using the
8
+ # [thread] gem.
9
+ #
10
+ # plugin :parallelize
11
+ #
12
+ # This plugin is generally only useful as an addition to the versions
13
+ # plugin, where multiple files are being uploaded and deleted at once. Note
14
+ # that it's not possible for this plugin to parallelize processing, but it
15
+ # should be easy to do that manually.
16
+ #
17
+ # By default a pool of 3 threads will be used, but you can change that:
18
+ #
19
+ # plugin :parallelize, threads: 5
20
+ #
21
+ # [thread]: https://github.com/meh/ruby-thread
22
+ module Parallelize
23
+ def self.configure(uploader, threads: 3)
24
+ uploader.opts[:parallelize_threads] = threads
25
+ end
26
+
27
+ module InstanceMethods
28
+ def store(io, context = {})
29
+ with_pool { |pool| super(io, thread_pool: pool, **context) }
30
+ end
31
+
32
+ def delete(uploaded_file, context = {})
33
+ with_pool { |pool| super(uploaded_file, thread_pool: pool, **context) }
34
+ end
35
+
36
+ private
37
+
38
+ def copy(io, context)
39
+ context[:thread_pool].process { super }
40
+ end
41
+
42
+ def move(io, context)
43
+ context[:thread_pool].process { super }
44
+ end
45
+
46
+ def remove(uploaded_file, context)
47
+ context[:thread_pool].process { super }
48
+ end
49
+
50
+ # We initialize a thread pool with configured number of threads.
51
+ def with_pool
52
+ pool = Thread.pool(opts[:parallelize_threads])
53
+ result = yield pool
54
+ pool.shutdown
55
+ result
56
+ end
57
+ end
58
+ end
59
+
60
+ register_plugin(:parallelize, Parallelize)
61
+ end
62
+ end
@@ -0,0 +1,32 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The pretty_location plugin attempts to generate a nicer folder structure
4
+ # for uploaded files.
5
+ #
6
+ # plugin :pretty_location
7
+ #
8
+ # This plugin uses the context information from the Attacher to try to
9
+ # generate a nested folder structure which separates files for each record.
10
+ # The newly generated locations will typically look like this:
11
+ #
12
+ # "user/564/avatar/thumb-493g82jf23.jpg"
13
+ # # :model/:id/:attachment/:version-:uid.:extension
14
+ module PrettyLocation
15
+ module InstanceMethods
16
+ def generate_location(io, context)
17
+ type = context[:record].class.name.downcase if context[:record] && context[:record].class.name
18
+ id = context[:record].id if context[:record].respond_to?(:id)
19
+ name = context[:name]
20
+
21
+ dirname, slash, basename = super.rpartition("/")
22
+ basename = "#{context[:version]}-#{basename}" if context[:version]
23
+ original = dirname + slash + basename
24
+
25
+ [type, id, name, original].compact.join("/")
26
+ end
27
+ end
28
+ end
29
+
30
+ register_plugin(:pretty_location, PrettyLocation)
31
+ end
32
+ end
@@ -0,0 +1,36 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The recache plugin allows you to process your attachment after
4
+ # validations succeed, but before the attachment is promoted. This is
5
+ # useful for example when you want to generate some versions upfront (so
6
+ # the user immediately sees them) and other versions you want to generate
7
+ # in the promotion phase in a background job.
8
+ #
9
+ # The phase will be set to `:recache`:
10
+ #
11
+ # class ImageUploader
12
+ # plugin :recache
13
+ #
14
+ # def process(io, context)
15
+ # case context[:phase]
16
+ # when :recache
17
+ # # generate cheap versions
18
+ # when :store
19
+ # # generate more expensive versions
20
+ # end
21
+ # end
22
+ # end
23
+ module Recache
24
+ module AttacherMethods
25
+ def save
26
+ if get && defined?(@old_attachment) # new file was assigned
27
+ _set cache!(get, phase: :recache)
28
+ end
29
+ super
30
+ end
31
+ end
32
+ end
33
+
34
+ register_plugin(:recache, Recache)
35
+ end
36
+ end
@@ -0,0 +1,127 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The remote_url plugin allows you to attach files from a remote location.
4
+ #
5
+ # plugin :remote_url, max_size: 20*1024*1024
6
+ #
7
+ # If for example your attachment is called "avatar", this plugin will add
8
+ # `#avatar_remote_url` and `#avatar_remote_url=` methods to your model.
9
+ #
10
+ # user.avatar #=> nil
11
+ # user.avatar_remote_url = "http://example.com/cool-image.png"
12
+ # user.avatar #=> #<Shrine::UploadedFile>
13
+ #
14
+ # user.avatar.mime_type #=> "image/png"
15
+ # user.avatar.size #=> 43423
16
+ # user.avatar.original_filename #=> "cool-image.png"
17
+ #
18
+ # The file will by default be downloaded using Ruby's open-uri standard
19
+ # library. Following redirects is disabled for security reasons. It's also
20
+ # good practice to limit the filesize of the remote file:
21
+ #
22
+ # plugin :remote_url, max_size: 20*1024*1024 # 20 MB
23
+ #
24
+ # Now if a file that is bigger than 20MB is assigned, Shrine will terminate
25
+ # the download as soon as it gets the "Content-Length" header, or the
26
+ # buffer size surpasses the maximum size. However, if for whatever reason
27
+ # you don't want to limit the maximum file size, you can set `:max_size` to
28
+ # nil:
29
+ #
30
+ # plugin :remote_url, max_size: nil
31
+ #
32
+ # If download fails, either because the remote file wasn't found, was too
33
+ # large, or the request redirected, an error will be added to the
34
+ # attachment. You can change the default error message:
35
+ #
36
+ # plugin :remote_url, error_message: "download failed"
37
+ # plugin :remote_url, error_message: ->(url) { I18n.t("errors.download_failed") }
38
+ #
39
+ # Finally, if for some reason the way the file is downloaded doesn't suit
40
+ # your needs, you can provide a custom downloader:
41
+ #
42
+ # plugin :remote_url, downloader: ->(url) do
43
+ # request = RestClient::Request.new(method: :get, url: url, raw_response: true)
44
+ # response = request.execute
45
+ # response.file
46
+ # end
47
+ module RemoteUrl
48
+ DEFAULT_ERROR_MESSAGE = "file was not found or was too large"
49
+
50
+ def self.load_dependencies(uploader, downloader: :open_uri, **)
51
+ case downloader
52
+ when :open_uri then require "down"
53
+ end
54
+ end
55
+
56
+ def self.configure(uploader, downloader: :open_uri, error_message: nil, max_size:)
57
+ uploader.opts[:remote_url_downloader] = downloader
58
+ uploader.opts[:remote_url_error_message] = error_message || DEFAULT_ERROR_MESSAGE
59
+ uploader.opts[:remote_url_max_size] = max_size
60
+ end
61
+
62
+ module AttachmentMethods
63
+ def initialize(name)
64
+ super
65
+
66
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
67
+ def #{name}_remote_url=(url)
68
+ #{name}_attacher.remote_url = url
69
+ end
70
+
71
+ def #{name}_remote_url
72
+ #{name}_attacher.remote_url
73
+ end
74
+ RUBY
75
+ end
76
+ end
77
+
78
+ module AttacherMethods
79
+ # Downloads the remote file and assigns it. If download failed, sets
80
+ # the error message and assigns the url to an instance variable so that
81
+ # it shows up in the form.
82
+ def remote_url=(url)
83
+ return if url == ""
84
+
85
+ if downloaded_file = download(url)
86
+ assign(downloaded_file)
87
+ else
88
+ message = shrine_class.opts[:remote_url_error_message]
89
+ message = message.call(url) if message.respond_to?(:call)
90
+ errors << message
91
+ @remote_url = url
92
+ end
93
+ end
94
+
95
+ # Form builders require the reader as well.
96
+ def remote_url
97
+ @remote_url
98
+ end
99
+
100
+ private
101
+
102
+ # Downloads the file using the "down" gem or a custom downloader.
103
+ # Checks the file size and terminates the download early if the file
104
+ # is too big.
105
+ def download(url)
106
+ downloader = shrine_class.opts[:remote_url_downloader]
107
+ max_size = shrine_class.opts[:remote_url_max_size]
108
+
109
+ if downloader.is_a?(Symbol)
110
+ send(:"download_with_#{downloader}", url, max_size: max_size)
111
+ else
112
+ downloader.call(url, max_size: max_size)
113
+ end
114
+ end
115
+
116
+ # We silence any download errors, because for the user's point of view
117
+ # the download simply failed.
118
+ def download_with_open_uri(url, max_size:)
119
+ Down.download(url, max_size: max_size)
120
+ rescue Down::Error
121
+ end
122
+ end
123
+ end
124
+
125
+ register_plugin(:remote_url, RemoteUrl)
126
+ end
127
+ end
@@ -0,0 +1,59 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The remove_attachment plugin allows you to delete attachments through
4
+ # checkboxes on the web form.
5
+ #
6
+ # plugin :remove_attachment
7
+ #
8
+ # If for example your attachment is called "avatar", this plugin will add
9
+ # `#remove_avatar` and `#remove_avatar=` methods to your model. This allows
10
+ # you to easily enable deleting attached files through the form:
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.
20
+ module RemoveAttachment
21
+ module AttachmentMethods
22
+ def initialize(name)
23
+ super
24
+
25
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
26
+ def remove_#{name}=(value)
27
+ #{name}_attacher.remove = value
28
+ end
29
+
30
+ def remove_#{name}
31
+ #{name}_attacher.remove
32
+ end
33
+ RUBY
34
+ end
35
+ end
36
+
37
+ module AttacherMethods
38
+ # We remove the attachment if the value evaluates to true.
39
+ def remove=(value)
40
+ @remove = value
41
+ set(nil) if remove?
42
+ end
43
+
44
+ def remove
45
+ @remove
46
+ end
47
+
48
+ private
49
+
50
+ # Rails sends "0" or "false" if the checkbox hasn't been ticked.
51
+ def remove?
52
+ remove && remove != "" && remove !~ /\A0|false$\z/
53
+ end
54
+ end
55
+ end
56
+
57
+ register_plugin(:remove_attachment, RemoveAttachment)
58
+ end
59
+ end
@@ -0,0 +1,36 @@
1
+ require "json"
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # The restore_cached plugin ensures the cached file data hasn't been
6
+ # tampered with, by restoring its metadata after assignment. The user can
7
+ # tamper with the cached file data by modifying the hidden field before
8
+ # submitting the form.
9
+ #
10
+ # Firstly the assignment is terminated if the cached file doesn't exist,
11
+ # which can happen if the user changes the "id" or "storage" data. If the
12
+ # cached file exists, the metadata is reextracted from the original file
13
+ # and replaced with the potentially tampered with ones.
14
+ #
15
+ # plugin :restore_cached
16
+ module RestoreCached
17
+ module AttacherMethods
18
+ private
19
+
20
+ def assign_cached(value)
21
+ uploaded_file = uploaded_file(value) do |file|
22
+ next unless cache.uploaded?(file)
23
+ return unless file.exists?
24
+ uploader = shrine_class.uploader_for(file)
25
+ real_metadata = uploader.extract_metadata(file.to_io, context)
26
+ file.metadata.update(real_metadata)
27
+ end
28
+
29
+ super(uploaded_file)
30
+ end
31
+ end
32
+ end
33
+
34
+ register_plugin(:restore_cached, RestoreCached)
35
+ end
36
+ end
@@ -0,0 +1,94 @@
1
+ require "sequel"
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # The sequel plugin extends the "attachment" interface with support for
6
+ # Sequel.
7
+ #
8
+ # plugin :sequel
9
+ #
10
+ # Now whenever an "attachment" module is included, additional callbacks are
11
+ # added to the model:
12
+ #
13
+ # * `before_save` -- Currently only used by the recache plugin.
14
+ # * `after_commit` -- Promotes the attachment, deletes replaced ones.
15
+ # * `after_destroy_commit` -- Deletes the attachment.
16
+ #
17
+ # Note that if your tests are wrapped in transactions, the `after_commit`
18
+ # and `after_destroy_commit` callbacks won't get called, so in order to
19
+ # test uploading you should first disable these transactions for those
20
+ # tests.
21
+ #
22
+ # If you want to put some parts of this lifecycle into a background job, see
23
+ # the background_helpers plugin.
24
+ #
25
+ # Additionally, any Shrine validation errors will added to Sequel's
26
+ # errors upon validation. Note that if you want to validate presence of the
27
+ # attachment, you can do it directly on the model.
28
+ #
29
+ # class User < Sequel::Model
30
+ # include ImageUploader[:avatar]
31
+ # validates_presence_of :avatar
32
+ # end
33
+ module Sequel
34
+ module AttachmentMethods
35
+ def initialize(name)
36
+ super
37
+
38
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
39
+ def validate
40
+ super
41
+ #{name}_attacher.errors.each do |message|
42
+ errors.add(:#{name}, message)
43
+ end
44
+ end
45
+
46
+ def before_save
47
+ super
48
+ #{name}_attacher.save
49
+ end
50
+
51
+ def after_commit
52
+ super
53
+ #{name}_attacher.replace
54
+ #{name}_attacher._promote
55
+ end
56
+
57
+ def after_destroy_commit
58
+ super
59
+ #{name}_attacher.destroy
60
+ end
61
+ RUBY
62
+ end
63
+ end
64
+
65
+ module AttacherClassMethods
66
+ # Needed by the background_helpers plugin.
67
+ def find_record(record_class, record_id)
68
+ record_class.with_pk!(record_id)
69
+ end
70
+ end
71
+
72
+ module AttacherMethods
73
+ private
74
+
75
+ # We save the record after updating, raising any validation errors.
76
+ def update(uploaded_file)
77
+ super
78
+ record.save(raise_on_failure: true)
79
+ end
80
+
81
+ # If we're in a transaction, then promoting is happening inline. If
82
+ # we're not, then this is happening in a background job. In that case
83
+ # when we're checking that the attachment changed during storing, we
84
+ # need to first reload the record to pick up new columns.
85
+ def changed?(uploaded_file)
86
+ record.reload
87
+ super
88
+ end
89
+ end
90
+ end
91
+
92
+ register_plugin(:sequel, Sequel)
93
+ end
94
+ end