shrine 0.9.0 → 1.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.

@@ -16,20 +16,23 @@ class Shrine
16
16
  # end
17
17
  # end
18
18
  #
19
- # Each hook will be called with 2 arguments, `io` and `context`. It's
20
- # generally good to always call super when overriding a hook, especially if
21
- # you're using inheritance with your uploaders.
19
+ # Each hook will be called with 2 arguments, `io` and `context`. You should
20
+ # always call `super` when overriding a hook, as other plugins may be using
21
+ # hooks internally, and without `super` they wouldn't get executed.
22
22
  #
23
23
  # Shrine calls hooks in the following order when uploading a file:
24
24
  #
25
- # * `around_process`
26
- # * `before_process`
27
- # * PROCESS
28
- # * `after_process`
29
- # * `around_store`
30
- # * `before_store`
31
- # * STORE
32
- # * `after_store`
25
+ # * `around_upload`
26
+ # * `before_upload`
27
+ # * `around_process`
28
+ # * `before_process`
29
+ # * PROCESS
30
+ # * `after_process`
31
+ # * `around_store`
32
+ # * `before_store`
33
+ # * STORE
34
+ # * `after_store`
35
+ # * `after_upload`
33
36
  #
34
37
  # Shrine calls hooks in the following order when deleting a file:
35
38
  #
@@ -59,6 +62,25 @@ class Shrine
59
62
  # existing keys.
60
63
  module Hooks
61
64
  module InstanceMethods
65
+ def upload(io, context = {})
66
+ result = nil
67
+ around_upload(io, context) { result = super }
68
+ result
69
+ end
70
+
71
+ def around_upload(*args)
72
+ before_upload(*args)
73
+ yield
74
+ after_upload(*args)
75
+ end
76
+
77
+ def before_upload(*)
78
+ end
79
+
80
+ def after_upload(*)
81
+ end
82
+
83
+
62
84
  def processed(io, context)
63
85
  result = nil
64
86
  around_process(io, context) { result = super }
@@ -14,37 +14,22 @@ class Shrine
14
14
  # :replaced
15
15
  # : If set to `true`, uploading a new attachment won't delete the old one.
16
16
  #
17
- # :cached
18
- # : If set to `true`, cached files that are uploaded to store won't be
19
- # deleted.
20
- #
21
17
  # For example, the following will keep destroyed and replaced files:
22
18
  #
23
19
  # plugin :keep_files, destroyed: true, :replaced: true
24
20
  #
25
21
  # [event store]: http://docs.geteventstore.com/introduction/event-sourcing-basics/
26
22
  module KeepFiles
27
- def self.configure(uploader, destroyed: nil, replaced: nil, cached: nil)
23
+ def self.configure(uploader, destroyed: nil, replaced: nil, **)
28
24
  uploader.opts[:keep_files] = []
29
25
  uploader.opts[:keep_files] << :destroyed if destroyed
30
26
  uploader.opts[:keep_files] << :replaced if replaced
31
- uploader.opts[:keep_files] << :cached if cached
32
27
  end
33
28
 
34
- module ClassMethods
35
- def keep?(type)
36
- opts[:keep_files].include?(type)
37
- end
38
-
29
+ module InstanceMethods
39
30
  # We hook to the generic deleting, and check the appropriate phases.
40
- def delete(io, context)
41
- case context[:phase]
42
- when :cached then super unless keep?(:cached)
43
- when :replaced then super unless keep?(:replaced)
44
- when :destroyed then super unless keep?(:destroyed)
45
- else
46
- super
47
- end
31
+ def delete(io, context = {})
32
+ super unless opts[:keep_files].include?(context[:phase])
48
33
  end
49
34
  end
50
35
  end
@@ -79,21 +79,21 @@ class Shrine
79
79
 
80
80
  module InstanceMethods
81
81
  def store(io, context = {})
82
- log("store", context) { super }
82
+ log("store", io, context) { super }
83
83
  end
84
84
 
