shrine 2.19.3 → 3.6.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.
Files changed (211) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +523 -41
  3. data/LICENSE.txt +1 -1
  4. data/README.md +83 -979
  5. data/doc/advantages.md +231 -204
  6. data/doc/attacher.md +304 -153
  7. data/doc/carrierwave.md +297 -226
  8. data/doc/changing_derivatives.md +308 -0
  9. data/doc/changing_location.md +103 -21
  10. data/doc/changing_storage.md +110 -0
  11. data/doc/creating_persistence_plugins.md +132 -0
  12. data/doc/creating_plugins.md +43 -23
  13. data/doc/creating_storages.md +19 -5
  14. data/doc/design.md +147 -97
  15. data/doc/direct_s3.md +38 -28
  16. data/doc/external/articles.md +63 -0
  17. data/doc/external/extensions.md +53 -0
  18. data/doc/external/misc.md +32 -0
  19. data/doc/getting_started.md +1156 -0
  20. data/doc/metadata.md +190 -109
  21. data/doc/multiple_files.md +93 -30
  22. data/doc/paperclip.md +384 -262
  23. data/doc/plugins/activerecord.md +177 -46
  24. data/doc/plugins/add_metadata.md +139 -38
  25. data/doc/plugins/atomic_helpers.md +217 -0
  26. data/doc/plugins/backgrounding.md +156 -98
  27. data/doc/plugins/cached_attachment_data.md +7 -5
  28. data/doc/plugins/column.md +121 -0
  29. data/doc/plugins/data_uri.md +23 -22
  30. data/doc/plugins/default_storage.md +36 -10
  31. data/doc/plugins/default_url.md +30 -13
  32. data/doc/plugins/delete_raw.md +4 -2
  33. data/doc/plugins/derivation_endpoint.md +186 -101
  34. data/doc/plugins/derivatives.md +839 -0
  35. data/doc/plugins/determine_mime_type.md +4 -2
  36. data/doc/plugins/download_endpoint.md +64 -8
  37. data/doc/plugins/dynamic_storage.md +5 -3
  38. data/doc/plugins/entity.md +263 -0
  39. data/doc/plugins/form_assign.md +55 -0
  40. data/doc/plugins/included.md +31 -8
  41. data/doc/plugins/infer_extension.md +21 -10
  42. data/doc/plugins/instrumentation.md +38 -16
  43. data/doc/plugins/keep_files.md +16 -17
  44. data/doc/plugins/metadata_attributes.md +42 -13
  45. data/doc/plugins/mirroring.md +118 -0
  46. data/doc/plugins/model.md +210 -0
  47. data/doc/plugins/module_include.md +4 -2
  48. data/doc/plugins/multi_cache.md +24 -0
  49. data/doc/plugins/persistence.md +101 -0
  50. data/doc/plugins/presign_endpoint.md +9 -4
  51. data/doc/plugins/pretty_location.md +16 -3
  52. data/doc/plugins/processing.md +4 -2
  53. data/doc/plugins/rack_file.md +8 -2
  54. data/doc/plugins/rack_response.md +6 -2
  55. data/doc/plugins/recache.md +4 -2
  56. data/doc/plugins/refresh_metadata.md +49 -9
  57. data/doc/plugins/remote_url.md +84 -47
  58. data/doc/plugins/remove_attachment.md +27 -6
  59. data/doc/plugins/remove_invalid.md +21 -6
  60. data/doc/plugins/restore_cached_data.md +11 -3
  61. data/doc/plugins/sequel.md +159 -35
  62. data/doc/plugins/signature.md +16 -5
  63. data/doc/plugins/store_dimensions.md +14 -2
  64. data/doc/plugins/tempfile.md +4 -2
  65. data/doc/plugins/type_predicates.md +96 -0
  66. data/doc/plugins/upload_endpoint.md +13 -13
  67. data/doc/plugins/upload_options.md +6 -4
  68. data/doc/plugins/{default_url_options.md → url_options.md} +9 -7
  69. data/doc/plugins/validation.md +97 -0
  70. data/doc/plugins/validation_helpers.md +16 -13
  71. data/doc/plugins/versions.md +15 -19
  72. data/doc/processing.md +438 -221
  73. data/doc/refile.md +188 -170
  74. data/doc/release_notes/1.0.0.md +4 -0
  75. data/doc/release_notes/1.1.0.md +6 -2
  76. data/doc/release_notes/1.2.0.md +4 -0
  77. data/doc/release_notes/1.3.0.md +4 -0
  78. data/doc/release_notes/1.4.0.md +4 -0
  79. data/doc/release_notes/1.4.1.md +4 -0
  80. data/doc/release_notes/1.4.2.md +4 -0
  81. data/doc/release_notes/2.0.0.md +4 -0
  82. data/doc/release_notes/2.0.1.md +4 -0
  83. data/doc/release_notes/2.1.0.md +5 -1
  84. data/doc/release_notes/2.1.1.md +4 -0
  85. data/doc/release_notes/2.10.0.md +4 -0
  86. data/doc/release_notes/2.10.1.md +4 -0
  87. data/doc/release_notes/2.11.0.md +4 -0
  88. data/doc/release_notes/2.12.0.md +4 -0
  89. data/doc/release_notes/2.13.0.md +4 -0
  90. data/doc/release_notes/2.14.0.md +5 -1
  91. data/doc/release_notes/2.15.0.md +11 -7
  92. data/doc/release_notes/2.16.0.md +4 -0
  93. data/doc/release_notes/2.17.0.md +4 -0
  94. data/doc/release_notes/2.18.0.md +4 -0
  95. data/doc/release_notes/2.19.0.md +6 -3
  96. data/doc/release_notes/2.2.0.md +4 -0
  97. data/doc/release_notes/2.3.0.md +4 -0
  98. data/doc/release_notes/2.3.1.md +4 -0
  99. data/doc/release_notes/2.4.0.md +4 -0
  100. data/doc/release_notes/2.4.1.md +4 -0
  101. data/doc/release_notes/2.5.0.md +4 -0
  102. data/doc/release_notes/2.6.0.md +4 -0
  103. data/doc/release_notes/2.6.1.md +4 -0
  104. data/doc/release_notes/2.7.0.md +4 -0
  105. data/doc/release_notes/2.8.0.md +4 -0
  106. data/doc/release_notes/2.9.0.md +4 -0
  107. data/doc/release_notes/3.0.0.md +981 -0
  108. data/doc/release_notes/3.0.1.md +22 -0
  109. data/doc/release_notes/3.1.0.md +73 -0
  110. data/doc/release_notes/3.2.0.md +96 -0
  111. data/doc/release_notes/3.2.1.md +31 -0
  112. data/doc/release_notes/3.2.2.md +14 -0
  113. data/doc/release_notes/3.3.0.md +105 -0
  114. data/doc/release_notes/3.4.0.md +35 -0
  115. data/doc/release_notes/3.5.0.md +63 -0
  116. data/doc/release_notes/3.6.0.md +23 -0
  117. data/doc/retrieving_uploads.md +5 -2
  118. data/doc/securing_uploads.md +60 -37
  119. data/doc/storage/file_system.md +20 -3
  120. data/doc/storage/memory.md +19 -0
  121. data/doc/storage/s3.md +122 -78
  122. data/doc/testing.md +141 -133
  123. data/doc/upgrading_to_3.md +708 -0
  124. data/doc/validation.md +54 -90
  125. data/lib/shrine/attacher.rb +292 -169
  126. data/lib/shrine/attachment.rb +13 -46
  127. data/lib/shrine/plugins/_persistence.rb +93 -0
  128. data/lib/shrine/plugins/activerecord.rb +77 -34
  129. data/lib/shrine/plugins/add_metadata.rb +25 -17
  130. data/lib/shrine/plugins/atomic_helpers.rb +119 -0
  131. data/lib/shrine/plugins/backgrounding.rb +77 -113
  132. data/lib/shrine/plugins/cached_attachment_data.rb +6 -15
  133. data/lib/shrine/plugins/column.rb +102 -0
  134. data/lib/shrine/plugins/data_uri.rb +38 -36
  135. data/lib/shrine/plugins/default_storage.rb +45 -15
  136. data/lib/shrine/plugins/default_url.rb +12 -24
  137. data/lib/shrine/plugins/default_url_options.rb +3 -30
  138. data/lib/shrine/plugins/delete_raw.rb +10 -16
  139. data/lib/shrine/plugins/derivation_endpoint.rb +130 -171
  140. data/lib/shrine/plugins/derivatives.rb +645 -0
  141. data/lib/shrine/plugins/determine_mime_type.rb +9 -21
  142. data/lib/shrine/plugins/download_endpoint.rb +118 -133
  143. data/lib/shrine/plugins/dynamic_storage.rb +5 -11
  144. data/lib/shrine/plugins/entity.rb +158 -0
  145. data/lib/shrine/plugins/form_assign.rb +108 -0
  146. data/lib/shrine/plugins/included.rb +6 -6
  147. data/lib/shrine/plugins/infer_extension.rb +17 -20
  148. data/lib/shrine/plugins/instrumentation.rb +59 -43
  149. data/lib/shrine/plugins/keep_files.rb +3 -15
  150. data/lib/shrine/plugins/metadata_attributes.rb +28 -19
  151. data/lib/shrine/plugins/mirroring.rb +142 -0
  152. data/lib/shrine/plugins/model.rb +160 -0
  153. data/lib/shrine/plugins/module_include.rb +3 -3
  154. data/lib/shrine/plugins/multi_cache.rb +27 -0
  155. data/lib/shrine/plugins/presign_endpoint.rb +27 -28
  156. data/lib/shrine/plugins/pretty_location.rb +15 -9
  157. data/lib/shrine/plugins/processing.rb +22 -9
  158. data/lib/shrine/plugins/rack_file.rb +2 -42
  159. data/lib/shrine/plugins/rack_response.rb +21 -10
  160. data/lib/shrine/plugins/recache.rb +6 -5
  161. data/lib/shrine/plugins/refresh_metadata.rb +13 -11
  162. data/lib/shrine/plugins/remote_url.rb +49 -49
  163. data/lib/shrine/plugins/remove_attachment.rb +12 -6
  164. data/lib/shrine/plugins/remove_invalid.rb +19 -8
  165. data/lib/shrine/plugins/restore_cached_data.rb +13 -7
  166. data/lib/shrine/plugins/sequel.rb +86 -36
  167. data/lib/shrine/plugins/signature.rb +10 -16
  168. data/lib/shrine/plugins/store_dimensions.rb +35 -40
  169. data/lib/shrine/plugins/tempfile.rb +1 -3
  170. data/lib/shrine/plugins/type_predicates.rb +113 -0
  171. data/lib/shrine/plugins/upload_endpoint.rb +28 -24
  172. data/lib/shrine/plugins/upload_options.rb +14 -15
  173. data/lib/shrine/plugins/url_options.rb +31 -0
  174. data/lib/shrine/plugins/validation.rb +80 -0
  175. data/lib/shrine/plugins/validation_helpers.rb +35 -58
  176. data/lib/shrine/plugins/versions.rb +107 -87
  177. data/lib/shrine/plugins.rb +22 -0
  178. data/lib/shrine/storage/file_system.rb +46 -64
  179. data/lib/shrine/storage/linter.rb +42 -7
  180. data/lib/shrine/storage/memory.rb +49 -0
  181. data/lib/shrine/storage/s3.rb +173 -160
  182. data/lib/shrine/uploaded_file.rb +32 -32
  183. data/lib/shrine/version.rb +3 -3
  184. data/lib/shrine.rb +87 -150
  185. data/shrine.gemspec +11 -12
  186. metadata +92 -82
  187. data/doc/migrating_storage.md +0 -76
  188. data/doc/plugins/backup.md +0 -31
  189. data/doc/plugins/copy.md +0 -24
  190. data/doc/plugins/delete_promoted.md +0 -12
  191. data/doc/plugins/direct_upload.md +0 -172
  192. data/doc/plugins/hooks.md +0 -58
  193. data/doc/plugins/logging.md +0 -42
  194. data/doc/plugins/migration_helpers.md +0 -60
  195. data/doc/plugins/moving.md +0 -19
  196. data/doc/plugins/multi_delete.md +0 -20
  197. data/doc/plugins/parallelize.md +0 -16
  198. data/doc/plugins/parsed_json.md +0 -23
  199. data/doc/regenerating_versions.md +0 -143
  200. data/lib/shrine/plugins/background_helpers.rb +0 -5
  201. data/lib/shrine/plugins/backup.rb +0 -90
  202. data/lib/shrine/plugins/copy.rb +0 -50
  203. data/lib/shrine/plugins/delete_promoted.rb +0 -20
  204. data/lib/shrine/plugins/direct_upload.rb +0 -217
  205. data/lib/shrine/plugins/hooks.rb +0 -90
  206. data/lib/shrine/plugins/logging.rb +0 -142
  207. data/lib/shrine/plugins/migration_helpers.rb +0 -70
  208. data/lib/shrine/plugins/moving.rb +0 -57
  209. data/lib/shrine/plugins/multi_delete.rb +0 -32
  210. data/lib/shrine/plugins/parallelize.rb +0 -78
  211. data/lib/shrine/plugins/parsed_json.rb +0 -29
