shrine 2.1.1 → 2.2.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.

data/lib/shrine.rb CHANGED
@@ -209,13 +209,11 @@ class Shrine
209
209
 
210
210
  # User is expected to perform processing inside of this method, and
211
211
  # return the processed files. Returning nil signals that no proccessing
212
- # has been done and that the original file should be used. When used
213
- # with Shrine::Attachment, the context variable will hold the record,
214
- # name of the attachment and the phase.
212
+ # has been done and that the original file should be used.
215
213
  #
216
214
  # class ImageUploader < Shrine
217
215
  # def process(io, context)
218
- # case context[:phase]
216
+ # case context[:action]
219
217
  # when :cache
220
218
  # # do processing
221
219
  # when :store
@@ -472,7 +470,7 @@ class Shrine
472
470
  return if value == "" || value == read || !cache.uploaded?(uploaded_file(value))
473
471
  assign_cached(uploaded_file(value))
474
472
  else
475
- uploaded_file = cache!(value, phase: :cache) if value
473
+ uploaded_file = cache!(value, action: :cache) if value
476
474
  set(uploaded_file)
477
475
  end
478
476
  end
@@ -504,19 +502,19 @@ class Shrine
504
502
  def finalize
505
503
  replace
506
504
  remove_instance_variable(:@old)
507
- _promote(phase: :store) if cached?
505
+ _promote(action: :store) if cached?
508
506
  end
509
507
 
510
508
  # Promotes the file.
511
- def _promote(uploaded_file = get, phase: nil)
512
- promote(uploaded_file, phase: phase)
509
+ def _promote(uploaded_file = get, **options)
510
+ promote(uploaded_file, **options)
513
511
  end
514
512
 
515
513
  # Uploads the cached file to store, and updates the record with the
516
514
  # stored file.
517
515
  def promote(uploaded_file = get, **options)
518
516
  stored_file = store!(uploaded_file, **options)
519
- result = swap(stored_file) or _delete(stored_file, phase: :abort)
517
+ result = swap(stored_file) or _delete(stored_file, action: :abort)
520
518
  result
521
519
  end
522
520
 
@@ -530,18 +528,18 @@ class Shrine
530
528
  # by ORM integrations. If also removes `@old` so that #save and #finalize
531
529
  # don't get called for the current attachment anymore.
532
530
  def replace
533
- _delete(@old, phase: :replace) if @old && !cache.uploaded?(@old)
531
+ _delete(@old, action: :replace) if @old && !cache.uploaded?(@old)
534
532
  end
535
533
 
536
534
  # Deletes the attachment. Typically this should be called after
537
535
  # destroying a record.
538
536
  def destroy
539
- _delete(get, phase: :destroy) if get && !cache.uploaded?(get)
537
+ _delete(get, action: :destroy) if get && !cache.uploaded?(get)
540
538
  end
541
539
 
542
540
  # Deletes the uploaded file.
543
- def _delete(uploaded_file, phase: nil)
544
- delete!(uploaded_file, phase: phase)
541
+ def _delete(uploaded_file, **options)
542
+ delete!(uploaded_file, **options)
545
543
  end
546
544
 
547
545
  # Returns the URL to the attached file (internally calls `#url` on the
@@ -573,17 +571,20 @@ class Shrine
573
571
 
574
572
  # Uploads the file to cache passing context.
575
573
  def cache!(io, **options)
576
- cache.upload(io, context.merge(options))
574
+ warn "Sending :phase to Shrine::Attacher#cache! is deprecated and will not be supported in Shrine 3. Use :action instead." if options[:phase]
575
+ cache.upload(io, context.merge(_equalize_phase_and_action(options)))
577
576
  end
578
577
 
579
578
  # Uploads the file to store passing context.
580
579
  def store!(io, **options)
581
- store.upload(io, context.merge(options))
580
+ warn "Sending :phase to Shrine::Attacher#store! is deprecated and will not be supported in Shrine 3. Use :action instead." if options[:phase]
581
+ store.upload(io, context.merge(_equalize_phase_and_action(options)))
582
582
  end
583
583
 
584
584
  # Deletes the file passing context.
585
585
  def delete!(uploaded_file, **options)
586
- store.delete(uploaded_file, context.merge(options))
586
+ warn "Sending :phase to Shrine::Attacher#delete! is deprecated and will not be supported in Shrine 3. Use :action instead." if options[:phase]
587
+ store.delete(uploaded_file, context.merge(_equalize_phase_and_action(options)))
587
588
  end
588
589
 