85
- def delete(uploaded_file, context = {})
86
- log("delete", context) { super }
85
+ def delete(io, context = {})
86
+ log("delete", io, context) { super }
87
87
  end
88
88
 
89
89
  private
90
90
 
91
91
  def processed(io, context = {})
92
- log("process", context) { super }
92
+ log("process", io, context) { super }
93
93
  end
94
94
 
95
95
  # Collects the data and sends it for logging.
96
- def log(action, context)
96
+ def log(action, io, context)
97
97
  result, duration = benchmark { yield }
98
98
 
99
99
  _log(
@@ -102,14 +102,15 @@ class Shrine
102
102
  uploader: self.class,
103
103
  attachment: context[:name],
104
104
  record_class: (context[:record].class if context[:record]),
105
- record_id: (context[:record].id if context[:record]),
106
- files: count(result),
105
+ record_id: (context[:record].id if context[:record].respond_to?(:id)),
106
+ files: count(io),
107
107
  duration: ("%.2f" % duration).to_f,
108
108
  ) unless result.nil?
109
109
 
110
110
  result
111
111
  end
112
112
 
113
+ # Determines format of logging and calls appropriate method.
113
114
  def _log(data)
114
115
  message = send("_log_message_#{opts[:logging_format]}", data)
115
116
  self.class.logger.info(message)
@@ -121,7 +122,8 @@ class Shrine
121
122
  components.last << "[#{data[:phase]}]" if data[:phase]
122
123
  components << "#{data[:uploader]}"
123
124
  components.last << "[:#{data[:attachment]}]" if data[:attachment]
124
- components << "#{data[:record_class]}[#{data[:record_id]}]" if data[:record_class]
125
+ components << "#{data[:record_class]}" if data[:record_class]
126
+ components.last << "[#{data[:record_id]}]" if data[:record_id]
125
127
  components << (data[:files] > 1 ? "#{data[:files]} files" : "#{data[:files]} file")
126
128
  components << "(#{data[:duration]}s)"
127
129
  components.join(" ")
@@ -139,9 +141,12 @@ class Shrine
139
141
  # hashes.
140
142
  def count(object)
141
143
  case object
142
- when Hash then object.count
143
- when Array then object.inject(0) { |sum, o| sum += count(o) }
144
- else 1
144
+ when Hash
145
+ object.count
146
+ when Array
147
+ object.inject(0) { |sum, o| sum += count(o) }
148
+ else
149
+ 1
145
150
  end
146
151
  end
147
152
 
@@ -20,9 +20,9 @@ class Shrine
20
20
  # user.avatar_store.upload(avatar) # saved to the record
21
21
  # end
22
22
  #
23
- # This will get triggered _only_ if the attachment exists and is stored.
24
- # The result can be anything that responds to `#to_json` and evaluates to
25
- # uploaded files' data.
23
+ # This will get triggered _only_ if the attachment is not nil and is
24
+ # stored. The result can be anything that responds to `#to_json` and
25
+ # evaluates to uploaded files' data.
26
26
  module MigrationHelpers
27
27
  module AttachmentMethods
28
28
  def initialize(name)
@@ -51,7 +51,7 @@ class Shrine
51
51
  attachment = get
52
52
  return if attachment.nil? || cache.uploaded?(attachment)
53
53
  new_attachment = block.call(attachment)
54
- update(new_attachment) unless changed?(get)
54
+ update(new_attachment) unless changed?(attachment)
55
55
  end
56
56
  end
57
57
  end