@@ -2,9 +2,7 @@
2
2
 
3
3
  class Shrine
4
4
  module Plugins
5
- # Documentation lives in [doc/plugins/determine_mime_type.md] on GitHub.
6
- #
7
- # [doc/plugins/determine_mime_type.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/determine_mime_type.md
5
+ # Documentation can be found on https://shrinerb.com/docs/plugins/determine_mime_type
8
6
  module DetermineMimeType
9
7
  LOG_SUBSCRIBER = -> (event) do
10
8
  Shrine.logger.info "MIME Type (#{event.duration}ms) – #{{
@@ -13,19 +11,12 @@ class Shrine
13
11
  }.inspect}"
14
12
  end
15
13
 
16
- def self.configure(uploader, opts = {})
17
- if opts[:analyzer] == :default
18
- Shrine.deprecation("The :default analyzer of the determine_mime_type plugin has been renamed to :content_type. The :default alias will not be supported in Shrine 3.")
19
- opts = opts.merge(analyzer: :content_type)
20
- end
21
-
22
- uploader.opts[:determine_mime_type] ||= { analyzer: :file, analyzer_options: {}, log_subscriber: LOG_SUBSCRIBER }
14
+ def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, **opts)
15
+ uploader.opts[:determine_mime_type] ||= { analyzer: :file, analyzer_options: {} }
23
16
  uploader.opts[:determine_mime_type].merge!(opts)
