shrine 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shrine might be problematic. Click here for more details.

Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -149
  3. data/doc/carrierwave.md +12 -16
  4. data/doc/changing_location.md +50 -0
  5. data/doc/creating_plugins.md +2 -2
  6. data/doc/creating_storages.md +70 -9
  7. data/doc/direct_s3.md +132 -61
  8. data/doc/migrating_storage.md +12 -10
  9. data/doc/paperclip.md +12 -17
  10. data/doc/refile.md +338 -0
  11. data/doc/regenerating_versions.md +75 -11
  12. data/doc/securing_uploads.md +172 -0
  13. data/lib/shrine.rb +21 -16
  14. data/lib/shrine/plugins/activerecord.rb +2 -2
  15. data/lib/shrine/plugins/background_helpers.rb +2 -148
  16. data/lib/shrine/plugins/backgrounding.rb +148 -0
  17. data/lib/shrine/plugins/backup.rb +88 -0
  18. data/lib/shrine/plugins/data_uri.rb +25 -4
  19. data/lib/shrine/plugins/default_url.rb +37 -0
  20. data/lib/shrine/plugins/delete_uploaded.rb +40 -0
  21. data/lib/shrine/plugins/determine_mime_type.rb +4 -2
  22. data/lib/shrine/plugins/direct_upload.rb +107 -62
  23. data/lib/shrine/plugins/download_endpoint.rb +157 -0
  24. data/lib/shrine/plugins/hooks.rb +19 -5
  25. data/lib/shrine/plugins/keep_location.rb +43 -0
  26. data/lib/shrine/plugins/moving.rb +11 -10
  27. data/lib/shrine/plugins/parallelize.rb +1 -5
  28. data/lib/shrine/plugins/parsed_json.rb +7 -1
  29. data/lib/shrine/plugins/pretty_location.rb +6 -0
  30. data/lib/shrine/plugins/rack_file.rb +7 -1
  31. data/lib/shrine/plugins/remove_invalid.rb +22 -0
  32. data/lib/shrine/plugins/sequel.rb +2 -2
  33. data/lib/shrine/plugins/upload_options.rb +41 -0
  34. data/lib/shrine/plugins/versions.rb +9 -7
  35. data/lib/shrine/storage/file_system.rb +46 -30
  36. data/lib/shrine/storage/linter.rb +48 -25
  37. data/lib/shrine/storage/s3.rb +89 -22
  38. data/lib/shrine/version.rb +1 -1
  39. data/shrine.gemspec +3 -3
  40. metadata +16 -5
