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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +663 -0
- data/doc/creating_plugins.md +100 -0
- data/doc/creating_storages.md +108 -0
- data/doc/direct_s3.md +97 -0
- data/doc/migrating_storage.md +79 -0
- data/doc/regenerating_versions.md +38 -0
- data/lib/shrine.rb +806 -0
- data/lib/shrine/plugins/activerecord.rb +89 -0
- data/lib/shrine/plugins/background_helpers.rb +148 -0
- data/lib/shrine/plugins/cached_attachment_data.rb +47 -0
- data/lib/shrine/plugins/data_uri.rb +93 -0
- data/lib/shrine/plugins/default_storage.rb +39 -0
- data/lib/shrine/plugins/delete_invalid.rb +25 -0
- data/lib/shrine/plugins/determine_mime_type.rb +119 -0
- data/lib/shrine/plugins/direct_upload.rb +274 -0
- data/lib/shrine/plugins/dynamic_storage.rb +57 -0
- data/lib/shrine/plugins/hooks.rb +123 -0
- data/lib/shrine/plugins/included.rb +48 -0
- data/lib/shrine/plugins/keep_files.rb +54 -0
- data/lib/shrine/plugins/logging.rb +158 -0
- data/lib/shrine/plugins/migration_helpers.rb +61 -0
- data/lib/shrine/plugins/moving.rb +75 -0
- data/lib/shrine/plugins/multi_delete.rb +47 -0
- data/lib/shrine/plugins/parallelize.rb +62 -0
- data/lib/shrine/plugins/pretty_location.rb +32 -0
- data/lib/shrine/plugins/recache.rb +36 -0
- data/lib/shrine/plugins/remote_url.rb +127 -0
- data/lib/shrine/plugins/remove_attachment.rb +59 -0
- data/lib/shrine/plugins/restore_cached.rb +36 -0
- data/lib/shrine/plugins/sequel.rb +94 -0
- data/lib/shrine/plugins/store_dimensions.rb +82 -0
- data/lib/shrine/plugins/validation_helpers.rb +168 -0
- data/lib/shrine/plugins/versions.rb +177 -0
- data/lib/shrine/storage/file_system.rb +165 -0
- data/lib/shrine/storage/linter.rb +94 -0
- data/lib/shrine/storage/s3.rb +118 -0
- data/lib/shrine/version.rb +14 -0
- data/shrine.gemspec +46 -0
- metadata +364 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The store_dimensions plugin extracts and stores dimensions of the
|
4
|
+
# uploaded image using the [fastimage] gem.
|
5
|
+
#
|
6
|
+
# plugin :store_dimensions
|
7
|
+
#
|
8
|
+
# You can access the dimensions through `#width` and `#height` methods:
|
9
|
+
#
|
10
|
+
# uploader = Shrine.new(:store)
|
11
|
+
# uploaded_file = uploader.upload(File.open("image.jpg"))
|
12
|
+
#
|
13
|
+
# uploaded_file.width #=> 300
|
14
|
+
# uploaded_file.height #=> 500
|
15
|
+
#
|
16
|
+
# The fastimage gem has built-in protection against [image bombs]. However,
|
17
|
+
# if for some reason it doesn't suit your needs, you can provide a custom
|
18
|
+
# `:analyzer`:
|
19
|
+
#
|
20
|
+
# plugin :store_dimensions, analyzer: ->(io) do
|
21
|
+
# MiniMagick::Image.new(io).dimensions #=> [300, 500]
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# [fastimage]: https://github.com/sdsykes/fastimage
|
25
|
+
# [image bombs]: https://www.bamsoftware.com/hacks/deflate.html
|
26
|
+
module StoreDimensions
|
27
|
+
def self.load_dependencies(uploader, analyzer: :fastimage)
|
28
|
+
case analyzer
|
29
|
+
when :fastimage then require "fastimage"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.configure(uploader, analyzer: :fastimage)
|
34
|
+
uploader.opts[:dimensions_analyzer] = analyzer
|
35
|
+
end
|
36
|
+
|
37
|
+
module InstanceMethods
|
38
|
+
# We update the metadata with "width" and "height".
|
39
|
+
def extract_metadata(io, context)
|
40
|
+
width, height = extract_dimensions(io)
|
41
|
+
|
42
|
+
super.update(
|
43
|
+
"width" => width,
|
44
|
+
"height" => height,
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
# If the `io` is an uploaded file, copies its dimensions, otherwise
|
49
|
+
# calls the predefined or custom analyzer.
|
50
|
+
def extract_dimensions(io)
|
51
|
+
analyzer = opts[:dimensions_analyzer]
|
52
|
+
|
53
|
+
if io.respond_to?(:width) && io.respond_to?(:height)
|
54
|
+
[io.width, io.height]
|
55
|
+
elsif analyzer.is_a?(Symbol)
|
56
|
+
send(:"_extract_dimensions_with_#{analyzer}", io)
|
57
|
+
else
|
58
|
+
analyzer.call(io)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def _extract_dimensions_with_fastimage(io)
|
65
|
+
FastImage.size(io)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
module FileMethods
|
70
|
+
def width
|
71
|
+
metadata["width"] && Integer(metadata["width"])
|
72
|
+
end
|
73
|
+
|
74
|
+
def height
|
75
|
+
metadata["height"] && Integer(metadata["height"])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
register_plugin(:store_dimensions, StoreDimensions)
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The validation_helpers plugin provides helper methods for validating
|
4
|
+
# attached files.
|
5
|
+
#
|
6
|
+
# class ImageUploader < Shrine
|
7
|
+
# plugin :validation_helpers
|
8
|
+
#
|
9
|
+
# Attacher.validate do
|
10
|
+
# if record.guest?
|
11
|
+
# validate_max_size 5*1024*1024
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# The validation methods are instance-level, the `Attacher.validate` block
|
17
|
+
# is evaluated in context of an instance of `Shrine::Attacher`, so you can
|
18
|
+
# easily to conditional validation.
|
19
|
+
#
|
20
|
+
# If you would like to change default validation error messages, you can
|
21
|
+
# pass in the `:default_messages` option to the plugin:
|
22
|
+
#
|
23
|
+
# plugin :validation_helpers, default_messages: {
|
24
|
+
# max_size: ->(max) { I18n.t("errors.file.max_size", max: max) },
|
25
|
+
# mime_type_inclusion: ->(whitelist) { I18n.t("errors.file.mime_type_inclusion", whitelist: whitelist) },
|
26
|
+
# }
|
27
|
+
#
|
28
|
+
# If you would like to change the error message inline, you can pass the
|
29
|
+
# `:message` option to any validation method:
|
30
|
+
#
|
31
|
+
# validate_mime_type_inclusion [/^image/], message: "is not an image"
|
32
|
+
#
|
33
|
+
# For a complete list of all validation helpers, see AttacherMethods.
|
34
|
+
module ValidationHelpers
|
35
|
+
def self.configure(uploader, default_messages: {})
|
36
|
+
uploader.opts[:validation_helpers_default_messages] = default_messages
|
37
|
+
end
|
38
|
+
|
39
|
+
DEFAULT_MESSAGES = {
|
40
|
+
max_size: ->(max) { "is larger than #{max.to_f/1024/1024} MB" },
|
41
|
+
min_size: ->(min) { "is smaller than #{min.to_f/1024/1024} MB" },
|
42
|
+
max_width: ->(max) { "is wider than #{max} px" },
|
43
|
+
min_width: ->(min) { "is narrower than #{min} px" },
|
44
|
+
max_height: ->(max) { "is taller than #{max} px" },
|
45
|
+
min_height: ->(min) { "is shorter than #{min} px" },
|
46
|
+
mime_type_inclusion: ->(list) { "isn't of allowed type: #{list.inspect}" },
|
47
|
+
mime_type_exclusion: ->(list) { "is of forbidden type: #{list.inspect}" },
|
48
|
+
extension_inclusion: ->(list) { "isn't in allowed format: #{list.inspect}" },
|
49
|
+
extension_exclusion: ->(list) { "is in forbidden format: #{list.inspect}" },
|
50
|
+
}
|
51
|
+
|
52
|
+
module AttacherClassMethods
|
53
|
+
def default_validation_messages
|
54
|
+
@default_validation_messages ||= DEFAULT_MESSAGES.merge(
|
55
|
+
shrine_class.opts[:validation_helpers_default_messages])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module AttacherMethods
|
60
|
+
# Validates that the file is not larger than `max`.
|
61
|
+
def validate_max_size(max, message: nil)
|
62
|
+
if get.size > max
|
63
|
+
errors << error_message(:max_size, message, max)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Validates that the file is not smaller than `min`.
|
68
|
+
def validate_min_size(min, message: nil)
|
69
|
+
if get.size < min
|
70
|
+
errors << error_message(:min_size, message, min)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Validates that the file is not wider than `max`. Requires the
|
75
|
+
# store_dimensions plugin.
|
76
|
+
def validate_max_width(max, message: nil)
|
77
|
+
raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:width)
|
78
|
+
if get.width > max
|
79
|
+
errors << error_message(:max_width, message, max)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Validates that the file is not narrower than `min`. Requires the
|
84
|
+
# store_dimensions plugin.
|
85
|
+
def validate_min_width(min, message: nil)
|
86
|
+
raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:width)
|
87
|
+
if get.width < min
|
88
|
+
errors << error_message(:min_width, message, min)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Validates that the file is not taller than `max`. Requires the
|
93
|
+
# store_dimensions plugin.
|
94
|
+
def validate_max_height(max, message: nil)
|
95
|
+
raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:height)
|
96
|
+
if get.height > max
|
97
|
+
errors << error_message(:max_height, message, max)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Validates that the file is not shorter than `min`. Requires the
|
102
|
+
# store_dimensions plugin.
|
103
|
+
def validate_min_height(min, message: nil)
|
104
|
+
raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:height)
|
105
|
+
if get.height < min
|
106
|
+
errors << error_message(:min_height, message, min)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Validates that the MIME type is in the `whitelist`. The whitelist is
|
111
|
+
# an array of strings or regexes.
|
112
|
+
#
|
113
|
+
# validate_mime_type_inclusion ["audio/mp3", /^video/]
|
114
|
+
def validate_mime_type_inclusion(whitelist, message: nil)
|
115
|
+
if whitelist.none? { |mime_type| regex(mime_type) =~ get.mime_type.to_s }
|
116
|
+
errors << error_message(:mime_type_inclusion, message, whitelist)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Validates that the MIME type is not in the `blacklist`. The blacklist
|
121
|
+
# is an array of strings or regexes.
|
122
|
+
#
|
123
|
+
# validate_mime_type_exclusion ["image/gif", /^audio/]
|
124
|
+
def validate_mime_type_exclusion(blacklist, message: nil)
|
125
|
+
if blacklist.any? { |mime_type| regex(mime_type) =~ get.mime_type.to_s }
|
126
|
+
errors << error_message(:mime_type_exclusion, message, blacklist)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Validates that the extension is in the `whitelist`. The whitelist
|
131
|
+
# is an array of strings or regexes.
|
132
|
+
#
|
133
|
+
# validate_extension_inclusion [/jpe?g/]
|
134
|
+
def validate_extension_inclusion(whitelist, message: nil)
|
135
|
+
if whitelist.none? { |extension| regex(extension) =~ get.extension.to_s }
|
136
|
+
errors << error_message(:extension_inclusion, message, whitelist)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Validates that the extension is not in the `blacklist`. The blacklist
|
141
|
+
# is an array of strings or regexes.
|
142
|
+
#
|
143
|
+
# validate_extension_exclusion ["mov", /^mp*/]
|
144
|
+
def validate_extension_exclusion(blacklist, message: nil)
|
145
|
+
if blacklist.any? { |extension| regex(extension) =~ get.extension.to_s }
|
146
|
+
errors << error_message(:extension_exclusion, message, blacklist)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
# Converts a string to a regex.
|
153
|
+
def regex(string)
|
154
|
+
string.is_a?(Regexp) ? string : /^#{Regexp.escape(string)}$/
|
155
|
+
end
|
156
|
+
|
157
|
+
# Returns the direct message if given, otherwise uses the default error
|
158
|
+
# message.
|
159
|
+
def error_message(type, message, object)
|
160
|
+
message ||= self.class.default_validation_messages.fetch(type)
|
161
|
+
message.is_a?(String) ? message : message.call(object)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
register_plugin(:validation_helpers, ValidationHelpers)
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The versions plugin enables your uploader to deal with versions. To
|
4
|
+
# generate versions, you simply return a hash of versions in `Shrine#process`:
|
5
|
+
#
|
6
|
+
# class ImageUploader < Shrine
|
7
|
+
# plugin :versions, names: [:large, :medium, :small]
|
8
|
+
#
|
9
|
+
# def process(io, context)
|
10
|
+
# if context[:phase] == :store
|
11
|
+
# size_700 = process_to_limit!(io.download, 700, 700)
|
12
|
+
# size_500 = process_to_limit!(size_700, 500, 500)
|
13
|
+
# size_300 = process_to_limit!(size_500, 300, 300)
|
14
|
+
#
|
15
|
+
# {large: size_700, medium: size_500, small: size_300}
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# Now when you access the attachment through the model, a hash of uploaded
|
21
|
+
# files will be returned:
|
22
|
+
#
|
23
|
+
# user.avatar #=>
|
24
|
+
# # {
|
25
|
+
# # large: #<Shrine::UploadedFile>,
|
26
|
+
# # medium: #<Shrine::UploadedFile>,
|
27
|
+
# # small: #<Shrine::UploadedFile>,
|
28
|
+
# # }
|
29
|
+
# user.avatar.class #=> Hash
|
30
|
+
#
|
31
|
+
# # With the store_dimensions plugin
|
32
|
+
# user.avatar[:large].width #=> 700
|
33
|
+
# user.avatar[:medium].width #=> 500
|
34
|
+
# user.avatar[:small].width #=> 300
|
35
|
+
#
|
36
|
+
# The plugin also extends the `avatar_url` method to accept versions:
|
37
|
+
#
|
38
|
+
# user.avatar_url(:medium)
|
39
|
+
#
|
40
|
+
# This method plays nice when generating versions in a background job,
|
41
|
+
# since it will just point to the original cached file until the versions
|
42
|
+
# are done processing:
|
43
|
+
#
|
44
|
+
# user.avatar #=> #<Shrine::UploadedFile>
|
45
|
+
# user.avatar_url(:medium) #=> "http://example.com/original.jpg"
|
46
|
+
#
|
47
|
+
# # the versions have finished generating
|
48
|
+
#
|
49
|
+
# user.avatar_url(:medium) #=> "http://example.com/medium.jpg"
|
50
|
+
#
|
51
|
+
# Any additional options will be properly forwarded to the underlying
|
52
|
+
# storage:
|
53
|
+
#
|
54
|
+
# user.avatar_url(:medium, download: true)
|
55
|
+
#
|
56
|
+
# You can also easily generate default URLs for specific versions, since
|
57
|
+
# the `context` will include the version name:
|
58
|
+
#
|
59
|
+
# class ImageUploader
|
60
|
+
# def default_url(io, context)
|
61
|
+
# "/images/defaults/#{context[:version]}.jpg"
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# When deleting versions, any multi delete capabilities will be leveraged,
|
66
|
+
# so when usingStorage::S3, deleting versions will issue only a single HTTP
|
67
|
+
# request.
|
68
|
+
module Versions
|
69
|
+
def self.load_dependencies(uploader, *)
|
70
|
+
uploader.plugin :multi_delete
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.configure(uploader, names:)
|
74
|
+
uploader.opts[:version_names] = names
|
75
|
+
end
|
76
|
+
|
77
|
+
module ClassMethods
|
78
|
+
def version_names
|
79
|
+
opts[:version_names]
|
80
|
+
end
|
81
|
+
|
82
|
+
# Checks that the identifier is a registered version.
|
83
|
+
def version?(name)
|
84
|
+
version_names.map(&:to_s).include?(name.to_s)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Asserts that the hash doesn't contain any unknown versions.
|
88
|
+
def versions!(hash)
|
89
|
+
hash.select do |name, version|
|
90
|
+
version?(name) or raise Error, "unknown version: #{name.inspect}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Filters the hash to contain only the registered versions.
|
95
|
+
def versions(hash)
|
96
|
+
hash.select { |name, version| version?(name) }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Converts a hash of data into a hash of versions.
|
100
|
+
def uploaded_file(object, &block)
|
101
|
+
if object.is_a?(Hash) && !object.key?("storage")
|
102
|
+
versions(object).inject({}) do |result, (name, data)|
|
103
|
+
result.update(name.to_sym => super(data, &block))
|
104
|
+
end
|
105
|
+
else
|
106
|
+
super
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
module InstanceMethods
|
112
|
+
# Checks whether all versions are uploaded by this uploader.
|
113
|
+
def uploaded?(uploaded_file)
|
114
|
+
if (hash = uploaded_file).is_a?(Hash)
|
115
|
+
hash.all? { |name, version| super(version) }
|
116
|
+
else
|
117
|
+
super
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
# Stores each version individually. It asserts that all versions are
|
124
|
+
# known, because later the versions will be silently filtered, so
|
125
|
+
# we want to let the user know that they forgot to register a new
|
126
|
+
# version.
|
127
|
+
def _store(io, context)
|
128
|
+
if (hash = io).is_a?(Hash)
|
129
|
+
self.class.versions!(hash).inject({}) do |result, (name, version)|
|
130
|
+
result.update(name => super(version, version: name, **context))
|
131
|
+
end
|
132
|
+
else
|
133
|
+
super
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Deletes each file individually, but uses S3's multi delete
|
138
|
+
# capabilities.
|
139
|
+
def _delete(uploaded_file, context)
|
140
|
+
if (versions = uploaded_file).is_a?(Hash)
|
141
|
+
super(versions.values, context)
|
142
|
+
versions
|
143
|
+
else
|
144
|
+
super
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
module AttacherMethods
|
150
|
+
# Smart versioned URLs, which include the version name in the default
|
151
|
+
# URL, and properly forwards any options to the underlying storage.
|
152
|
+
def url(version = nil, **options)
|
153
|
+
if get.is_a?(Hash)
|
154
|
+
if version
|
155
|
+
raise Error, "unknown version: #{version.inspect}" if !shrine_class.version_names.include?(version)
|
156
|
+
if file = get[version]
|
157
|
+
file.url(**options)
|
158
|
+
else
|
159
|
+
default_url(options.merge(version: version))
|
160
|
+
end
|
161
|
+
else
|
162
|
+
raise Error, "must call #{name}_url with the name of the version"
|
163
|
+
end
|
164
|
+
else
|
165
|
+
if get || version.nil?
|
166
|
+
super(**options)
|
167
|
+
else
|
168
|
+
default_url(options.merge(version: version))
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
register_plugin(:versions, Versions)
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require "down"
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
class Shrine
|
7
|
+
module Storage
|
8
|
+
class FileSystem
|
9
|
+
attr_reader :directory, :subdirectory, :host, :permissions
|
10
|
+
|
11
|
+
# The `directory` is the root directory where uploaded files will be
|
12
|
+
# stored. In web applications this is typically the "public/" directory,
|
13
|
+
# to make the files available via URL.
|
14
|
+
#
|
15
|
+
# If `:subdirectory` is given, #url will return a URL relative to
|
16
|
+
# `directory` (and include `:subdirectory`). So, `FileSystem.new('public',
|
17
|
+
# subdirectory: 'uploads')` will upload files to "public/uploads", and
|
18
|
+
# URLs will be "/uploads/*".
|
19
|
+
#
|
20
|
+
# In applications it's common to serve files over CDN, so an additional
|
21
|
+
# `:host` option can be provided. This option can also be used without
|
22
|
+
# `:subdirectory`, if for example files are located on another server
|
23
|
+
# which requires an IP address.
|
24
|
+
#
|
25
|
+
# By default FileSystem will clean empty directories when files get
|
26
|
+
# deleted. However, if this puts too much load on the filesystem, it can
|
27
|
+
# be disabled with `clean: false`.
|
28
|
+
#
|
29
|
+
# Optional folder and file permissions can be set through the
|
30
|
+
# `:permissions` option.
|
31
|
+
def initialize(directory, subdirectory: nil, host: nil, clean: true, permissions: nil)
|
32
|
+
if subdirectory
|
33
|
+
@subdirectory = Pathname(relative(subdirectory))
|
34
|
+
@directory = Pathname(directory).join(@subdirectory)
|
35
|
+
else
|
36
|
+
@directory = Pathname(directory)
|
37
|
+
end
|
38
|
+
|
39
|
+
@host = host
|
40
|
+
@permissions = permissions
|
41
|
+
@clean = clean
|
42
|
+
|
43
|
+
@directory.mkpath
|
44
|
+
@directory.chmod(permissions) if permissions
|
45
|
+
end
|
46
|
+
|
47
|
+
# Copies the file into the given location.
|
48
|
+
def upload(io, id, metadata = {})
|
49
|
+
IO.copy_stream(io, path!(id)); io.rewind
|
50
|
+
path(id).chmod(permissions) if permissions
|
51
|
+
end
|
52
|
+
|
53
|
+
# Downloads the file from the given location, and returns a `Tempfile`.
|
54
|
+
def download(id)
|
55
|
+
Down.copy_to_tempfile(id, open(id))
|
56
|
+
end
|
57
|
+
|
58
|
+
# Moves the file to the given location. This gets called by the "moving"
|
59
|
+
# plugin.
|
60
|
+
def move(io, id, metadata = {})
|
61
|
+
if io.respond_to?(:path)
|
62
|
+
FileUtils.mv io.path, path!(id)
|
63
|
+
else
|
64
|
+
FileUtils.mv io.storage.path(io.id), path!(id)
|
65
|
+
io.storage.clean(io.id) if io.storage.clean?
|
66
|
+
end
|
67
|
+
path(id).chmod(permissions) if permissions
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns true if the file is a `File` or a UploadedFile uploaded by the
|
71
|
+
# FileSystem storage.
|
72
|
+
def movable?(io, id)
|
73
|
+
io.respond_to?(:path) ||
|
74
|
+
(io.is_a?(UploadedFile) && io.storage.is_a?(Storage::FileSystem))
|
75
|
+
end
|
76
|
+
|
77
|
+
# Opens the file on the given location in read mode.
|
78
|
+
def open(id)
|
79
|
+
path(id).open("rb")
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns the contents of the file as a String.
|
83
|
+
def read(id)
|
84
|
+
path(id).binread
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns true if the file exists on the filesystem.
|
88
|
+
def exists?(id)
|
89
|
+
path(id).exist?
|
90
|
+
end
|
91
|
+
|
92
|
+
# Delets the file, and by default deletes the containing directory if
|
93
|
+
# it's empty.
|
94
|
+
def delete(id)
|
95
|
+
path(id).delete
|
96
|
+
clean(id) if clean?
|
97
|
+
end
|
98
|
+
|
99
|
+
# If #subdirectory is present, returns the path relative to #directory,
|
100
|
+
# with an optional #host in front. Otherwise returns the full path to the
|
101
|
+
# file (also with an optional #host).
|
102
|
+
def url(id, **options)
|
103
|
+
if subdirectory
|
104
|
+
File.join(host || "", subdirectory, id)
|
105
|
+
else
|
106
|
+
if host
|
107
|
+
File.join(host, path(id))
|
108
|
+
else
|
109
|
+
path(id).to_s
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Without any options it deletes all files from the #directory (and this
|
115
|
+
# requires confirmation). If `:older_than` is passed in (a `Time`
|
116
|
+
# object), deletes all files which were last modified before that time.
|
117
|
+
def clear!(confirm = nil, older_than: nil)
|
118
|
+
if older_than
|
119
|
+
directory.find do |path|
|
120
|
+
path.mtime < older_than ? path.rmtree : Find.prune
|
121
|
+
end
|
122
|
+
else
|
123
|
+
raise Shrine::Confirm unless confirm == :confirm
|
124
|
+
directory.rmtree
|
125
|
+
directory.mkpath
|
126
|
+
directory.chmod(permissions) if permissions
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
protected
|
131
|
+
|
132
|
+
# Returns the full path to the file.
|
133
|
+
def path(id)
|
134
|
+
directory.join(relative(id))
|
135
|
+
end
|
136
|
+
|
137
|
+
# Cleans all empty subdirectories up the hierarchy.
|
138
|
+
def clean(id)
|
139
|
+
path(id).dirname.ascend do |pathname|
|
140
|
+
if pathname.children.empty? && pathname != directory
|
141
|
+
pathname.rmdir
|
142
|
+
else
|
143
|
+
break
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def clean?
|
149
|
+
@clean
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
# Creates all intermediate directories for that location.
|
155
|
+
def path!(id)
|
156
|
+
path(id).dirname.mkpath
|
157
|
+
path(id)
|
158
|
+
end
|
159
|
+
|
160
|
+
def relative(path)
|
161
|
+
path.sub(%r{^/}, "")
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|