589
590
  # Returns the Shrine class related to this attacher.
@@ -593,7 +594,7 @@ class Shrine
593
594
 
594
595
  private
595
596
 
596
- # Assigns a cached file (refuses if the file is stored).
597
+ # Assigns a cached file.
597
598
  def assign_cached(cached_file)
598
599
  set(cached_file)
599
600
  end
@@ -629,6 +630,13 @@ class Shrine
629
630
  def context
630
631
  {name: name, record: record}
631
632
  end
633
+
634
+ # Temporary method used for transitioning from :phase to :action.
635
+ def _equalize_phase_and_action(options)
636
+ options[:phase] = options[:action] if options.key?(:action)
637
+ options[:action] = options[:phase] if options.key?(:phase)
638
+ options
639
+ end
632
640
  end
633
641
 
634
642
  module FileClassMethods
@@ -691,6 +699,33 @@ class Shrine
691
699
  end
692
700
  alias content_type mime_type
693
701
 
702
+ # Opens the underlying IO for reading and yields it to the block,
703
+ # closing it after the block finishes. Use #to_io for opening without a
704
+ # block.
705
+ #
706
+ # uploaded_file.open do |io|
707
+ # # ...
708
+ # end
709
+ def open
710
+ @io = storage.open(id)
711
+ yield @io
712
+ ensure
713
+ @io.close
714
+ @io = nil
715
+ end
716
+
717
+ # Calls `#download` on the storage if it is implemented, otherwise
718
+ # streams the underlying IO to a Tempfile.
719
+ def download
720
+ if storage.respond_to?(:download)
721
+ storage.download(id)
722
+ else
723
+ tempfile = Tempfile.new(["shrine", File.extname(id)], binmode: true)
724
+ open { |io| IO.copy_stream(io, tempfile.path) }
725
+ tempfile.tap(&:open)
726
+ end
727
+ end
728
+
694
729
  # Part of Shrine::UploadedFile's complying to the IO interface. It
695
730
  # delegates to the internally downloaded file.
696
731
  def read(*args)
@@ -728,11 +763,6 @@ class Shrine
728
763
  storage.exists?(id)
729
764
  end
730
765
 
731
- # Calls `#download` on the storage, which downloads the file to disk.
732
- def download
733
- storage.download(id)
734
- end
735
-
736
766
  # Uploads a new file to this file's location and returns it.
737
767
  def replace(io, context = {})
738
768
  uploader.upload(io, context.merge(location: id))
@@ -743,7 +773,7 @@ class Shrine
743
773
  storage.delete(id)
744
774
  end
745
775
 
746
- # Added as a Ruby conversion method. It typically downloads the file.
776
+ # Returns the underlying IO.
747
777
  def to_io
748
778
  io
749
779
  end
@@ -46,6 +46,11 @@ class Shrine
46
46
  # end
47
47
  # end
48
48
  #
49
+ # If you don't want callbacks (e.g. you want to use the attacher object
50
+ # directly), you can turn them off:
51
+ #
52
+ # plugin :activerecord, callbacks: false
53
+ #
49
54
  # ## Validations
50
55
  #
51
56
  # Additionally, any Shrine validation errors will be added to
@@ -56,32 +61,48 @@ class Shrine
56
61
  # include ImageUploader[:avatar]
57
62
  # validates_presence_of :avatar
58
63
  # end
64
+ #
65
+ # If you're doing validation separately from your models, you can turn off
66
+ # validations for your models:
67
+ #
68
+ # plugin :activerecord, validations: false
59
69
  module Activerecord
70
+ def self.configure(uploader, opts = {})
71
+ uploader.opts[:activerecord_callbacks] = opts.fetch(:callbacks, uploader.opts.fetch(:activerecord_callbacks, true))
72
+ uploader.opts[:activerecord_validations] = opts.fetch(:validations, uploader.opts.fetch(:activerecord_validations, true))
73
+ end
74
+
60
75
  module AttachmentMethods
61
76
  def included(model)
62
77
  super
63
78
 
64
79
  return unless model < ::ActiveRecord::Base
65
80
 