@@ -0,0 +1,157 @@
1
+ require "roda"
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # The download_endpoint plugin provides a [Roda] endpoint for downloading
6
+ # uploaded files from specified storages. This is useful when files from
7
+ # your storages aren't accessible over URL (e.g. database storages) or if
8
+ # you want to authenticate your downloads.
9
+ #
10
+ # plugin :download_endpoint, storages: [:store], prefix: "attachments"
11
+ #
12
+ # After loading the plugin the endpoint should be mounted:
13
+ #
14
+ # Rails.appliations.routes.draw do
15
+ # mount Shrine::DownloadEndpoint => "/attachments"
16
+ # end
17
+ #
18
+ # Now all stored files can be downloaded through the endpoint, and the
19
+ # endpoint will efficiently stream the file from the storage when the
20
+ # storage supports it. `UploadedFile#url` will automatically return the URL
21
+ # to the endpoint for specified storages, so it's not needed to change the
22
+ # code:
23
+ #
24
+ # user.avatar_url #=> "/attachments/store/sdg0lsf8.jpg"
25
+ #
26
+ # :storages
27
+ # : An array of storage keys which the download endpoint should be used for.
28
+ #
29
+ # :prefix
30
+ # : The location where the download endpoint was mounted. If it was
31
+ # mounted at the root level, this should be set to nil.
32
+ #
33
+ # :host
34
+ # : The host that you want the download URLs to use (e.g. your app's domain
35
+ # name or a CDN). By default URLs are relative.
36
+ #
37
+ # :disposition
38
+ # : Can be set to "attachment" if you want that the user is always
39
+ # prompted to download the file when visiting the download URL.
40
+ # The default is "inline".
41
+ #
42
+ # This plugin is also suitable on Heroku when using FileSystem storage for
43
+ # cache. On Heroku files cannot be stored to the "public" folder but rather
44
+ # to the "tmp" folder, which means that by default it's not possible to
45
+ # show the URL to the cached file. The download endpoint generates the URL
46
+ # to any file, regardless of its location.
47
+ #
48
+ # [Roda]: https://github.com/jeremyevans/roda
49
+ module DownloadEndpoint
50
+ def self.configure(uploader, storages:, prefix:, disposition: "inline", host: nil)
51
+ uploader.opts[:download_endpoint_storages] = storages
52
+ uploader.opts[:download_endpoint_prefix] = prefix
53
+ uploader.opts[:download_endpoint_disposition] = disposition
54
+ uploader.opts[:download_endpoint_host] = host
55
+
56
+ uploader.assign_download_endpoint(App) unless uploader.const_defined?(:DownloadEndpoint)
57
+ end
58
+
59
+ module ClassMethods
60
+ # Assigns the subclass a copy of the download endpoint class.
61
+ def inherited(subclass)
62
+ super
63
+ subclass.assign_download_endpoint(self::DownloadEndpoint)
64
+ end
65
+
66
+ # Assigns the subclassed endpoint as the `DownloadEndpoint` constant.
67
+ def assign_download_endpoint(klass)
68
+ endpoint_class = Class.new(klass)
69
+ endpoint_class.opts[:shrine_class] = self
70
+ const_set(:DownloadEndpoint, endpoint_class)
71
+ end
72
+ end
73
+
74
+ module FileMethods
75
+ # Constructs the URL from the optional host, prefix, storage key and
76
+ # uploaded file's id. For other uploaded files that aren't in the list
77
+ # of storages it just returns their original URL.
78
+ def url(**options)
79
+ if shrine_class.opts[:download_endpoint_storages].include?(storage_key.to_sym)
80
+ [
81
+ shrine_class.opts[:download_endpoint_host],
82
+ *shrine_class.opts[:download_endpoint_prefix],
83
+ storage_key,
84
+ id,
85
+ ].join("/")
86
+ else
87
+ super(options)
88
+ end
89
+ end
90
+ end
91
+
92
+ # Routes incoming requests. It first asserts that the storage is existent
93
+ # and allowed. Afterwards it proceeds with the file download using
94
+ # streaming.
95
+ class App < Roda
96
+ plugin :streaming
97
+
98
+ route do |r|
99
+ r.on ":storage" do |storage_key|
100
+ allow_storage!(storage_key)
101
+ @storage = shrine_class.find_storage(storage_key)
102
+
103
+ r.get /(.*)/ do |id|
104
+ filename = request.path.split("/").last
105
+ extname = File.extname(filename)
106
+
107
+ response["Content-Disposition"] = "#{disposition}; filename=#{filename.inspect}"
108
+ response["Content-Type"] = Rack::Mime.mime_type(extname)
109
+
110
+ stream do |out|
111
+ if @storage.respond_to?(:stream)
112
+ @storage.stream(id) { |chunk| out << chunk }
113
+ else
114
+ io, buffer = @storage.open(id), ""
115
+ out << io.read(16384, buffer) until io.eof?
116
+ io.close
117
+ io.delete if io.class.name == "Tempfile"
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ # Halts the request if storage is not allowed.
127
+ def allow_storage!(storage)
128
+ if !allowed_storages.map(&:to_s).include?(storage)
129
+ error! 403, "Storage #{storage.inspect} is not allowed."
130
+ end
131
+ end
132
+
133
+ # Halts the request with the error message.
134
+ def error!(status, message)
135
+ response.status = status
136
+ response["Content-Type"] = "application/json"
137
+ response.write({error: message}.to_json)
138
+ request.halt
139
+ end
140
+
141
+ def disposition
142
+ shrine_class.opts[:download_endpoint_disposition]
143
+ end
144
+
145
+ def allowed_storages
146
+ shrine_class.opts[:download_endpoint_storages]
147
+ end
148
+
149
+ def shrine_class
150
+ opts[:shrine_class]
151
+ end
152
+ end
153
+ end
154
+
155
+ register_plugin(:download_endpoint, DownloadEndpoint)
156
+ end
157
+ end
@@ -18,7 +18,7 @@ class Shrine
18
18
  #
19
19
  # Each hook will be called with 2 arguments, `io` and `context`. You should
20
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.
21
+ # hooks internally, and without `super` those wouldn't get executed.
22
22
  #
23
23
  # Shrine calls hooks in the following order when uploading a file:
24
24
  #
@@ -41,6 +41,17 @@ class Shrine
41
41
  # * DELETE
42
42
  # * `after_delete`
43
43
  #