@@ -0,0 +1,46 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The module_include plugin allows you to extend Shrine's core classes for
4
+ # the given uploader with modules/methods.
5
+ #
6
+ # plugin :module_include
7
+ #
8
+ # To add a module to a core class, call the appropriate method:
9
+ #
10
+ # Shrine.attachment_module CustomAttachmentMethods
11
+ # Shrine.attacher_module CustomAttacherMethods
12
+ # Shrine.file_module CustomFileMethods
13
+ #
14
+ # Alternatively you can pass in a block (which internally creates a module):
15
+ #
16
+ # Shrine.file_module do
17
+ # def base64
18
+ # Base64.encode64(read)
19
+ # end
20
+ # end
21
+ module ModuleInclude
22
+ module ClassMethods
23
+ def attachment_module(mod = nil, &block)
24
+ module_include(self::Attachment, mod, &block)
25
+ end
26
+
27
+ def attacher_module(mod = nil, &block)
28
+ module_include(self::Attacher, mod, &block)
29
+ end
30
+
31
+ def file_module(mod = nil, &block)
32
+ module_include(self::UploadedFile, mod, &block)
33
+ end
34
+
35
+ private
36
+
37
+ def module_include(klass, mod, &block)
38
+ mod ||= Module.new(&block)
39
+ klass.include(mod)
40
+ end
41
+ end
42
+ end
43
+
44
+ register_plugin(:module_include, ModuleInclude)
45
+ end
46
+ end
@@ -43,22 +43,11 @@ class Shrine
43
43
  super
44
44
  io.delete if io.respond_to?(:delete)
45
45
  end
46
- # Promoting cached files will by default always delete the cached
47
- # file. But, if moving plugin is enabled we want the cached file to
48
- # be moved instead. However, there is no good way of letting the
49
- # Attacher know that it shouldn't attempt to delete the file, so we
50
- # make this instance variable hack.
51
- io.instance_variable_set("@shrine_deleted", true)
52
46
  else
53
47
  super
54
48
  end
55
49
  end
56
50
 
57
- # Don't delete the file if it has been moved.
58
- def remove(io, context)
59
- super unless io.instance_variable_get("@shrine_deleted")
60
- end
61
-
62
51
  # Ask the storage if the given file is movable.
63
52
  def movable?(io, context)
64
53
  storage.respond_to?(:move) && storage.movable?(io, context[:location])
@@ -5,9 +5,9 @@ class Shrine
5
5
  #
6
6
  # plugin :multi_delete
7
7
  #
8
- # This plugin allows you pass an array of files to `Shrine.delete`.
8
+ # This plugin allows you pass an array of files to `Shrine#delete`.
9
9
  #
10
- # Shrine.delete([file1, file2, file3])
10
+ # Shrine.new(:storage).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
@@ -15,15 +15,6 @@ class Shrine
15
15
  # at once.
16
16
  module MultiDelete
17
17
  module InstanceMethods
18
- # This allows `Shrine.delete` to accept an array of files.
19
- def uploaded?(uploaded_file)
20
- if uploaded_file.is_a?(Array)
21
- uploaded_file.all? { |file| super(file) }
22
- else
23
- super
24
- end
25
- end
26
-
27
18
  private
28
19
 
29
20
  # Adds the ability to upload multiple files, leveraging the underlying
@@ -33,7 +24,7 @@ class Shrine
33
24
  if storage.respond_to?(:multi_delete)
34
25
  storage.multi_delete(uploaded_file.map(&:id))
35
26
  else
36
- uploaded_file.map { |file| super(file, context) }
27
+ uploaded_file.map { |file| _delete(file, context) }
37
28
  end
38
29
  else
39
30
  super