24
17
 
25
18
  # instrumentation plugin integration
26
- if uploader.respond_to?(:subscribe)
27
- uploader.subscribe(:mime_type, &uploader.opts[:determine_mime_type][:log_subscriber])
28
- end
19
+ uploader.subscribe(:mime_type, &log_subscriber) if uploader.respond_to?(:subscribe)
29
20
  end
30
21
 
31
22
  module ClassMethods
@@ -79,11 +70,6 @@ class Shrine
79
70
  def extract_mime_type(io)
80
71
  self.class.determine_mime_type(io)
81
72
  end
82
-
83
- # Returns a hash of built-in MIME type analyzers.
84
- def mime_type_analyzers
85
- self.class.mime_type_analyzers
86
- end
87
73
  end
88
74
 
89
75
  class MimeTypeAnalyzer
@@ -138,6 +124,8 @@ class Shrine
138
124
  require "fastimage"
139
125
 
140
126
  type = FastImage.type(io)
127
+ return 'image/svg+xml' if type == :svg
128
+
141
129
  "image/#{type}" if type
142
130
  end
143
131
 
@@ -155,7 +143,7 @@ class Shrine
155
143
  require "mimemagic"
156
144
 
157
145
  mime = MimeMagic.by_magic(io)
158
- mime.type if mime
146
+ mime&.type
159
147
  end