66
- model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
67
- validate do
68
- #{@name}_attacher.errors.each do |message|
69
- errors.add(:#{@name}, message)
81
+ if shrine_class.opts[:activerecord_validations]
82
+ model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
83
+ validate do
84
+ #{@name}_attacher.errors.each do |message|
85
+ errors.add(:#{@name}, message)
86
+ end
70
87
  end
71
- end
88
+ RUBY
89
+ end
72
90
 
73
- before_save do
74
- #{@name}_attacher.save if #{@name}_attacher.attached?
75
- end
91
+ if shrine_class.opts[:activerecord_callbacks]
92
+ model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
93
+ before_save do
94
+ #{@name}_attacher.save if #{@name}_attacher.attached?
95
+ end
76
96
 
77
- after_commit on: [:create, :update] do
78
- #{@name}_attacher.finalize if #{@name}_attacher.attached?
79
- end
97
+ after_commit on: [:create, :update] do
98
+ #{@name}_attacher.finalize if #{@name}_attacher.attached?
99
+ end
80
100
 
81
- after_commit on: [:destroy] do
82
- #{@name}_attacher.destroy
83
- end
84
- RUBY
101
+ after_commit on: [:destroy] do
102
+ #{@name}_attacher.destroy
103
+ end
104
+ RUBY
105
+ end
85
106
  end
86
107
  end
87
108
 
@@ -0,0 +1,50 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The metadata plugin provides a convenient method for extracting and
4
+ # adding custom metadata values.
5
+ #
6
+ # plugin :add_metadata
7
+ #
8
+ # add_metadata :exif do |io, context|
9
+ # MiniMagick::Image.new(io.path).exif
10
+ # end
11
+ #
12
+ # The above will add "exif" to the metadata hash, and also add the `#exif`
13
+ # method to the `UploadedFile`:
14
+ #
15
+ # uploaded_file.metadata["exif"]
16
+ # # or
17
+ # uploaded_file.exif
18
+ module AddMetadata
19
+ def self.configure(uploader)
20
+ uploader.opts[:metadata] = {}
21
+ end
22
+
23
+ module ClassMethods
24
+ def add_metadata(name, &block)
25
+ opts[:metadata][name] = block
26
+
27
+ self::UploadedFile.send(:define_method, name) do
28
+ metadata[name.to_s]
29
+ end
30
+ end
31
+ end
32
+
33
+ module InstanceMethods
34
+ def extract_metadata(io, context)
35
+ metadata = super
36
+
37
+ opts[:metadata].each do |name, block|
38
+ value = instance_exec(io, context, &block)
39
+ metadata[name.to_s] = value unless value.nil?
40
+ io.rewind
41
+ end
42
+
43
+ metadata
44
+ end
45
+ end
46
+ end
47
+
48
+ register_plugin(:add_metadata, AddMetadata)
49
+ end
50
+ end
@@ -92,10 +92,10 @@ class Shrine
92
92
  else
93
93
  attacher = load(data)
94
94
  cached_file = attacher.uploaded_file(data["attachment"])
95
- phase = data["phase"].to_sym if data["phase"]
95
+ action = data["action"].to_sym if data["action"]
96
96
 
97
97
  return if cached_file != attacher.get
98
- attacher.promote(cached_file, phase: phase) or return
98
+ attacher.promote(cached_file, action: action) or return
99
99
 
100
100
  attacher
101
101
  end
@@ -109,9 +109,9 @@ class Shrine
109
109
  else
110
110
  attacher = load(data)
111
111
  uploaded_file = attacher.uploaded_file(data["attachment"])
112
- phase = data["phase"].to_sym if data["phase"]
112
+ action = data["action"].to_sym if data["action"]
113
113
 
114
- attacher.delete!(uploaded_file, phase: phase)
114
+ attacher.delete!(uploaded_file, action: action)
115
115
 
116
116
  attacher
117
117
  end
@@ -130,8 +130,14 @@ class Shrine
130
130
  record = find_record(record_class, record_id) ||
131
131
  record_class.new.tap { |object| object.id = record_id }
132
132
 
133
- name = data["name"]
134
- attacher = record.send("#{name}_attacher")
133
+ name = data["name"].to_sym
134
+
135
+ if data["shrine_class"]
136
+ shrine_class = Object.const_get(data["shrine_class"])
137
+ attacher = shrine_class::Attacher.new(record, name)
138
+ else
139
+ attacher = record.send("#{name}_attacher")
140
+ end
135
141
 
136
142
  attacher
137
143
  end
@@ -140,11 +146,12 @@ class Shrine
140
146
  module AttacherMethods
141
147
  # Calls the promoting block (if registered) with a serializable data
142
148
  # hash.
143
- def _promote(uploaded_file = get, phase: nil)
149
+ def _promote(uploaded_file = get, phase: nil, action: phase)
144
150
  if background_promote = shrine_class.opts[:backgrounding_promote]
145
151
  data = self.class.dump(self).merge(
146
152
  "attachment" => uploaded_file.to_json,
147
- "phase" => (phase.to_s if phase),
153
+ "phase" => (action.to_s if action),
154
+ "action" => (action.to_s if action),
148
155
  )
149
156
  instance_exec(data, &background_promote)
150
157
  else
@@ -154,11 +161,12 @@ class Shrine
154
161
 
155
162
  # Calls the deleting block (if registered) with a serializable data
156
163
  # hash.
157
- def _delete(uploaded_file, phase: nil)
164
+ def _delete(uploaded_file, phase: nil, action: phase)
158
165
  if background_delete = shrine_class.opts[:backgrounding_delete]
159
166
  data = self.class.dump(self).merge(
160
167
  "attachment" => uploaded_file.to_json,
161
- "phase" => (phase.to_s if phase),
168
+ "phase" => (action.to_s if action),
169
+ "action" => (action.to_s if action),
162
170
  )
163
171
  instance_exec(data, &background_delete)
164
172
  uploaded_file
@@ -171,9 +179,10 @@ class Shrine
171
179
  # suitable for passing as an argument to background jobs.
172
180
  def dump
173
181
  {
174
- "attachment" => (get && get.to_json),
175
- "record" => [record.class.to_s, record.id.to_s],
176
- "name" => name.to_s,
182
+ "attachment" => (get && get.to_json),
183
+ "record" => [record.class.to_s, record.id.to_s],
184
+ "name" => name.to_s,
185
+ "shrine_class" => shrine_class.name,
177
186
  }
178
187
  end
179
188
  end
@@ -64,12 +64,13 @@ class Shrine
64
64
 
65
65
  # Upload the stored file to the backup storage.
66
66
  def store_backup!(stored_file)
67
- backup_store.upload(stored_file, context.merge(phase: :backup))
67
+ options = _equalize_phase_and_action(action: :backup)
68
+ backup_store.upload(stored_file, context.merge(options))
68
69
  end
69
70
 
70
71
  # Deleted the stored file from the backup storage.
71
72
  def delete_backup!(deleted_file)
72
- _delete(backup_file(deleted_file), phase: :backup)
73
+ _delete(backup_file(deleted_file), action: :backup)
73
74
  end
74
75
 
75
76
  def backup_store
@@ -90,7 +91,7 @@ class Shrine
90
91
 
91
92
  # We preserve the location when uploading from store to backup.
92
93
  def get_location(io, context)
93
- if context[:phase] == :backup
94
+ if context[:action] == :backup
94
95
  io.id
95
96
  else
96
97
  super
@@ -103,7 +103,7 @@ class Shrine
103
103
 
104
104
  # Returns contents of the file base64-encoded.
105
105
  def base64
106
- content = storage.read(id)
106
+ content = open { |io| io.read }
107
107
  Base64.encode64(content).chomp
108
108
  end
109
109
  end
@@ -10,7 +10,7 @@ class Shrine
10
10
  module AttacherMethods
11
11
  def promote(uploaded_file = get, **options)
12
12
  result = super
13
- _delete(uploaded_file, phase: :promote)
13
+ _delete(uploaded_file, action: :promote)
14
14
  result
15
15
  end
16
16
  end
@@ -22,7 +22,7 @@ class Shrine
22
22
  def copy(io, context)
23
23
  super
24
24
  if io.respond_to?(:delete) && !io.is_a?(UploadedFile)
25
- io.delete if delete_uploaded?(io)
25
+ io.delete rescue nil if delete_uploaded?(io)
26
26
  end
27
27
  end
28
28
 
@@ -83,10 +83,19 @@ class Shrine
83
83
  def _extract_mime_type_with_file(io)
84
84
  require "open3"
85
85
 
86
- mime_type, status = Open3.capture2("file", "--mime-type", "--brief", "-",
87
- stdin_data: magic_header(io), binmode: true)
86
+ cmd = ["file", "--mime-type", "--brief", "-"]
87
+ options = {stdin_data: magic_header(io), binmode: true}
88
88
 
89
- mime_type.strip unless mime_type.empty?
89
+ begin
90
+ stdout, stderr, status = Open3.capture3(*cmd, options)
91
+ rescue Errno::ENOENT
92
+ raise Error, "The `file` command-line tool is not installed"
93
+ end
94
+
95
+ raise Error, stderr unless status.success?
96
+ $stderr.print(stderr)
97
+
98
+ stdout.strip
90
99
  end
91
100
 
92
101
  def _extract_mime_type_with_mimemagic(io)