shrine 2.18.1 → 2.19.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -1
  3. data/README.md +96 -137
  4. data/doc/advantages.md +4 -4
  5. data/doc/attacher.md +1 -2
  6. data/doc/carrierwave.md +3 -2
  7. data/doc/creating_storages.md +0 -20
  8. data/doc/design.md +1 -1
  9. data/doc/metadata.md +62 -36
  10. data/doc/paperclip.md +7 -6
  11. data/doc/plugins/data_uri.md +50 -4
  12. data/doc/plugins/derivation_endpoint.md +24 -0
  13. data/doc/plugins/determine_mime_type.md +47 -5
  14. data/doc/plugins/infer_extension.md +45 -9
  15. data/doc/plugins/instrumentation.md +170 -0
  16. data/doc/plugins/presign_endpoint.md +1 -1
  17. data/doc/plugins/pretty_location.md +23 -0
  18. data/doc/plugins/remote_url.md +59 -8
  19. data/doc/plugins/signature.md +54 -7
  20. data/doc/plugins/store_dimensions.md +69 -4
  21. data/doc/plugins/upload_endpoint.md +2 -2
  22. data/doc/plugins/validation_helpers.md +71 -29
  23. data/doc/refile.md +1 -1
  24. data/doc/release_notes/2.18.0.md +2 -2
  25. data/doc/release_notes/2.19.0.md +263 -0
  26. data/doc/storage/file_system.md +26 -8
  27. data/doc/testing.md +10 -10
  28. data/lib/shrine.rb +32 -16
  29. data/lib/shrine/attacher.rb +3 -0
  30. data/lib/shrine/attachment.rb +3 -0
  31. data/lib/shrine/plugins/add_metadata.rb +12 -16
  32. data/lib/shrine/plugins/backup.rb +2 -0
  33. data/lib/shrine/plugins/copy.rb +2 -0
  34. data/lib/shrine/plugins/data_uri.rb +56 -28
  35. data/lib/shrine/plugins/derivation_endpoint.rb +61 -27
  36. data/lib/shrine/plugins/determine_mime_type.rb +27 -5
  37. data/lib/shrine/plugins/infer_extension.rb +26 -5
  38. data/lib/shrine/plugins/instrumentation.rb +300 -0
  39. data/lib/shrine/plugins/logging.rb +2 -0
  40. data/lib/shrine/plugins/moving.rb +2 -0
  41. data/lib/shrine/plugins/pretty_location.rb +21 -12
  42. data/lib/shrine/plugins/rack_file.rb +23 -18
  43. data/lib/shrine/plugins/refresh_metadata.rb +4 -4
  44. data/lib/shrine/plugins/remote_url.rb +42 -23
  45. data/lib/shrine/plugins/signature.rb +32 -1
  46. data/lib/shrine/plugins/store_dimensions.rb +54 -9
  47. data/lib/shrine/plugins/validation_helpers.rb +148 -47
  48. data/lib/shrine/storage/file_system.rb +32 -15
  49. data/lib/shrine/storage/linter.rb +0 -13
  50. data/lib/shrine/storage/s3.rb +2 -5
  51. data/lib/shrine/uploaded_file.rb +8 -0
  52. data/lib/shrine/version.rb +2 -2
  53. data/shrine.gemspec +18 -3
  54. metadata +58 -27
@@ -11,7 +11,7 @@ storage.url("image.jpg") #=> "/uploads/image.jpg"
11
11
  ```
12
12
 
13
13
  This storage will upload all files to "public/uploads", and the URLs of the
14
- uploaded files will start with "/uploads/*". This way you can use FileSystem
14
+ uploaded files will start with "/uploads/\*". This way you can use FileSystem
15
15
  for both cache and store, one having the prefix "uploads/cache" and other
16
16
  "uploads/store". If you're uploading files to the `public` directory itself,
17
17
  you need to set `:prefix` to `"/"`:
@@ -29,12 +29,6 @@ storage = Shrine::Storage::FileSystem.new(Dir.tmpdir)
29
29
  storage.url("image.jpg") #=> "/var/folders/k7/6zx6dx6x7ys3rv3srh0nyfj00000gn/T/image.jpg"
30
30
  ```
31
31
 
32
- In general you can always retrieve path to the file using `#path`:
33
-
34
- ```rb
35
- storage.path("image.jpg") #=> #<Pathname:public/image.jpg>
36
- ```
37
-
38
32
  ## Host
39
33
 
40
34
  It's generally a good idea to serve your files via a CDN, so an additional
@@ -58,6 +52,28 @@ storage.url("image.jpg", host: "http://943.23.43.1")
58
52
  #=> "http://943.23.43.1/opt/files/image.jpg"
59
53
  ```
60
54
 
55
+ ## Moving
56
+
57
+ If you're uploading files on disk and want to improve performance, you can tell
58
+ the `FileSystem#upload` method to **move** files instead of copying them:
59
+
60
+ ```rb
61
+ storage.upload(file, "/path/to/destination", move: true) # performs the `mv` command
62
+
63
+ File.exist?(file.path) #=> false
64
+ ```
65
+
66
+ If you want to make this option default, you can use the
67
+ [`upload_options`][upload_options] plugin.
68
+
69
+ ## Path
70
+
71
+ You can retrieve path to the file using `#path`:
72
+
73
+ ```rb
74
+ storage.path("image.jpg") #=> #<Pathname:public/image.jpg>
75
+ ```
76
+
61
77
  ## Clearing cache
62
78
 
63
79
  If you're using FileSystem as cache, you will probably want to periodically
@@ -66,7 +82,7 @@ periodically:
66
82
 
67
83
  ```rb
68
84
  file_system = Shrine.storages[:cache]
69
- file_system.clear!(older_than: Time.now - 7*24*60*60) # delete files older than 1 week
85
+ file_system.clear! { |path| path.mtime < Time.now - 7*24*60*60 } # delete files older than 1 week
70
86
  ```
71
87
 
72
88
  ## Permissions
@@ -94,3 +110,5 @@ use it for cache, since Heroku wipes this directory between app restarts. This
94
110
  also means that deploying the app can cancel someone's uploading if you're
95
111
  using backgrounding. Also, by default you cannot generate URLs to files in the
96
112
  "tmp" directory, but you can with the `download_endpoint` plugin.
113
+
114
+ [upload_options]: /doc/plugins/upload_options.md#readme
data/doc/testing.md CHANGED
@@ -54,14 +54,14 @@ Shrine.storages = {
54
54
  }
55
55
  ```
56
56
 
57
- If you're using AWS S3 storage, you can use [Minio] (explained below) instead
57
+ If you're using AWS S3 storage, you can use [MinIO] (explained below) instead
58
58
  of S3, both in test and development environment. Alternatively, you can [stub
59
59
  aws-sdk-s3 requests][aws-sdk-ruby stubs] in tests.
60
60
 
61
- ### Minio
61
+ ### MinIO
62
62
 
63
- [Minio] is an open source object storage server with AWS S3 compatible API which
64
- you can run locally. The advantage of using Minio for your development and test
63
+ [MinIO] is an open source object storage server with AWS S3 compatible API which
64
+ you can run locally. The advantage of using MinIO for your development and test
65
65
  environments is that all AWS S3 functionality should still continue to work,
66
66
  including direct uploads, so you don't need to update your code.
67
67
 
@@ -71,17 +71,17 @@ If you're on a Mac you can install it with Homebrew:
71
71
  $ brew install minio/stable/minio
72
72
  ```
73
73
 
74
- Afterwards you can start the Minio server and give it a directory where it will
74
+ Afterwards you can start the MinIO server and give it a directory where it will
75
75
  store the data:
76
76
 
77
77
  ```
78
78
  $ minio server data/
79
79
  ```
80
80
 
81
- This command will print out the credentials for the running Minio server, as
82
- well as a link to the Minio web interface. Follow that link and create a new
81
+ This command will print out the credentials for the running MinIO server, as
82
+ well as a link to the MinIO web interface. Follow that link and create a new
83
83
  bucket. Once you've done that, you can configure `Shrine::Storage::S3` to use
84
- your Minio server:
84
+ your MinIO server:
85
85
 
86
86
  ```rb
87
87
  Shrine::Storage::S3.new(
@@ -94,7 +94,7 @@ Shrine::Storage::S3.new(
94
94
  )
95
95
  ```
96
96
 
97
- The `:endpoint` option will make aws-sdk-s3 point all URLs to your Minio server
97
+ The `:endpoint` option will make aws-sdk-s3 point all URLs to your MinIO server
98
98
  (instead of `s3.amazonaws.com`), and `:force_path_style` tells it not to use
99
99
  subdomains when generating URLs.
100
100
 
@@ -285,4 +285,4 @@ isolation.
285
285
  [Rack::Test]: https://github.com/brynary/rack-test
286
286
  [Rack::TestApp]: https://github.com/kwatch/rack-test_app
287
287
  [aws-sdk-ruby stubs]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ClientStubs.html
288
- [Minio]: https://minio.io
288
+ [MinIO]: https://min.io/
data/lib/shrine.rb CHANGED
@@ -9,6 +9,7 @@ require "shrine/plugins"
9
9
  require "securerandom"
10
10
  require "json"
11
11
  require "tempfile"
12
+ require "logger"
12
13
 
13
14
  # Core class that represents uploader.
14
15
  # Base implementation is defined in InstanceMethods and ClassMethods.
@@ -37,6 +38,8 @@ class Shrine
37
38
 
38
39
  @opts = {}
39
40
  @storages = {}
41
+ @logger = Logger.new(STDOUT)
42
+ @logger.formatter = -> (*, message) { "#{message}\n" }
40
43
 
41
44
  module ClassMethods
42
45
  # Generic options for this class, plugins store their options here.
@@ -45,6 +48,9 @@ class Shrine
45
48
  # A hash of storages with their symbol identifiers.
46
49
  attr_accessor :storages
47
50
 
51
+ # A logger instance.
52
+ attr_accessor :logger
53
+
48
54
  # When inheriting Shrine, copy the instance variables into the subclass,
49
55
  # and create subclasses of core classes.
50
56
  def inherited(subclass)
@@ -100,7 +106,7 @@ class Shrine
100
106
  # model class. Example:
101
107
  #
102
108
  # class Photo
103
- # include Shrine.attachment(:image) # creates a Shrine::Attachment object
109
+ # include Shrine::Attachment(:image) # creates a Shrine::Attachment object
104
110
  # end
105
111
  def Attachment(name, *args)
106
112
  self::Attachment.new(name, *args)
@@ -154,9 +160,14 @@ class Shrine
154
160
  end
155
161
  end
156
162
 
157
- # Prints a deprecation warning to standard error.
163
+ # Prints a warning to the logger.
164
+ def warn(message)
165
+ Shrine.logger.warn "SHRINE WARNING: #{message}"
166
+ end
167
+
168
+ # Prints a deprecation warning to the logger.
158
169
  def deprecation(message)
159
- warn "SHRINE DEPRECATION WARNING: #{message}"
170
+ Shrine.logger.warn "SHRINE DEPRECATION WARNING: #{message}"
160
171
  end
161
172
  end
162
173
 
@@ -233,11 +244,7 @@ class Shrine
233
244
  # file extension. Can be overriden in uploaders for generating custom
234
245
  # location.
235
246
  def generate_location(io, context = {})
236
- extension = ".#{io.extension}" if io.is_a?(UploadedFile) && io.extension
237
- extension ||= File.extname(extract_filename(io).to_s).downcase
238
- basename = generate_uid(io)
239
-
240
- basename + extension
247
+ basic_location(io)
241
248
  end
242
249
 
243
250
  # Extracts filename, size and MIME type from the file, which is later
@@ -264,7 +271,7 @@ class Shrine
264
271
  # Attempts to extract the MIME type from the IO object.
265
272
  def extract_mime_type(io)
266
273
  if io.respond_to?(:content_type) && io.content_type
267
- warn "The \"mime_type\" Shrine metadata field will be set from the \"Content-Type\" request header, which might not hold the actual MIME type of the file. It is recommended to load the determine_mime_type plugin which determines MIME type from file content."
274
+ Shrine.warn "The \"mime_type\" Shrine metadata field will be set from the \"Content-Type\" request header, which might not hold the actual MIME type of the file. It is recommended to load the determine_mime_type plugin which determines MIME type from file content."
268
275
  io.content_type.split(";").first # exclude media type parameters
269
276
  end
270
277
  end
@@ -283,7 +290,7 @@ class Shrine
283
290
  _enforce_io(io)
284
291
 
285
292
  metadata = get_metadata(io, context)
286
- metadata = metadata.merge(context[:metadata]) if context[:metadata]
293
+ metadata = metadata.merge(context[:metadata]) if context[:metadata].is_a?(Hash)
287
294
 
288
295
  location = get_location(io, context.merge(metadata: metadata))
289
296
 
@@ -329,6 +336,15 @@ class Shrine
329
336
  process(io, context)
330
337
  end
331
338
 
339
+ # Generates a basic location for an uploaded file
340
+ def basic_location(io)
341
+ extension = ".#{io.extension}" if io.is_a?(UploadedFile) && io.extension
342
+ extension ||= File.extname(extract_filename(io).to_s).downcase
343
+ basename = generate_uid(io)
344
+
345
+ basename + extension
346
+ end
347
+
332
348
  # Retrieves the location for the given IO and context. First it looks
333
349
  # for the `:location` option, otherwise it calls #generate_location.
334
350
  def get_location(io, context)
@@ -339,10 +355,12 @@ class Shrine
339
355
  # If the IO object is a Shrine::UploadedFile, it simply copies over its
340
356
  # metadata, otherwise it calls #extract_metadata.
341
357
  def get_metadata(io, context)
342
- if io.is_a?(UploadedFile)
358
+ if io.is_a?(UploadedFile) && context[:metadata] != true
343
359
  io.metadata.dup
344
- else
360
+ elsif context[:metadata] != false
345
361
  extract_metadata(io, context)
362
+ else
363
+ {}
346
364
  end
347
365
  end
348
366
 
@@ -360,9 +378,7 @@ class Shrine
360
378
  SecureRandom.hex
361
379
  end
362
380
  end
363
- end
364
381
 
365
- [Shrine, Shrine::UploadedFile, Shrine::Attacher, Shrine::Attachment].each do |core_class|
366
- core_class.include core_class.const_get(:InstanceMethods)
367
- core_class.extend core_class.const_get(:ClassMethods)
382
+ extend ClassMethods
383
+ include InstanceMethods
368
384
  end
@@ -267,5 +267,8 @@ class Shrine
267
267
  options
268
268
  end
269
269
  end
270
+
271
+ extend ClassMethods
272
+ include InstanceMethods
270
273
  end
271
274
  end
@@ -93,5 +93,8 @@ class Shrine
93
93
  self.class.shrine_class
94
94
  end
95
95
  end
96
+
97
+ extend ClassMethods
98
+ include InstanceMethods
96
99
  end
97
100
  end
@@ -7,12 +7,12 @@ class Shrine
7
7
  # [doc/plugins/add_metadata.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/add_metadata.md
8
8
  module AddMetadata
9
9
  def self.configure(uploader)
10
- uploader.opts[:metadata] ||= []
10
+ uploader.opts[:add_metadata_definitions] ||= []
11
11
  end
12
12
 
13
13
  module ClassMethods
14
- def add_metadata(name = nil, **options, &block)
15
- opts[:metadata] << [name, options, block]
14
+ def add_metadata(name = nil, &block)
15
+ opts[:add_metadata_definitions] << [name, block]
16
16
 