@@ -0,0 +1,22 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The parsed_json plugin is suitable for the case when your framework is
4
+ # automatically parsing JSON query parameters, allowing you to assign
5
+ # cached files with hashes/arrays.
6
+ #
7
+ # plugin :parsed_json
8
+ module ParsedJson
9
+ module AttacherMethods
10
+ def assign(value)
11
+ if value.is_a?(Hash) && value.keys.any? { |key| key.is_a?(String) }
12
+ assign(value.to_json)
13
+ else
14
+ super
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ register_plugin(:parsed_json, ParsedJson)
21
+ end
22
+ end
@@ -0,0 +1,63 @@
1
+ require "forwardable"
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # The rack_file plugin enables models to accept Rack file hashes as
6
+ # attachments.
7
+ #
8
+ # rack_file #=>
9
+ # # {
10
+ # # filename: "cats.png",
11
+ # # type: "image/png",
12
+ # # tempfile: #<Tempfile:/var/folders/3n/3asd/-Tmp-/RackMultipart201-1476-nfw2-0>,
13
+ # # head: "Content-Disposition: form-data; ...",
14
+ # # }
15
+ # user.avatar = rack_file
16
+ # user.avatar.original_filename #=> "cats.png"
17
+ # user.avatar.mime_type #=> "image/png"
18
+ #
19
+ # Internally the plugin wraps the Rack file hash into an IO-like object,
20
+ # and this is what is passed to `Shrine#upload`.
21
+ #
22
+ # plugin :rack_file
23
+ module RackFile
24
+ module AttacherMethods
25
+ # Checks whether a file is a Rack file hash, and in that case wraps the
26
+ # hash in an IO-like object.
27
+ def assign(value)
28
+ if value.is_a?(Hash) && value.key?(:tempfile)
29
+ assign(UploadedFile.new(value))
30
+ else
31
+ super
32
+ end
33
+ end
34
+ end
35
+
36
+ # This is used to wrap the Rack hash into an IO-like object which Shrine
37
+ # can upload.
38
+ class UploadedFile
39
+ attr_reader :original_filename, :content_type
40
+ attr_accessor :tempfile
41
+
42
+ def initialize(tempfile:, filename: nil, type: nil, **)
43
+ @tempfile = tempfile
44
+ @original_filename = filename
45
+ @content_type = type
46
+ end
47
+
48
+ def path
49
+ @tempfile.path
50
+ end
51
+
52
+ def to_io
53
+ @tempfile
54
+ end
55
+
56
+ extend Forwardable
57
+ delegate Shrine::IO_METHODS.keys => :@tempfile
58
+ end
59
+ end
60
+
61
+ register_plugin(:rack_file, RackFile)
62
+ end
63
+ end
@@ -23,7 +23,7 @@ class Shrine
23
23
  module Recache
24
24
  module AttacherMethods
25
25
  def save
26
- if get && defined?(@old_attachment) # new file was assigned
26
+ if get && cache.uploaded?(get)
27
27
  _set cache!(get, phase: :recache)
28
28
  end
29
29
  super
@@ -21,8 +21,7 @@ class Shrine
21
21
  uploaded_file = uploaded_file(value) do |file|
22
22
  next unless cache.uploaded?(file)
23
23
  return unless file.exists?
24
- uploader = shrine_class.uploader_for(file)
25
- real_metadata = uploader.extract_metadata(file.to_io, context)
24
+ real_metadata = cache.extract_metadata(file.to_io, context)
26
25
  file.metadata.update(real_metadata)
27
26
  end
28
27
 
@@ -45,13 +45,12 @@ class Shrine
45
45
 
46
46
  def before_save
47
47
  super
48
- #{name}_attacher.save
48
+ #{name}_attacher.save if #{name}_attacher.attached?
49
49
  end
50
50
 
51
51
  def after_commit
52
52
  super
53
- #{name}_attacher.replace
54
- #{name}_attacher._promote
53
+ #{name}_attacher.finalize if #{name}_attacher.attached?
55
54
  end
56
55
 
57
56
  def after_destroy_commit
@@ -86,6 +85,13 @@ class Shrine
86
85
  record.reload
87
86
  super
88
87
  end
88
+
89
+ # Support for Postgres JSON columns.
90
+ def read
91
+ value = super
92
+ value = value.to_hash if value.respond_to?(:to_hash)
93
+ value
94
+ end
89
95
  end
90
96
  end
91
97
 
@@ -15,7 +15,7 @@ class Shrine
15
15
  #
16
16
  # The validation methods are instance-level, the `Attacher.validate` block
17
17
  # is evaluated in context of an instance of `Shrine::Attacher`, so you can
18
- # easily to conditional validation.
18
+ # easily do conditional validation.
19
19
  #
20
20
  # If you would like to change default validation error messages, you can
21
21
  # pass in the `:default_messages` option to the plugin: