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.
- checksums.yaml +4 -4
- data/README.md +214 -248
- data/doc/carrierwave.md +11 -22
- data/doc/changing_location.md +3 -3
- data/doc/creating_storages.md +48 -21
- data/doc/direct_s3.md +37 -29
- data/doc/paperclip.md +7 -9
- data/doc/refile.md +8 -11
- data/doc/regenerating_versions.md +14 -19
- data/lib/shrine.rb +53 -23
- data/lib/shrine/plugins/activerecord.rb +36 -15
- data/lib/shrine/plugins/add_metadata.rb +50 -0
- data/lib/shrine/plugins/backgrounding.rb +22 -13
- data/lib/shrine/plugins/backup.rb +4 -3
- data/lib/shrine/plugins/data_uri.rb +1 -1
- data/lib/shrine/plugins/delete_promoted.rb +1 -1
- data/lib/shrine/plugins/delete_raw.rb +1 -1
- data/lib/shrine/plugins/determine_mime_type.rb +12 -3
- data/lib/shrine/plugins/direct_upload.rb +55 -65
- data/lib/shrine/plugins/download_endpoint.rb +7 -3
- data/lib/shrine/plugins/logging.rb +1 -1
- data/lib/shrine/plugins/moving.rb +6 -5
- data/lib/shrine/plugins/processing.rb +50 -0
- data/lib/shrine/plugins/rack_file.rb +3 -0
- data/lib/shrine/plugins/recache.rb +8 -12
- data/lib/shrine/plugins/remove_invalid.rb +1 -1
- data/lib/shrine/plugins/restore_cached_data.rb +1 -3
- data/lib/shrine/plugins/sequel.rb +40 -19
- data/lib/shrine/plugins/versions.rb +22 -23
- data/lib/shrine/storage/file_system.rb +0 -5
- data/lib/shrine/storage/linter.rb +4 -9
- data/lib/shrine/storage/s3.rb +28 -13
- data/lib/shrine/version.rb +2 -2
- data/shrine.gemspec +3 -2
- metadata +24 -9
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.
|
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[:
|
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,
|
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(
|
505
|
+
_promote(action: :store) if cached?
|
508
506
|
end
|
509
507
|
|
510
508
|
# Promotes the file.
|
511
|
-
def _promote(uploaded_file = get,
|
512
|
-
promote(uploaded_file,
|
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,
|
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,
|
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,
|
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,
|
544
|
-
delete!(uploaded_file,
|
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.
|
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.
|
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
|
-
|
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
|
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
|
-
#
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
88
|
+
RUBY
|
89
|
+
end
|
72
90
|
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
97
|
+
after_commit on: [:create, :update] do
|
98
|
+
#{@name}_attacher.finalize if #{@name}_attacher.attached?
|
99
|
+
end
|
80
100
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
95
|
+
action = data["action"].to_sym if data["action"]
|
96
96
|
|
97
97
|
return if cached_file != attacher.get
|
98
|
-
attacher.promote(cached_file,
|
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
|
-
|
112
|
+
action = data["action"].to_sym if data["action"]
|
113
113
|
|
114
|
-
attacher.delete!(uploaded_file,
|
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
|
-
|
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" => (
|
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" => (
|
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"
|
175
|
-
"record"
|
176
|
-
"name"
|
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
|
-
|
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),
|
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[:
|
94
|
+
if context[:action] == :backup
|
94
95
|
io.id
|
95
96
|
else
|
96
97
|
super
|
@@ -83,10 +83,19 @@ class Shrine
|
|
83
83
|
def _extract_mime_type_with_file(io)
|
84
84
|
require "open3"
|
85
85
|
|
86
|
-
|
87
|
-
|
86
|
+
cmd = ["file", "--mime-type", "--brief", "-"]
|
87
|
+
options = {stdin_data: magic_header(io), binmode: true}
|
88
88
|
|
89
|
-
|
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)
|