17
17
  metadata_method(name) if name
18
18
  end
@@ -24,7 +24,7 @@ class Shrine
24
24
  private
25
25
 
26
26
  def _metadata_method(name)
27
- self::UploadedFile.send(:define_method, name) do
27
+ FileMethods.send(:define_method, name) do
28
28
  metadata[name.to_s]
29
29
  end
30
30
  end
@@ -43,28 +43,24 @@ class Shrine
43
43
  private
44
44
 
45
45
  def extract_custom_metadata(io, context)
46
- opts[:metadata].each do |name, options, block|
47
- result = instance_exec(io, context, &block)
48
- metadata = {}
46
+ opts[:add_metadata_definitions].each do |name, block|
47
+ result = instance_exec(io, context, &block)
49
48
 
50
49
  if name
51
- metadata[name.to_s] = result
50
+ context[:metadata].merge! name.to_s => result
52
51
  else
53
- metadata.merge!(result) if result
52
+ context[:metadata].merge! result.transform_keys(&:to_s) if result
54
53
  end
55
54
 
56
- # convert symbol keys to strings
57
- metadata.keys.each do |key|
58
- metadata[key.to_s] = metadata.delete(key) if key.is_a?(Symbol)
59
- end
60
-
61
- context[:metadata].merge!(metadata)
62
-
63
55
  # rewind between metadata blocks
64
56
  io.rewind
65
57
  end
66
58
  end
67
59
  end
60
+
61
+ module FileMethods
62
+ # methods will be dynamically defined here through `Shrine.add_metadata`
63
+ end
68
64
  end
69
65
 
70
66
  register_plugin(:add_metadata, AddMetadata)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ Shrine.deprecation("The backup plugin has been deprecated, the new preferred way to implement mirroring is via the instrumentation plugin – see https://github.com/shrinerb/shrine/wiki/Mirroring-Uploads. The backup plugin will be removed in Shrine 3.")
4
+
3
5
  class Shrine
4
6
  module Plugins
5
7
  # Documentation lives in [doc/plugins/backup.md] on GitHub.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ Shrine.deprecation("The copy plugin is deprecated and will be removed in Shrine 3.")
4
+
3
5
  class Shrine
4
6
  module Plugins
5
7
  # Documentation lives in [doc/plugins/copy.md] on GitHub.
@@ -20,11 +20,24 @@ class Shrine
20
20
  CONTENT_SEPARATOR = /,/
21
21
  DEFAULT_CONTENT_TYPE = "text/plain"
22
22
 
23
+ LOG_SUBSCRIBER = -> (event) do
24
+ Shrine.logger.info "Data URI (#{event.duration}ms) – #{{
25
+ uploader: event[:uploader],
26
+ }.inspect}"
27
+ end
28
+
23
29
  def self.configure(uploader, opts = {})
24
- uploader.opts[:data_uri_filename] = opts.fetch(:filename, uploader.opts[:data_uri_filename])
25
- uploader.opts[:data_uri_error_message] = opts.fetch(:error_message, uploader.opts[:data_uri_error_message])
30
+ uploader.opts[:data_uri] ||= { log_subscriber: LOG_SUBSCRIBER }
31
+ uploader.opts[:data_uri].merge!(opts)
32
+
33
+ if uploader.opts[:data_uri][:filename]
34
+ Shrine.deprecation("The :filename option is deprecated for the data_uri plugin, and will be removed in Shrine 3. Use the infer_extension plugin instead.")
35
+ end
26
36
 
27
- Shrine.deprecation("The :filename option is deprecated for the data_uri plugin, and will be removed in Shrine 3. Use the infer_extension plugin instead.") if opts[:filename]
37
+ # instrumentation plugin integration
38
+ if uploader.respond_to?(:subscribe)
39
+ uploader.subscribe(:data_uri, &uploader.opts[:data_uri][:log_subscriber])
40
+ end
28
41
  end
29
42
 