160
148
 
161
149
  def extract_with_marcel(io, options)
@@ -172,7 +160,7 @@ class Shrine
172
160
 
173
161
  if filename = extract_filename(io)
174
162
  mime_type = MIME::Types.of(filename).first
175
- mime_type.content_type if mime_type
163
+ mime_type&.content_type
176
164
  end
177
165
  end
178
166
 
@@ -181,7 +169,7 @@ class Shrine
181
169
 
182
170
  if filename = extract_filename(io)
183
171
  info = MiniMime.lookup_by_filename(filename)
184
- info.content_type if info
172
+ info&.content_type
185
173
  end
186
174
  end
187
175
 
@@ -2,78 +2,60 @@
2
2
 
3
3
  class Shrine
4
4
  module Plugins
5
- # Documentation lives in [doc/plugins/download_endpoint.md] on GitHub.
6
- #
7
- # [doc/plugins/download_endpoint.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/download_endpoint.md
5
+ # Documentation can be found on https://shrinerb.com/docs/plugins/download_endpoint
8
6
  module DownloadEndpoint
9
- def self.load_dependencies(uploader, opts = {})
7
+ def self.load_dependencies(uploader, **)
10
8
  uploader.plugin :rack_response
11
9
  uploader.plugin :_urlsafe_serialization
