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,48 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The included plugin allows you to hook up to the `.included` hook when
4
+ # of the "attachment" module. This allows you to add additonal methods to
5
+ # the model whenever an attachment is included.
6
+ #
7
+ # plugin :included do |name|
8
+ # define_method("#{name}_width") do
9
+ # send(name).width if send(name)
10
+ # end
11
+ #
12
+ # define_method("#{name}_height") do
13
+ # send(name).height if send(name)
14
+ # end
15
+ # end
16
+ #
17
+ # The block is evaluated in the context of the model, but note that you
18
+ # cannot use keywords like `def`, instead you should use the
19
+ # metaprogramming equivalents like `define_method`. Now when an attachment
20
+ # is included to a model, it will receive the appropriate methods:
21
+ #
22
+ # class User
23
+ # include ImageUploader[:avatar]
24
+ # end
25
+ #
26
+ # user = User.new
27
+ # user.avatar_width #=> nil
28
+ # user.avatar_height #=> nil
29
+ #
30
+ # user.avatar = File.open("avatar.jpg")
31
+ # user.avatar_width #=> 300
32
+ # user.avatar_height #=> 500
33
+ module Included
34
+ def self.configure(uploader, &block)
35
+ uploader.opts[:included_block] = block
36
+ end
37
+
38
+ module AttachmentMethods
39
+ def included(model)
40
+ super
41
+ model.instance_exec(@name, &shrine_class.opts[:included_block])
42
+ end
43
+ end
44
+ end
45
+
46
+ register_plugin(:included, Included)
47
+ end
48
+ end
@@ -0,0 +1,54 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The keep_files plugin gives you the ability to prevent files from being
4
+ # deleted. This functionality is useful when implementing soft deletes, or
5
+ # when implementing some kind of [event store] where you need to track
6
+ # history.
7
+ #
8
+ # The plugin accepts the following options:
9
+ #
10
+ # :destroyed
11
+ # : If set to `true`, destroying the record won't delete the associated
12
+ # attachment.
13
+ #
14
+ # :replaced
15
+ # : If set to `true`, uploading a new attachment won't delete the old one.
16
+ #
17
+ # :cached
18
+ # : If set to `true`, cached files that are uploaded to store won't be
19
+ # deleted.
20
+ #
21
+ # For example, the following will keep destroyed and replaced files:
22
+ #
23
+ # plugin :keep_files, destroyed: true, :replaced: true
24
+ #
25
+ # [event store]: http://docs.geteventstore.com/introduction/event-sourcing-basics/
26
+ module KeepFiles
27
+ def self.configure(uploader, destroyed: nil, replaced: nil, cached: nil)
28
+ uploader.opts[:keep_files] = []
29
+ uploader.opts[:keep_files] << :destroyed if destroyed
30
+ uploader.opts[:keep_files] << :replaced if replaced
31
+ uploader.opts[:keep_files] << :cached if cached
32
+ end
33
+
34
+ module ClassMethods
35
+ def keep?(type)
36
+ opts[:keep_files].include?(type)
37
+ end
38
+
39
+ # 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
48
+ end
49
+ end
50
+ end
51
+
52
+ register_plugin(:keep_files, KeepFiles)
53
+ end
54
+ end
@@ -0,0 +1,158 @@
1
+ require "logger"
2
+ require "benchmark"
3
+ require "json"
4
+
5
+ class Shrine
6
+ module Plugins
7
+ # The logging plugin logs any storing/processing/deleting that is performed.
8
+ #
9
+ # plugin :logging
10
+ #
11
+ # This plugin is useful when you want to have overview of what exactly is
12
+ # going on, or you simply want to have it logged for future debugging.
13
+ # By default the logging output looks something like this:
14
+ #
15
+ # 2015-10-09T20:06:06.676Z #25602: UPLOAD[direct] ImageUploader[:avatar] User[29543] 1 file (0.1s)
16
+ # 2015-10-09T20:06:06.854Z #25602: PROCESS[promote]: ImageUploader[:avatar] User[29543] 3 files (0.22s)
17
+ # 2015-10-09T20:06:07.133Z #25602: DELETE[destroyed]: ImageUploader[:avatar] User[29543] 3 files (0.07s)
18
+ #
19
+ # The plugin accepts the following options:
20
+ #
21
+ # :format
22
+ # : This allows you to change the logging output into something that may be
23
+ # easier to grep. Accepts `:human` (default), `:json` and `:heroku`.
24
+ #
25
+ # :stream
26
+ # : The default logging stream is `$stdout`, but you may want to change it,
27
+ # e.g. if you log into a file. This option is passed directly to
28
+ # `Logger.new` (from the "logger" Ruby standard library).
29
+ #
30
+ # :logger
31
+ # : This allows you to change the logger entirely. This is useful for example
32
+ # in Rails applications, where you might want to assign this option to
33
+ # `Rails.logger`.
34
+ #
35
+ # The default format is probably easiest to read, but may not be easiest to
36
+ # grep. If this is important to you, you can switch to another format:
37
+ #
38
+ # plugin :logging, format: :json
39
+ # # {"action":"upload","phase":"direct","uploader":"ImageUploader","attachment":"avatar",...}
40
+ #
41
+ # plugin :logging, format: :heroku
42
+ # # action=upload phase=direct uploader=ImageUploader attachment=avatar record_class=User ...
43
+ #
44
+ # Logging is by default disabled in tests, but you can enable it by setting
45
+ # `Shrine.logger.level = Logger::INFO`.
46
+ module Logging
47
+ def self.configure(uploader, logger: nil, stream: $stdout, format: :human)
48
+ uploader.logger = logger if logger
49
+ uploader.opts[:logging_stream] = stream
50
+ uploader.opts[:logging_format] = format
51
+ end
52
+
53
+ module ClassMethods
54
+ def logger=(logger)
55
+ @logger = logger || Logger.new(nil)
56
+ end
57
+
58
+ # Initializes a new logger if it hasn't been initialized.
59
+ def logger
60
+ @logger ||= (
61
+ logger = Logger.new(opts[:logging_stream])
62
+ logger.level = Logger::INFO
63
+ logger.level = Logger::WARN if ENV["RACK_ENV"] == "test"
64
+ logger.formatter = pretty_formatter
65
+ logger
66
+ )
67
+ end
68
+
69
+ # It makes logging preamble simpler than the default logger. Also, it
70
+ # doesn't output timestamps if on Heroku.
71
+ def pretty_formatter
72
+ proc do |severity, time, program_name, message|
73
+ output = "#{Process.pid}: #{message}\n"
74
+ output.prepend "#{time.utc.iso8601(3)} " unless ENV["DYNO"]
75
+ output
76
+ end
77
+ end
78
+ end
79
+
80
+ module InstanceMethods
81
+ def store(io, context = {})
82
+ log("store", context) { super }
83
+ end
84
+
85
+ def delete(uploaded_file, context = {})
86
+ log("delete", context) { super }
87
+ end
88
+
89
+ private
90
+
91
+ def processed(io, context = {})
92
+ log("process", context) { super }
93
+ end
94
+
95
+ # Collects the data and sends it for logging.
96
+ def log(action, context)
97
+ result, duration = benchmark { yield }
98
+
99
+ _log(
100
+ action: action,
101
+ phase: context[:phase],
102
+ uploader: self.class,
103
+ attachment: context[:name],
104
+ record_class: (context[:record].class if context[:record]),
105
+ record_id: (context[:record].id if context[:record]),
106
+ files: count(result),
107
+ duration: ("%.2f" % duration).to_f,
108
+ ) unless result.nil?
109
+
110
+ result
111
+ end
112
+
113
+ def _log(data)
114
+ message = send("_log_message_#{opts[:logging_format]}", data)
115
+ self.class.logger.info(message)
116
+ end
117
+
118
+ def _log_message_human(data)
119
+ components = []
120
+ components << "#{data[:action].upcase}"
121
+ components.last << "[#{data[:phase]}]" if data[:phase]
122
+ components << "#{data[:uploader]}"
123
+ components.last << "[:#{data[:attachment]}]" if data[:attachment]
124
+ components << "#{data[:record_class]}[#{data[:record_id]}]" if data[:record_class]
125
+ components << (data[:files] > 1 ? "#{data[:files]} files" : "#{data[:files]} file")
126
+ components << "(#{data[:duration]}s)"
127
+ components.join(" ")
128
+ end
129
+
130
+ def _log_message_json(data)
131
+ data.to_json
132
+ end
133
+
134
+ def _log_message_heroku(data)
135
+ data.map { |key, value| "#{key}=#{value}" }.join(" ")
136
+ end
137
+
138
+ # We may have one file, a hash of versions, or an array of files or
139
+ # hashes.
140
+ def count(object)
141
+ case object
142
+ when Hash then object.count
143
+ when Array then object.inject(0) { |sum, o| sum += count(o) }
144
+ else 1
145
+ end
146
+ end
147
+
148
+ def benchmark
149
+ result = nil
150
+ duration = Benchmark.realtime { result = yield }
151
+ [result, duration]
152
+ end
153
+ end
154
+ end
155
+
156
+ register_plugin(:logging, Logging)
157
+ end
158
+ end
@@ -0,0 +1,61 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The migration_helpers plugin gives the model additional helper methods
4
+ # which are convenient when doing attachment migrations.
5
+ #
6
+ # plugin :migration_helpers
7
+ #
8
+ # If your attachment's name is "avatar", the model will get `#avatar_cache`
9
+ # and `#avatar_store` methods.
10
+ #
11
+ # user = User.new
12
+ # user.avatar_cache #=> #<Shrine @storage_key=:cache @storage=#<Shrine::Storage::FileSystem @directory=public/uploads>>
13
+ # user.avatar_store #=> #<Shrine @storage_key=:store @storage=#<Shrine::Storage::S3:0x007fb8343397c8 @bucket=#<Aws::S3::Bucket name="foo">>>
14
+ #
15
+ # The model will also get `#update_avatar` method, which should be used
16
+ # when doing attachment migrations. It will update the record's attachment
17
+ # with the result of the passed in block.
18
+ #
19
+ # user.update_avatar do |avatar|
20
+ # user.avatar_store.upload(avatar) # saved to the record
21
+ # end
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.
26
+ module MigrationHelpers
27
+ module AttachmentMethods
28
+ def initialize(name)
29
+ super
30
+
31
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
32
+ def update_#{name}(&block)
33
+ #{name}_attacher.update_stored(&block)
34
+ end
35
+
36
+ def #{name}_cache
37
+ #{name}_attacher.cache
38
+ end
39
+
40
+ def #{name}_store
41
+ #{name}_attacher.store
42
+ end
43
+ RUBY
44
+ end
45
+ end
46
+
47
+ module AttacherMethods
48
+ # Updates the attachment with the result of the block. It will get
49
+ # called only if the attachment exists and is stored.
50
+ def update_stored(&block)
51
+ attachment = get
52
+ return if attachment.nil? || cache.uploaded?(attachment)
53
+ new_attachment = block.call(attachment)
54
+ update(new_attachment) unless changed?(get)
55
+ end
56
+ end
57
+ end
58
+
59
+ register_plugin(:migration_helpers, MigrationHelpers)
60
+ end
61
+ end
@@ -0,0 +1,75 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The moving plugin enables you to move files to specified storages. On
4
+ # the filesystem moving is istantaneous, since the OS only changes the
5
+ # pointer, so this plugin is useful when dealing with large files.
6
+ #
7
+ # This plugin is also recommended if you're doing processing, since by
8
+ # default temporary files won't immediately get deleted (Ruby's Tempfiles
9
+ # usually get deleted only when the process ends).
10
+ #
11
+ # plugin :moving, storages: [:cache]
12
+ #
13
+ # The `:storages` option specifies which storages the file will be moved
14
+ # to. The above will move Rails's uploaded files to cache (without this
15
+ # plugin it's simply copied over). However, you may want to move cached
16
+ # files to `:store` as well:
17
+ #
18
+ # plugin :moving, storages: [:cache, :store]
19
+ #
20
+ # What exactly means "moving"? Usually this means that the file which is
21
+ # being uploaded will be deleted afterwards. However, if both the file
22
+ # being uploaded and the destination are on the filesystem, a `mv` command
23
+ # will be executed instead. Some other storages may implement moving as
24
+ # well, usually if also both the `:cache` and `:store` are using the same
25
+ # storage.
26
+ module Moving
27
+ def self.configure(uploader, storages:)
28
+ uploader.opts[:move_files_to_storages] = storages
29
+ end
30
+
31
+ module InstanceMethods
32
+ private
33
+
34
+ # If the file is movable (usually this means that both the file and
35
+ # the destination are on the filesystem), use the underlying storage's
36
+ # ability to move. Otherwise we "imitate" moving by deleting the file
37
+ # after it was uploaded.
38
+ def put(io, context)
39
+ if move?(io, context)
40
+ if movable?(io, context)
41
+ move(io, context)
42
+ else
43
+ super
44
+ io.delete if io.respond_to?(:delete)
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
+ else
53
+ super
54
+ end
55
+ end
56
+
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
+ # Ask the storage if the given file is movable.
63
+ def movable?(io, context)
64
+ storage.respond_to?(:move) && storage.movable?(io, context[:location])
65
+ end
66
+
67
+ def move?(io, context)
68
+ opts[:move_files_to_storages].include?(storage_key)
69
+ end
70
+ end
71
+ end
72
+
73
+ register_plugin(:moving, Moving)
74
+ end
75
+ end
@@ -0,0 +1,47 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The multi_delete plugins allows you to leverage your storage's multi
4
+ # delete capabilities.
5
+ #
6
+ # plugin :multi_delete
7
+ #
8
+ # This plugin allows you pass an array of files to `Shrine.delete`.
9
+ #
10
+ # Shrine.delete([file1, file2, file3])
11
+ #
12
+ # Now if you're using Storage::S3, deleting an array of files will issue a
13
+ # single HTTP request. Some other storages may support multi deletes as
14
+ # well. The versions plugin uses this plugin for deleting multiple versions
15
+ # at once.
16
+ module MultiDelete
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
+ private
28
+
29
+ # Adds the ability to upload multiple files, leveraging the underlying
30
+ # storage's potential multi delete capability.
31
+ def _delete(uploaded_file, context)
32
+ if uploaded_file.is_a?(Array)
33
+ if storage.respond_to?(:multi_delete)
34
+ storage.multi_delete(uploaded_file.map(&:id))
35
+ else
36
+ uploaded_file.map { |file| super(file, context) }
37
+ end
38
+ else
39
+ super
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ register_plugin(:multi_delete, MultiDelete)
46
+ end
47
+ end