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,89 @@
1
+ require "active_record"
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # The activerecord plugin extends the "attachment" interface with support
6
+ # for ActiveRecord.
7
+ #
8
+ # plugin :activerecord
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 on: [:create, :update]` -- Promotes the attachment, deletes replaced ones.
15
+ # * `after_commit on: [:destroy]` -- Deletes the attachment.
16
+ #
17
+ # Note that if your tests are wrapped in transactions, the `after_commit`
18
+ # callbacks won't get called, so in order to test uploading you should first
19
+ # disable these transactions for those tests.
20
+ #
21
+ # If you want to put some parts of this lifecycle into a background job, see
22
+ # the background_helpers plugin.
23
+ #
24
+ # Additionally, any Shrine validation errors will added to ActiveRecord's
25
+ # errors upon validation. Note that if you want to validate presence of the
26
+ # attachment, you can do it directly on the model.
27
+ #
28
+ # class User < ActiveRecord::Base
29
+ # include ImageUploader[:avatar]
30
+ # validates_presence_of :avatar
31
+ # end
32
+ module Activerecord
33
+ module AttachmentMethods
34
+ def included(model)
35
+ super
36
+
37
+ model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
38
+ validate do
39
+ #{@name}_attacher.errors.each do |message|
40
+ errors.add(:#{@name}, message)
41
+ end
42
+ end
43
+
44
+ before_save do
45
+ #{@name}_attacher.save
46
+ end
47
+
48
+ after_commit on: [:create, :update] do
49
+ #{@name}_attacher.replace
50
+ #{@name}_attacher._promote
51
+ end
52
+
53
+ after_commit on: :destroy do
54
+ #{@name}_attacher.destroy
55
+ end
56
+ RUBY
57
+ end
58
+ end
59
+
60
+ module AttacherClassMethods
61
+ # Needed by the background_helpers plugin.
62
+ def find_record(record_class, record_id)
63
+ record_class.find(record_id)
64
+ end
65
+ end
66
+
67
+ module AttacherMethods
68
+ private
69
+
70
+ # We save the record after updating, raising any validation errors.
71
+ def update(uploaded_file)
72
+ super
73
+ record.save!
74
+ end
75
+
76
+ # If we're in a transaction, then promoting is happening inline. If
77
+ # we're not, then this is happening in a background job. In that case
78
+ # when we're checking that the attachment changed during storing, we
79
+ # need to first reload the record to pick up new columns.
80
+ def changed?(uploaded_file)
81
+ record.reload
82
+ super
83
+ end
84
+ end
85
+ end
86
+
87
+ register_plugin(:activerecord, Activerecord)
88
+ end
89
+ end
@@ -0,0 +1,148 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The background_helpers plugin enables you to intercept phases of
4
+ # uploading and put them into background jobs. This doesn't require any
5
+ # additional columns.
6
+ #
7
+ # plugin :background_helpers
8
+ #
9
+ # ## Promoting
10
+ #
11
+ # If you're doing processing, or your `:store` is something other than
12
+ # Storage::FileSystem, it's recommended to put promoting (moving to store)
13
+ # into a background job. This plugin allows you to do that by calling
14
+ # `Shrine::Attacher.promote`:
15
+ #
16
+ # Shrine::Attacher.promote { |data| UploadJob.perform_async(data) }
17
+ #
18
+ # When you call `Shrine::Attacher.promote` with a block, it will save the
19
+ # block and call it on every promotion. Then in your background job you can
20
+ # again call `Shrine::Attacher.promote` with the data, and internally it
21
+ # will resolve all necessary objects, do the promoting and update the
22
+ # record.
23
+ #
24
+ # class UploadJob
25
+ # include Sidekiq::Worker
26
+ #
27
+ # def perform(data)
28
+ # Shrine::Attacher.promote(data)
29
+ # end
30
+ # end
31
+ #
32
+ # Shrine automatically handles all concurrency issues, such as canceling
33
+ # promoting if the attachment has changed in the meanwhile.
34
+ #
35
+ # ## Deleting
36
+ #
37
+ # If your `:store` is something other than Storage::FileSystem, it's
38
+ # recommended to put deleting files into a background job. This plugin
39
+ # allows you to do that by calling `Shrine::Attacher.delete`:
40
+ #
41
+ # Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
42
+ #
43
+ # When you call `Shrine::Attacher.delete` with a block, it will save the
44
+ # block and call it on every delete. Then in your background job you can
45
+ # again call `Shrine::Attacher.delete` with the data, and internally it
46
+ # will resolve all necessary objects, and delete the file.
47
+ #
48
+ # class DeleteJob
49
+ # include Sidekiq::Worker
50
+ #
51
+ # def perform(data)
52
+ # Shrine::Attacher.delete(data)
53
+ # end
54
+ # end
55
+ #
56
+ # ## Conclusion
57
+ #
58
+ # The examples above used Sidekiq, but obviously you can just as well use
59
+ # any other backgrounding library. Also, if you want you can use
60
+ # backgrounding just for certain uploaders:
61
+ #
62
+ # class ImageUploader < Shrine
63
+ # Attacher.promote { |data| UploadJob.perform_async(data) }
64
+ # Attacher.delete { |data| DeleteJob.perform_async(data) }
65
+ # end
66
+ #
67
+ # If you would like to speed up your uploads and deletes, you can use the
68
+ # parallelize plugin, either as a replacement or an addition to
69
+ # background_helpers.
70
+ module BackgroundHelpers
71
+ module AttacherClassMethods
72
+ # If block is passed in, stores it to be called on promotion. Otherwise
73
+ # resolves data into objects and calls Attacher#promote.
74
+ def promote(data = nil, &block)
75
+ if block
76
+ shrine_class.opts[:background_promote] = block
77
+ else
78
+ record_class, record_id = data["record"]
79
+ record_class = Object.const_get(record_class)
80
+ record = find_record(record_class, record_id)
81
+
82
+ name = data["attachment"]
83
+ attacher = record.send("#{name}_attacher")
84
+ cached_file = attacher.uploaded_file(data["uploaded_file"])
85
+
86
+ attacher.promote(cached_file)
87
+ end
88
+ end
89
+
90
+ # If block is passed in, stores it to be called on deletion. Otherwise
91
+ # resolves data into objects and calls Shrine.delete.
92
+ def delete(data = nil, &block)
93
+ if block
94
+ shrine_class.opts[:background_delete] = block
95
+ else
96
+ record_class, record_id = data["record"]
97
+ record = Object.const_get(record_class).new
98
+ record.id = record_id
99
+
100
+ name, phase = data["attachment"], data["phase"]
101
+ shrine_class = record.send("#{name}_attacher").shrine_class
102
+ uploaded_file = shrine_class.uploaded_file(data["uploaded_file"])
103
+ context = {name: name.to_sym, record: record, phase: phase.to_sym}
104
+
105
+ shrine_class.delete(uploaded_file, context)
106
+ end
107
+ end
108
+ end
109
+
110
+ module AttacherMethods
111
+ # Calls the promoting block with the data if it's been registered.
112
+ def _promote
113
+ if background_promote = shrine_class.opts[:background_promote]
114
+ data = {
115
+ "uploaded_file" => get.to_json,
116
+ "record" => [record.class.to_s, record.id],
117
+ "attachment" => name,
118
+ }
119
+
120
+ instance_exec(data, &background_promote) if promote?(get)
121
+ else
122
+ super
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ # Calls the deleting block with the data if it's been registered.
129
+ def delete!(uploaded_file, phase:)
130
+ if background_delete = shrine_class.opts[:background_delete]
131
+ data = {
132
+ "uploaded_file" => uploaded_file.to_json,
133
+ "record" => [record.class.to_s, record.id],
134
+ "attachment" => name,
135
+ "phase" => phase,
136
+ }
137
+
138
+ instance_exec(data, &background_delete)
139
+ else
140
+ super
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ register_plugin(:background_helpers, BackgroundHelpers)
147
+ end
148
+ end
@@ -0,0 +1,47 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The cached_attachment_data adds a method for assigning cached files that
4
+ # is more convenient for forms.
5
+ #
6
+ # plugin :cached_attachment_data
7
+ #
8
+ # If for example your attachment is called "avatar", this plugin will add
9
+ # `#cached_avatar_data` and `#cached_avatar_data=` methods to your model.
10
+ # This allows you to write your hidden field without explicitly setting
11
+ # `:value`:
12
+ #
13
+ # <%= form_for @user do |f| %>
14
+ # <%= f.hidden_field :cached_avatar_data %>
15
+ # <%= f.field_field :avatar %>
16
+ # <% end %>
17
+ #
18
+ # Additionally, the hidden field will only be set when the attachment is
19
+ # cached (as opposed to the default where `user.avatar_data` will return
20
+ # both cached and stored files). This keeps Rails logs cleaner.
21
+ module CachedAttachmentData
22
+ module AttachmentMethods
23
+ def initialize(name)
24
+ super
25
+
26
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
27
+ def cached_#{name}_data
28
+ #{name}_attacher.read_cached
29
+ end
30
+
31
+ def cached_#{name}_data=(value)
32
+ #{name}_attacher.assign(value)
33
+ end
34
+ RUBY
35
+ end
36
+ end
37
+
38
+ module AttacherMethods
39
+ def read_cached
40
+ get.to_json if get && cache.uploaded?(get)
41
+ end
42
+ end
43
+ end
44
+
45
+ register_plugin(:cached_attachment_data, CachedAttachmentData)
46
+ end
47
+ end
@@ -0,0 +1,93 @@
1
+ require "base64"
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # The data_uri plugin enables you to upload files as [data URIs].
6
+ # This plugin is useful for example when using [HTML5 Canvas].
7
+ #
8
+ # plugin :data_uri
9
+ #
10
+ # If for example your attachment is called "avatar", this plugin will add
11
+ # `#avatar_data_uri` and `#avatar_data_uri=` methods to your model.
12
+ #
13
+ # user.avatar #=> nil
14
+ # user.avatar_data_uri = "data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"
15
+ # user.avatar #=> #<Shrine::UploadedFile>
16
+ #
17
+ # user.avatar.mime_type #=> "image/jpeg"
18
+ # user.avatar.size #=> 43423
19
+ # user.avatar.original_filename #=> nil
20
+ #
21
+ # If the data URI wasn't correctly parsed, an error message will added to
22
+ # the attachment column. You can change the default error message:
23
+ #
24
+ # plugin :data_uri, error_message: "data URI was invalid"
25
+ # plugin :data_uri, error_message: ->(uri) { I18n.t("errors.data_uri_invalid") }
26
+ #
27
+ # [data URIs]: https://tools.ietf.org/html/rfc2397
28
+ # [HTML5 Canvas]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
29
+ module DataUri
30
+ DEFAULT_ERROR_MESSAGE = "data URI was invalid"
31
+ DEFAULT_CONTENT_TYPE = "text/plain"
32
+ DATA_URI_REGEXP = /\Adata:([-\w]+\/[-\w\+\.]+)?;base64,(.*)/m
33
+
34
+ def self.configure(uploader, error_message: DEFAULT_ERROR_MESSAGE)
35
+ uploader.opts[:data_uri_error_message] = error_message
36
+ end
37
+
38
+ module AttachmentMethods
39
+ def initialize(name)
40
+ super
41
+
42
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
43
+ def #{name}_data_uri=(uri)
44
+ #{name}_attacher.data_uri = uri
45
+ end
46
+
47
+ def #{name}_data_uri
48
+ #{name}_attacher.data_uri
49
+ end
50
+ RUBY
51
+ end
52
+ end
53
+
54
+ module AttacherMethods
55
+ # Handles assignment of a data URI. If the regexp matches, it extracts
56
+ # the content type, decodes it, wrappes it in a StringIO and assigns it.
57
+ # If it fails, it sets the error message and assigns the uri in an
58
+ # instance variable so that it shows up on the UI.
59
+ def data_uri=(uri)
60
+ return if uri == ""
61
+
62
+ if match = uri.match(DATA_URI_REGEXP)
63
+ content_type = match[1] || DEFAULT_CONTENT_TYPE
64
+ content = Base64.decode64(match[2])
65
+
66
+ assign DataFile.new(content, content_type: content_type)
67
+ else
68
+ message = shrine_class.opts[:data_uri_error_message]
69
+ message = message.call(uri) if message.respond_to?(:call)
70
+ errors << message
71
+ @data_uri = uri
72
+ end
73
+ end
74
+
75
+ # Form builders require the reader as well.
76
+ def data_uri
77
+ @data_uri
78
+ end
79
+ end
80
+
81
+ class DataFile < StringIO
82
+ attr_reader :content_type
83
+
84
+ def initialize(content, content_type: nil)
85
+ @content_type = content_type
86
+ super(content)
87
+ end
88
+ end
89
+ end
90
+
91
+ register_plugin(:data_uri, DataUri)
92
+ end
93
+ end
@@ -0,0 +1,39 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The default_storage plugin enables you to change which storages are going
4
+ # to be used for this uploader's attacher (the default is `:cache` and
5
+ # `:store`).
6
+ #
7
+ # plugin :default_storage, cache: :special_cache, store: :special_store
8
+ #
9
+ # You can also pass a block and choose the values depending on the record
10
+ # values and the name of the attachment. This is useful if you're using the
11
+ # dynamic_storage plugin. Example:
12
+ #
13
+ # plugin :default_storage, store: ->(record, name) { :"store_#{record.username}" }
14
+ module DefaultStorage
15
+ def self.configure(uploader, cache: nil, store: nil)
16
+ uploader.opts[:default_storage_cache] = cache
17
+ uploader.opts[:default_storage_store] = store
18
+ end
19
+
20
+ module AttacherMethods
21
+ def initialize(record, name, **options)
22
+ if cache = shrine_class.opts[:default_storage_cache]
23
+ cache = cache.call(record, name) if cache.respond_to?(:call)
24
+ options[:cache] = cache
25
+ end
26
+
27
+ if store = shrine_class.opts[:default_storage_store]
28
+ store = store.call(record, name) if store.respond_to?(:call)
29
+ options[:store] = store
30
+ end
31
+
32
+ super
33
+ end
34
+ end
35
+ end
36
+
37
+ register_plugin(:default_storage, DefaultStorage)
38
+ end
39
+ end
@@ -0,0 +1,25 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The delete_invalid plugin immediately deletes the assigned attachment if
4
+ # it failed validation.
5
+ #
6
+ # plugin :delete_invalid
7
+ #
8
+ # By default an attachment is always cached before it's validated. This
9
+ # way the attachment will persist when the form is resubmitted, which is
10
+ # consistent with the other fields in the form. However, if this is a
11
+ # concern, you can load this plugin.
12
+ module DeleteInvalid
13
+ module AttacherMethods
14
+ # Delete the assigned uploaded file if it was invalid.
15
+ def validate
16
+ super
17
+ ensure
18
+ delete!(get, phase: :invalid) if !errors.empty?
19
+ end
20
+ end
21
+ end
22
+
23
+ register_plugin(:delete_invalid, DeleteInvalid)
24
+ end
25
+ end
@@ -0,0 +1,119 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The determine_mime_type plugin stores the actual MIME type of the
4
+ # uploaded file.
5
+ #
6
+ # plugin :determine_mime_type
7
+ #
8
+ # The plugin accepts the following analyzers:
9
+ #
10
+ # :file
11
+ # : (Default). Uses the UNIX [file] utility to determine the MIME type
12
+ # from file contents.
13
+ #
14
+ # :filemagic
15
+ # : Uses the [ruby-filemagic] gem to determine the MIME type from file
16
+ # contents, using a similar MIME database as the `file` utility.
17
+ # Unlike the `file` utility, ruby-filemagic should work on Windows.
18
+ #
19
+ # :mimemagic
20
+ # : Uses the [mimemagic] gem to determine the MIME type from file contents.
21
+ # Unlike ruby-filemagic, mimemagic is a pure-ruby solution, so it will
22
+ # work across all Ruby implementations.
23
+ #
24
+ # :mime_types
25
+ # : Uses the [mime-types] gem to determine the MIME type from the file
26
+ # *extension*. Note that unlike other solutions, this analyzer is not
27
+ # guaranteed to return the actual MIME type of the file.
28
+ #
29
+ # By default the UNIX [file] utility is used to detrmine the MIME type, but
30
+ # you can change it:
31
+ #
32
+ # plugin :determine_mime_type, analyzer: :filemagic
33
+ #
34
+ # If none of these quite suit your needs, you can use a custom analyzer:
35
+ #
36
+ # plugin :determine_mime_type, analyzer: ->(io) do
37
+ # if io.path.end_with?(".odt")
38
+ # "application/vnd.oasis.opendocument.text"
39
+ # else
40
+ # MimeMagic.by_magic(io).type
41
+ # end
42
+ # end
43
+ #
44
+ # [file]: http://linux.die.net/man/1/file
45
+ # [ruby-filemagic]: https://github.com/blackwinter/ruby-filemagic
46
+ # [mimemagic]: https://github.com/minad/mimemagic
47
+ # [mime-types]: https://github.com/mime-types/ruby-mime-types
48
+ module DetermineMimeType
49
+ def self.load_dependencies(uploader, analyzer: :file)
50
+ case analyzer
51
+ when :file then require "open3"
52
+ when :filemagic then require "filemagic"
53
+ when :mimemagic then require "mimemagic"
54
+ when :mime_types
55
+ begin
56
+ require "mime/types/columnar"
57
+ rescue LoadError
58
+ require "mime/types"
59
+ end
60
+ end
61
+ end
62
+
63
+ def self.configure(uploader, analyzer: :file)
64
+ uploader.opts[:mime_type_analyzer] = analyzer
65
+ end
66
+
67
+ module InstanceMethods
68
+ # If a Shrine::UploadedFile was given, it returns its MIME type, since
69
+ # that value was already determined by this analyzer. Otherwise it calls
70
+ # a built-in analyzer or a custom one.
71
+ def extract_mime_type(io)
72
+ analyzer = opts[:mime_type_analyzer]
73
+
74
+ if io.respond_to?(:mime_type)
75
+ io.mime_type
76
+ elsif analyzer.is_a?(Symbol)
77
+ send(:"_extract_mime_type_with_#{analyzer}", io)
78
+ else
79
+ analyzer.call(io)
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ # Uses the UNIX file utility to extract the MIME type. It does so only
86
+ # if it's a file, because even though the utility accepts standard
87
+ # input, it would mean that we have to read the whole file in memory.
88
+ def _extract_mime_type_with_file(io)
89
+ if io.respond_to?(:path)
90
+ mime_type, _ = Open3.capture2("file", "-b", "--mime-type", io.path)
91
+ mime_type.strip unless mime_type.empty?
92
+ end
93
+ end
94
+
95
+ # Uses the ruby-filemagic gem to magically extract the MIME type.
96
+ def _extract_mime_type_with_filemagic(io)
97
+ filemagic = FileMagic.new(FileMagic::MAGIC_MIME_TYPE)
98
+ data = io.read(1024); io.rewind
99
+ filemagic.buffer(data)
100
+ end
101
+
102
+ # Uses the mimemagic gem to extract the MIME type.
103
+ def _extract_mime_type_with_mimemagic(io)
104
+ MimeMagic.by_magic(io).type
105
+ end
106
+
107
+ # Uses the mime-types gem to determine MIME type from file extension.
108
+ def _extract_mime_type_with_mime_types(io)
109
+ if filename = extract_filename(io)
110
+ mime_type = MIME::Types.of(filename).first
111
+ mime_type.to_s if mime_type
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ register_plugin(:determine_mime_type, DetermineMimeType)
118
+ end
119
+ end