12
10
  end
13
11
 
14
- def self.configure(uploader, opts = {})
12
+ def self.configure(uploader, **opts)
15
13
  uploader.opts[:download_endpoint] ||= { disposition: "inline", download_options: {} }
16
14
  uploader.opts[:download_endpoint].merge!(opts)
17
-
18
- Shrine.deprecation("The :storages download_endpoint option is deprecated, you should use UploadedFile#download_url for generating URLs to the download endpoint.") if uploader.opts[:download_endpoint][:storages]
19
-
20
- uploader.assign_download_endpoint(App) unless uploader.const_defined?(:DownloadEndpoint)
21
15
  end
22
16
 
23
17
  module ClassMethods
24
- # Assigns the subclass a copy of the download endpoint class.
25
- def inherited(subclass)
26
- super
27
- subclass.assign_download_endpoint(@download_endpoint)
28
- end
29
-
30
18
  # Returns the Rack application that retrieves requested files.
31
19
  def download_endpoint(**options)
32
- new_download_endpoint(App, **options)
33
- end
34
-
35
- # Assigns the subclassed endpoint as the `DownloadEndpoint` constant.
36
- def assign_download_endpoint(app_class)
37
- @download_endpoint = new_download_endpoint(app_class)
38
-
39
- const_set(:DownloadEndpoint, @download_endpoint)
40
- deprecate_constant(:DownloadEndpoint)
41
- end
42
-
43
- private
44
-
45
- def new_download_endpoint(app_class, **options)
46
- app_class.new(
20
+ Shrine::DownloadEndpoint.new(
47
21
  shrine_class: self,
48
22
  **opts[:download_endpoint],
49
23
  **options,
50
24
  )
51
25
  end
52
- end
53
26
 
54
- module FileMethods
55
- # Constructs the URL from the optional host, prefix, storage key and
56
- # uploaded file's id. For other uploaded files that aren't in the list
57
- # of storages it just returns their original URL.
58
- def url(**options)
59
- if download_storages && download_storages.include?(storage_key.to_sym)
60
- Shrine.deprecation("The :storages option for download_endpoint plugin is deprecated and will be obsolete in Shrine 3. Use UploadedFile#download_url instead.")
61
- download_url
62
- else
63
- super
27
+ # Calls the download endpoint passing the request information, and
28
+ # returns the Rack response triple.
29
+ #
30
+ # It uses a trick where it removes the download path prefix from the
31
+ # path info before calling the Rack app, which is what web framework
32
+ # routers do before they're calling a mounted Rack app.
33
+ def download_response(env, **options)
34
+ script_name = env["SCRIPT_NAME"]
35
+ path_info = env["PATH_INFO"]
36
+
37
+ prefix = opts[:download_endpoint][:prefix]
38
+ match = path_info.match(/^\/#{prefix}/)
39
+
40
+ fail Error, "request path must start with \"/#{prefix}\", but is \"#{path_info}\"" unless match
41
+
42
+ begin
43
+ env["SCRIPT_NAME"] += match.to_s
44
+ env["PATH_INFO"] = match.post_match
45
+
46
+ download_endpoint(**options).call(env)
47
+ ensure
48
+ env["SCRIPT_NAME"] = script_name
49
+ env["PATH_INFO"] = path_info
64
50
  end
65
51
  end
52
+ end
66
53
 
54
+ module FileMethods
67
55
  # Returns file URL on the download endpoint.
68
56
  def download_url(**options)
69
57
  FileUrl.new(self).call(**options)
70
58
  end
71
-
72
- private
73
-
74
- def download_storages
75
- shrine_class.opts[:download_endpoint][:storages]
76
- end
77
59
  end
78
60
 
79
61
  class FileUrl
@@ -105,116 +87,119 @@ class Shrine
105
87
  file.shrine_class.opts[:download_endpoint]
106
88
  end
107
89
  end
90
+ end
108
91
 
109
- # Routes incoming requests. It first asserts that the storage is existent
110
- # and allowed. Afterwards it proceeds with the file download using
111
- # streaming.
112
- class App
113
- # Writes given options to instance variables.
114
- def initialize(options)
115
- options.each do |name, value|
116
- instance_variable_set("@#{name}", value)
117
- end
118
- end
92
+ register_plugin(:download_endpoint, DownloadEndpoint)
93
+ end
119
94
 
120
- def call(env)
121
- request = Rack::Request.new(env)
122
95
 
123
- status, headers, body = catch(:halt) do
124
- error!(405, "Method Not Allowed") unless request.get?
96
+ # Routes incoming requests. It first asserts that the storage is existent
97
+ # and allowed. Afterwards it proceeds with the file download using
98
+ # streaming.
99
+ class DownloadEndpoint
100
+ # Writes given options to instance variables.
101
+ def initialize(options)
102
+ options.each do |name, value|
103
+ instance_variable_set("@#{name}", value)
104
+ end
105
+ end
125
106
 
126
- handle_request(request)
127
- end
107
+ def call(env)
108
+ request = Rack::Request.new(env)
128
109
 
129
- headers["Content-Length"] ||= body.map(&:bytesize).inject(0, :+).to_s
110
+ status, headers, body = catch(:halt) do
111
+ error!(405, "Method Not Allowed") unless request.get?
130
112
 
131
- [status, headers, body]
132
- end
113
+ handle_request(request)
114
+ end
133
115
 
134
- def inspect
135
- "#<#{@shrine_class}::DownloadEndpoint>"
136
- end
137
- alias to_s inspect
116
+ headers = Rack::Headers[headers] if Rack.release >= "3"
117
+ headers["Content-Length"] ||= body.respond_to?(:bytesize) ? body.bytesize.to_s :
118
+ body.map(&:bytesize).inject(0, :+).to_s
138
119
 
139
- private
120
+ [status, headers, body]
121
+ end
140
122
 
141
- def handle_request(request)
142
- _, *components = request.path_info.split("/")
123
+ def inspect
124
+ "#<#{@shrine_class}::DownloadEndpoint>"
125
+ end
126
+ alias to_s inspect
143
127
 
144
- if components.count == 1
145
- uploaded_file = get_uploaded_file(components.first)
146
- elsif components.count == 2
147
- # handle legacy "/:storage/:id" URLs
148
- uploaded_file = @shrine_class::UploadedFile.new(
149
- "storage" => components.first,
150
- "id" => components.last,
151
- )
152
- end
128
+ private
153
129
 
154
- serve_file(uploaded_file, request)
155
- end
130
+ def handle_request(request)
131
+ _, serialized, * = request.path_info.split("/")
156
132
 
157
- # Streams or redirects to the uploaded file.
158
- def serve_file(uploaded_file, request)
159
- if @redirect
160
- redirect_to_file(uploaded_file, request)
161
- else
162
- stream_file(uploaded_file, request)
163
- end
164
- end
133
+ uploaded_file = get_uploaded_file(serialized)
165
134
 
166
- # Streams the uploaded file content.
167
- def stream_file(uploaded_file, request)
168
- open_file(uploaded_file, request)
135
+ serve_file(uploaded_file, request)
136
+ end
169
137
 
170
- response = uploaded_file.to_rack_response(
171
- disposition: @disposition,
172
- range: request.env["HTTP_RANGE"],
173
- )
138
+ # Streams or redirects to the uploaded file.
139
+ def serve_file(uploaded_file, request)
140
+ if @redirect
141
+ redirect_to_file(uploaded_file, request)
142
+ else
143
+ stream_file(uploaded_file, request)
144
+ end
145
+ end
174
146
 
175
- response[1]["Cache-Control"] = "max-age=#{365*24*60*60}" # cache for a year
147
+ # Streams the uploaded file content.
148
+ def stream_file(uploaded_file, request)
149
+ open_file(uploaded_file, request)
176
150
 
177
- response
178
- end
151
+ response = uploaded_file.to_rack_response(
152
+ disposition: @disposition,
153
+ range: request.env["HTTP_RANGE"],
154
+ )
179
155
 
180
- # Redirects to the uploaded file's direct URL or the specified URL proc.
181
- def redirect_to_file(uploaded_file, request)
182
- if @redirect == true
183
- redirect_url = uploaded_file.url
184
- else
185
- redirect_url = @redirect.call(uploaded_file, request)
186
- end
156
+ response[1]["Cache-Control"] = "max-age=#{365*24*60*60}" # cache for a year
187
157
 
188
- [302, { "Location" => redirect_url }, []]
189
- end
158
+ response
159
+ end
190
160
 
191
- def open_file(uploaded_file, request)
192
- download_options = @download_options
193
- download_options = download_options.call(uploaded_file, request) if download_options.respond_to?(:call)
161
+ # Redirects to the uploaded file's direct URL or the specified URL proc.
162
+ def redirect_to_file(uploaded_file, request)
163
+ if @redirect == true
164
+ redirect_url = uploaded_file.url
165
+ else
166
+ redirect_url = @redirect.call(uploaded_file, request)
167
+ end
194
168
 
195
- uploaded_file.open(**download_options)
196
- end
169
+ [302, { "Location" => redirect_url }, []]
170
+ end
197
171
 
198
- # Returns a Shrine::UploadedFile, or returns 404 if file doesn't exist.
199
- def get_uploaded_file(serialized)
200
- uploaded_file = @shrine_class::UploadedFile.urlsafe_load(serialized)
201
- not_found! unless uploaded_file.exists?
202
- uploaded_file
203
- rescue Shrine::Error # storage not found
204
- not_found!
205
- end
172
+ def open_file(uploaded_file, request)
173
+ download_options = @download_options
174
+ download_options = download_options.call(uploaded_file, request) if download_options.respond_to?(:call)
206
175
 
207
- def not_found!
208
- error!(404, "File Not Found")
209
- end
176
+ uploaded_file.open(**download_options)
177
+ rescue Shrine::FileNotFound
178
+ not_found!
179
+ end
210
180
 
211
- # Halts the request with the error message.
212
- def error!(status, message)
213
- throw :halt, [status, { "Content-Type" => "text/plain" }, [message]]
214
- end
215
- end
181
+ # Deserializes a Shrine::UploadedFile from a URL component. Returns 404 if
182
+ # storage is not found.
183
+ def get_uploaded_file(serialized)
184
+ @shrine_class::UploadedFile.urlsafe_load(serialized)
185
+ rescue Shrine::Error # storage not found
186
+ not_found!
187
+ rescue JSON::ParserError, ArgumentError => error # invalid serialized component
188
+ raise if error.is_a?(ArgumentError) && error.message != "invalid base64"
189
+ bad_request!("Invalid serialized file")
216
190
  end
217
191
 
218
- register_plugin(:download_endpoint, DownloadEndpoint)
192
+ def not_found!
193
+ error!(404, "File Not Found")
194
+ end
195
+
196
+ def bad_request!(message)
197
+ error!(400, message)
198
+ end
199
+
200
+ # Halts the request with the error message.
201
+ def error!(status, message)
202
+ throw :halt, [status, { "Content-Type" => "text/plain" }, [message]]
203
+ end
219
204
  end
220
205
  end
@@ -2,21 +2,15 @@
2
2
 
3
3
  class Shrine
4
4
  module Plugins
5
- # Documentation lives in [doc/plugins/dynamic_storage.md] on GitHub.
6
- #
7
- # [doc/plugins/dynamic_storage.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/dynamic_storage.md
5
+ # Documentation can be found on https://shrinerb.com/docs/plugins/dynamic_storage
8
6
  module DynamicStorage
9
- def self.configure(uploader, options = {})
10
- uploader.opts[:dynamic_storages] ||= {}
7
+ def self.configure(uploader)
8
+ uploader.opts[:dynamic_storage] ||= { resolvers: {} }
11
9
  end
12
10
 
13
11
  module ClassMethods
14
- def dynamic_storages
15
- opts[:dynamic_storages]
16
- end
17
-
18
12
  def storage(regex, &block)
19
- dynamic_storages[regex] = block
13
+ opts[:dynamic_storage][:resolvers][regex] = block
20
14
  end
21
15
 
22
16
  def find_storage(name)
@@ -26,7 +20,7 @@ class Shrine
26
20
  private
27
21
 
28
22
  def resolve_dynamic_storage(name)
29
- dynamic_storages.each do |regex, block|
23
+ opts[:dynamic_storage][:resolvers].each do |regex, block|
30
24
  if match = name.to_s.match(regex)
31
25
  return block.call(match)
32
26
  end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # Documentation can be found on https://shrinerb.com/docs/plugins/entity
6
+ module Entity
7
+ def self.load_dependencies(uploader, **)
8
+ uploader.plugin :column
9
+ end
10
+
11
+ module AttachmentMethods
12
+ # Defines instance methods on initialization.
13
+ def initialize(name, **options)
14
+ super
15
+
16
+ define_entity_methods(name)
17
+ end
18
+
19
+ # Defines class methods on inclusion.
20
+ def included(klass)
21
+ super
22
+
23
+ attachment = self
24
+
25
+ klass.send(:define_singleton_method, :"#{@name}_attacher") do |**options|
26
+ attachment.send(:class_attacher, **options)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ # Defines `#<name>`, `#<name>_url`, and `#<name>_attacher` methods.
33
+ def define_entity_methods(name)
34
+ super if defined?(super)
35
+
36
+ attachment = self
37
+
38
+ # Returns the attached file.
39
+ if shrine_class::Attacher.instance_method(:get).arity == 0
40
+ define_method :"#{name}" do
41
+ send(:"#{name}_attacher").get
42
+ end
43
+ else # derivatives
44
+ define_method :"#{name}" do |*args|
45
+ send(:"#{name}_attacher").get(*args)
46
+ end
47
+ end
48
+
49
+ # Returns the URL to the attached file.
50
+ define_method :"#{name}_url" do |*args, **options|
51
+ send(:"#{name}_attacher").url(*args, **options)
52
+ end
53
+
54
+ # Returns an attacher instance.
55
+ define_method :"#{name}_attacher" do |**options|
56
+ attachment.send(:attacher, self, **options)
57
+ end
58
+ end
59
+
60
+ # Returns the class attacher instance with loaded entity. It's not
61
+ # memoized because the entity object could be frozen.
62
+ def attacher(record, **options)
63
+ attacher = class_attacher(**options)
64
+ attacher.load_entity(record, @name)
65
+ attacher
66
+ end
67
+
68
+ # Creates an instance of the corresponding attacher class with set
69
+ # name.
70
+ def class_attacher(**options)
71
+ attacher = shrine_class::Attacher.new(**@options, **options)
72
+ attacher.instance_variable_set(:@name, @name)
73
+ attacher
74
+ end
75
+ end
76
+
77
+ module AttacherClassMethods
78
+ # Initializes itself from an entity instance and attachment name.
79
+ #
80
+ # photo.image_data #=> "{...}" # a file is attached
81
+ #
82
+ # attacher = Attacher.from_entity(photo, :image)
83
+ # attacher.file #=> #<Shrine::UploadedFile>
84
+ def from_entity(record, name, **options)
85
+ attacher = new(**options)
86
+ attacher.load_entity(record, name)
87
+ attacher
88
+ end
89
+ end
90
+
91
+ module AttacherMethods
92
+ attr_reader :record, :name
93
+
94
+ # Saves record and name and initializes attachment from the entity
95
+ # attribute. Called from `Attacher.from_entity`.
96
+ def load_entity(record, name)
97
+ set_entity(record, name)
98
+ read
99
+ end
100
+
101
+ # Sets record and name without loading the attachment from the entity
102
+ # attribute.
103
+ def set_entity(record, name)
104
+ @record = record
105
+ @name = name.to_sym
106
+
107
+ @context.merge!(record: record, name: name)
108
+ end
109
+
110
+ # Overwrites the current attachment with the one from model attribute.
111
+ #
112
+ # photo.image_data #=> nil
113
+ # attacher = Shrine::Attacher.from_entity(photo, :image)
114
+ # photo.image_data = uploaded_file.to_json
115
+ #
116
+ # attacher.file #=> nil
117
+ # attacher.reload
118
+ # attacher.file #=> #<Shrine::UploadedFile>
119
+ def reload
120
+ read
121
+ @previous = nil
122
+ self
123
+ end
124
+
125
+ # Loads attachment from the entity attribute.
126
+ def read
127
+ load_column(read_attribute)
128
+ end
129
+
130
+ # Returns a hash with entity attribute name and column data.
131
+ #
132
+ # attacher.column_values
133
+ # #=> { image_data: '{"id":"...","storage":"...","metadata":{...}}' }
134
+ def column_values
135
+ { attribute => column_data }
136
+ end
137
+
138
+ # Returns the entity attribute name used for reading and writing
139
+ # attachment data.
140
+ #
141
+ # attacher = Shrine::Attacher.from_entity(photo, :image)
142
+ # attacher.attribute #=> :image_data
143
+ def attribute
144
+ :"#{name}_data" if name
145
+ end
146
+
147
+ private
148
+
149
+ # Reads value from the entity attribute.
150
+ def read_attribute
151
+ record.public_send(attribute)
152
+ end
153
+ end
154
+ end
155
+
156
+ register_plugin(:entity, Entity)
157
+ end
158
+ end