30
43
  module ClassMethods
@@ -36,20 +49,25 @@ class Shrine
36
49
  # io.size #=> 21
37
50
  # io.read # decoded content
38
51
  def data_uri(uri, filename: nil)
39
- info = parse_data_uri(uri)
52
+ instrument_data_uri(uri) do
53
+ info = parse_data_uri(uri)
54
+ create_data_file(info, filename: filename)
55
+ end
56
+ end
40
57
 
58
+ private
59
+
60
+ def create_data_file(info, filename: nil)
41
61
  content_type = info[:content_type] || DEFAULT_CONTENT_TYPE
42
62
  content = info[:base64] ? Base64.decode64(info[:data]) : CGI.unescape(info[:data])
43
- filename = opts[:data_uri_filename].call(content_type) if opts[:data_uri_filename]
63
+ filename = opts[:data_uri][:filename].call(content_type) if opts[:data_uri][:filename]
44
64
 
45
- data_file = DataFile.new(content, content_type: content_type, filename: filename)
65
+ data_file = Shrine::DataFile.new(content, content_type: content_type, filename: filename)
46
66
  info[:data].clear
47
67
 
48
68
  data_file
49
69
  end
50
70
 
51
- private
52
-
53
71
  def parse_data_uri(uri)
54
72
  scanner = StringScanner.new(uri)
55
73
  scanner.scan(DATA_REGEXP) or raise ParseError, "data URI has invalid format"
@@ -60,6 +78,13 @@ class Shrine
60
78
 
61
79
  { content_type: media_type, base64: !!base64, data: content }
62
80
  end
81
+
82
+ # Sends a `data_uri.shrine` event for instrumentation plugin.
83
+ def instrument_data_uri(uri, &block)
84
+ return yield unless respond_to?(:instrument)
85
+
86
+ instrument(:data_uri, data_uri: uri, &block)
87
+ end
63
88
  end
64
89
 
65
90
  module AttachmentMethods
@@ -87,7 +112,7 @@ class Shrine
87
112
  data_file = shrine_class.data_uri(uri)
88
113
  assign(data_file, **options)
89
114
  rescue ParseError => error
90
- message = shrine_class.opts[:data_uri_error_message] || error.message
115
+ message = shrine_class.opts[:data_uri][:error_message] || error.message
91
116
  message = message.call(uri) if message.respond_to?(:call)
92
117
  errors.replace [message]
93
118
  @data_uri = uri
@@ -118,30 +143,33 @@ class Shrine
118
143
  result
119
144
  end
120
145
  end
146
+ end
121
147
 
122
- class DataFile
123
- attr_reader :content_type, :original_filename
124
-
125
- def initialize(content, content_type: nil, filename: nil)
126
- @content_type = content_type
127
- @original_filename = filename
128
- @io = StringIO.new(content)
129
- end
148
+ register_plugin(:data_uri, DataUri)
149
+ end
130
150
 
131
- def to_io
132
- @io
133
- end
151
+ class DataFile
152
+ attr_reader :content_type, :original_filename
134
153
 
135
- extend Forwardable
136
- delegate [:read, :size, :rewind, :eof?] => :@io
154
+ def initialize(content, content_type: nil, filename: nil)
155
+ @content_type = content_type
156
+ @original_filename = filename
157
+ @io = StringIO.new(content)
158
+ end
137
159
 
138
- def close
139
- @io.close
140
- @io.string.clear # deallocate string
141
- end
142
- end
160
+ def to_io
161
+ @io
143
162
  end
144
163
 
145
- register_plugin(:data_uri, DataUri)
164
+ extend Forwardable
165
+ delegate [:read, :size, :rewind, :eof?] => :@io
166
+
167
+ def close
168
+ @io.close
169
+ @io.string.clear # deallocate string
170
+ end
146
171
  end
172
+
173
+ Plugins::DataUri.const_set(:DataFile, DataFile)
174
+ Plugins::DataUri.deprecate_constant(:DataFile)
147
175
  end