44
+ # By default every `around_*` hook returns the result of the corresponding
45
+ # operation:
46
+ #
47
+ # class ImageUploader < Shrine
48
+ # def around_store(io, context)
49
+ # result = super
50
+ # result.class #=> Shrine::UploadedFile
51
+ # result # it's good to always return the result for consistent behaviour
52
+ # end
53
+ # end
54
+ #
44
55
  # It may be useful to know that you can realize some form of communication
45
56
  # between the hooks; whatever you save to the `context` hash will be
46
57
  # forwarded further down:
@@ -70,8 +81,9 @@ class Shrine
70
81
 
71
82
  def around_upload(*args)
72
83
  before_upload(*args)
73
- yield
84
+ result = yield
74
85
  after_upload(*args)
86
+ result
75
87
  end
76
88
 
77
89
  def before_upload(*)
@@ -90,8 +102,9 @@ class Shrine
90
102
 
91
103
  def around_process(*args)
92
104
  before_process(*args)
93
- yield
105
+ result = yield
94
106
  after_process(*args)
107
+ result
95
108
  end
96
109
 
97
110
  def before_process(*)
@@ -109,8 +122,9 @@ class Shrine
109
122
 
110
123
  def around_store(*args)
111
124
  before_store(*args)
112
- yield
125
+ result = yield
113
126
  after_store(*args)
127
+ result
114
128
  end
115
129
 
116
130
  def before_store(*)
@@ -128,7 +142,7 @@ class Shrine
128
142
 
129
143
  def around_delete(*args)
130
144
  before_delete(*args)
131
- yield
145
+ result = yield
132
146
  after_delete(*args)
133
147
  end
134
148
 
@@ -0,0 +1,43 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The keep_location plugin allows you to preserve locations when
4
+ # transferring files from one storage to another. This can be useful for
5
+ # debugging purposes or for backups.
6
+ #
7
+ # plugin :keep_location, :cache => :store
8
+ #
9
+ # The above will preserve location of cached files uploaded to store. More
10
+ # precisely, if a Shrine::UploadedFile from cache is begin uploaded to
11
+ # store, the stored file will have the same location as the cached file.
12
+ #
13
+ # The destination storage can also be specified as an array:
14
+ #
15
+ # plugin :keep_location, :cache => [:storage1, :storage2]
16
+ module KeepLocation
17
+ def self.configure(uploader, mappings = {})
18
+ uploader.opts[:keep_location_mappings] = mappings
19
+ end
20
+
21
+ module InstanceMethods
22
+ private
23
+
24
+ def get_location(io, context)
25
+ if io.is_a?(UploadedFile) && keep_location?(io)
26
+ io.id
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ def keep_location?(uploaded_file)
33
+ opts[:keep_location_mappings].any? do |source, destination|
34
+ source == uploaded_file.storage_key.to_sym &&
35
+ Array(destination).include?(self.storage_key)
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ register_plugin(:keep_location, KeepLocation)
42
+ end
43
+ end
@@ -11,9 +11,9 @@ class Shrine
11
11
  # plugin :moving, storages: [:cache]
12
12
  #
13
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:
14
+ # to. The above will move raw files to cache (without this plugin it's
15
+ # simply copied over). However, you may want to move cached files to
16
+ # `:store` as well:
17
17
  #
18
18
  # plugin :moving, storages: [:cache, :store]
19
19
  #
@@ -21,7 +21,7 @@ class Shrine
21
21
  # being uploaded will be deleted afterwards. However, if both the file
22
22
  # being uploaded and the destination are on the filesystem, a `mv` command
23
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
24
+ # well, usually if also both the cache and store are using the same
25
25
  # storage.
26
26
  module Moving
27
27
  def self.configure(uploader, storages:)
@@ -31,11 +31,9 @@ class Shrine
31
31
  module InstanceMethods
32
32
  private
33
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)
34
+ # If the storage supports moving we use that, otherwise we do moving by
35
+ # copying and deleting.
36
+ def copy(io, context)
39
37
  if move?(io, context)
40
38
  if movable?(io, context)
41
39
  move(io, context)
@@ -48,7 +46,10 @@ class Shrine
48
46
  end
49
47
  end
50
48
 
51
- # Ask the storage if the given file is movable.
49
+ def move(io, context)
50
+ storage.move(io, context[:location], context[:metadata])
51
+ end
52
+
52
53
  def movable?(io, context)
53
54
  storage.respond_to?(:move) && storage.movable?(io, context[:location])
54
55
  end
@@ -35,11 +35,7 @@ class Shrine
35
35
 
36
36
  private
37
37
 
38
- def copy(io, context)
39
- context[:thread_pool].process { super }
40
- end
41
-
42
- def move(io, context)
38
+ def put(io, context)
43
39
  context[:thread_pool].process { super }
44
40
  end
45
41
 
@@ -8,12 +8,18 @@ class Shrine
8
8
  module ParsedJson
9
9
  module AttacherMethods
10
10
  def assign(value)
11
- if value.is_a?(Hash) && value.keys.any? { |key| key.is_a?(String) }
11
+ if value.is_a?(Hash) && parsed_json?(value)
12
12
  assign(value.to_json)
13
13
  else
14
14
  super
15
15
  end
16
16
  end
17
+
18
+ private
19
+
20
+ def parsed_json?(hash)
21
+ hash.keys.any? { |key| key.is_a?(String) }
22
+ end
17
23
  end
18
24
  end
19
25
 
@@ -24,6 +24,12 @@ class Shrine
24
24
 
25
25
  [type, id, name, original].compact.join("/")
26
26
  end
27
+
28
+ private
29
+
30
+ def generate_uid(io)
31
+ SecureRandom.hex(5)
32
+ end
27
33
  end
28
34
  end
29
35
 
@@ -25,12 +25,18 @@ class Shrine
25
25
  # Checks whether a file is a Rack file hash, and in that case wraps the
26
26
  # hash in an IO-like object.
27
27
  def assign(value)
28
- if value.is_a?(Hash) && value.key?(:tempfile)
28
+ if value.is_a?(Hash) && rack_file?(value)
29
29
  assign(UploadedFile.new(value))
30
30
  else
31
31
  super
32
32
  end
33
33
  end
34
+
35
+ private
36
+
37
+ def rack_file?(hash)
38
+ hash.key?(:tempfile)
39
+ end
34
40
  end
35
41
 
36
42
  # This is used to wrap the Rack hash into an IO-like object which Shrine
@@ -0,0 +1,22 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The remove_invalid plugin automatically deletes a cached file if it was
4
+ # invalid and deassigns it from the record.
5
+ #
6
+ # plugin :remove_invalid
7
+ module RemoveInvalid
8
+ module AttacherMethods
9
+ def validate
10
+ super
11
+ ensure
12
+ if errors.any? && cache.uploaded?(get)
13
+ delete!(get, phase: :invalid)
14
+ _set(nil)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ register_plugin(:remove_invalid, RemoveInvalid)
21
+ end
22
+ end
@@ -20,7 +20,7 @@ class Shrine
20
20
  # tests.
21
21
  #
22
22
  # If you want to put some parts of this lifecycle into a background job, see
23
- # the background_helpers plugin.
23
+ # the backgrounding plugin.
24
24
  #
25
25
  # Additionally, any Shrine validation errors will added to Sequel's
26
26
  # errors upon validation. Note that if you want to validate presence of the
@@ -62,7 +62,7 @@ class Shrine
62
62
  end
63
63
 
64
64
  module AttacherClassMethods
65
- # Needed by the background_helpers plugin.
65
+ # Needed by the backgrounding plugin.
66
66
  def find_record(record_class, record_id)
67
67
  record_class.with_pk!(record_id)
68
68
  end
@@ -0,0 +1,41 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The upload_options allows you to create additional upload options depending
4
+ # on file and context, and forward them to the underlying storage.
5
+ #
6
+ # plugin :upload_options, store: ->(io, context) do
7
+ # if [:original, :thumb].include?(context[:version])
8
+ # {acl: "public-read"}
9
+ # else
10
+ # {acl: "private"}
11
+ # end
12
+ # end
13
+ #
14
+ # Keys are names of the registered storages, and values are either blocks
15
+ # or hashes.
16
+ module UploadOptions
17
+ def self.configure(uploader, options = {})
18
+ uploader.opts[:upload_options_options] = options
19
+ end
20
+
21
+ module InstanceMethods
22
+ def put(io, context)
23
+ upload_options = get_upload_options(io, context)
24
+ key = storage.class.name.split("::").last.downcase
25
+ context[:metadata][key] = upload_options if upload_options
26
+ super
27
+ end
28
+
29
+ private
30
+
31
+ def get_upload_options(io, context)
32
+ options = opts[:upload_options_options][storage_key]
33
+ options = options.call(io, context) if options.respond_to?(:call)
34
+ options
35
+ end
36
+ end
37
+ end
38
+
39
+ register_plugin(:upload_options, UploadOptions)
40
+ end
41
+ end