resizing 1.2.1 → 1.3.1

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +73 -0
  3. data/.devcontainer/compose.yaml +58 -0
  4. data/.devcontainer/devcontainer-lock.json +9 -0
  5. data/.devcontainer/devcontainer.json +32 -0
  6. data/.devcontainer/post-create.sh +41 -0
  7. data/.github/copilot-instructions.md +1 -0
  8. data/.github/labeler.yml +28 -0
  9. data/.github/release.yml +35 -0
  10. data/.github/workflows/labeler.yml +20 -0
  11. data/.github/workflows/lint.yml +37 -0
  12. data/.github/workflows/test.yml +16 -9
  13. data/.gitignore +6 -0
  14. data/AGENTS.md +1 -0
  15. data/CHANGELOG.md +3 -0
  16. data/CLAUDE.md +68 -0
  17. data/Gemfile +9 -3
  18. data/README.md +114 -2
  19. data/Rakefile +5 -7
  20. data/bin/setup +3 -0
  21. data/git-hooks/pre-push +6 -0
  22. data/git-hooks/pre-push-ruby.sh +48 -0
  23. data/lib/resizing/carrier_wave/callback_file.rb +88 -0
  24. data/lib/resizing/carrier_wave/storage/file.rb +120 -47
  25. data/lib/resizing/carrier_wave/storage/remote.rb +7 -3
  26. data/lib/resizing/carrier_wave.rb +51 -7
  27. data/lib/resizing/client.rb +0 -1
  28. data/lib/resizing/configurable.rb +1 -1
  29. data/lib/resizing/configuration.rb +40 -10
  30. data/lib/resizing/mock_client.rb +23 -7
  31. data/lib/resizing/public_id.rb +3 -2
  32. data/lib/resizing/version.rb +1 -1
  33. data/lib/resizing.rb +0 -1
  34. data/resizing.gemspec +3 -3
  35. data/test/resizing/carrier_wave/storage/file_test.rb +455 -9
  36. data/test/resizing/carrier_wave/storage/remote_test.rb +91 -9
  37. data/test/resizing/carrier_wave_cache_callbacks_test.rb +224 -0
  38. data/test/resizing/carrier_wave_callback_file_test.rb +77 -0
  39. data/test/resizing/carrier_wave_test.rb +477 -23
  40. data/test/resizing/carrier_wave_uploader_test.rb +211 -0
  41. data/test/resizing/client_test.rb +324 -25
  42. data/test/resizing/configuration_test.rb +235 -10
  43. data/test/resizing/http_clientable_test.rb +27 -0
  44. data/test/resizing/mock_client_test.rb +111 -1
  45. data/test/resizing/public_id_test.rb +96 -0
  46. data/test/resizing_module_test.rb +110 -0
  47. data/test/resizing_test.rb +8 -0
  48. data/test/test_helper.rb +358 -11
  49. data/test/vcr/carrier_wave_test/update_image.yml +63 -0
  50. metadata +35 -10
data/Rakefile CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'bundler/gem_tasks'
4
4
  require 'rake/testtask'
5
- require 'github_changelog_generator/task'
5
+ require 'rubocop/rake_task'
6
6
 
7
7
  Rake::TestTask.new(:test) do |t|
8
8
  t.libs << 'test'
@@ -10,11 +10,9 @@ Rake::TestTask.new(:test) do |t|
10
10
  t.test_files = FileList['test/**/*_test.rb']
11
11
  end
12
12
 
13
- GitHubChangelogGenerator::RakeTask.new :changelog do |config|
14
- config.user = 'jksy'
15
- config.project = 'resizing-gem'
16
- config.since_tag = 'v0.1.0'
17
- config.future_release = Resizing::VERSION
18
- end
13
+ RuboCop::RakeTask.new
14
+
15
+ desc 'Run the same checks as CI (RuboCop and the tests)'
16
+ task ci: %i[rubocop test]
19
17
 
20
18
  task default: :test
data/bin/setup CHANGED
@@ -5,4 +5,7 @@ set -vx
5
5
 
6
6
  bundle install
7
7
 
8
+ # Run the CI checks before every push (see git-hooks/pre-push)
9
+ git config core.hooksPath git-hooks
10
+
8
11
  # Do any other automated setup that you need to do here
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+ # Entry point for the pre-push hook. Enabled by bin/setup, which points
3
+ # core.hooksPath at this directory.
4
+ HOOKS_DIR="$(git rev-parse --show-toplevel)/git-hooks"
5
+
6
+ "$HOOKS_DIR/pre-push-ruby.sh" || exit 1
@@ -0,0 +1,48 @@
1
+ #!/bin/sh
2
+ # pre-push-ruby.sh
3
+ # Runs the same checks as CI (RuboCop and the test suite) before pushing.
4
+ # Called from git-hooks/pre-push. Aborts the push if any check fails.
5
+ #
6
+ # The checks run wherever the gems are installed: the current shell when it
7
+ # already has them (inside the devcontainer, or a host with `bundle install`
8
+ # done), otherwise the devcontainer via its CLI or docker compose.
9
+ #
10
+ # PRE_PUSH_SKIP_TESTS=1 git push # lint only, skip the test suite
11
+ # git push --no-verify # skip every check (not recommended)
12
+ ROOT="$(git rev-parse --show-toplevel)"
13
+ cd "$ROOT" || exit 1
14
+
15
+ if bundle check >/dev/null 2>&1; then
16
+ MODE=local
17
+ elif command -v devcontainer >/dev/null 2>&1; then
18
+ MODE=devcontainer
19
+ elif docker compose -f .devcontainer/compose.yaml ps --status running --quiet app 2>/dev/null | grep -q .; then
20
+ MODE=compose
21
+ else
22
+ echo "No environment with the gems installed was found."
23
+ echo "Run 'devcontainer up --workspace-folder .' (or 'bundle install') first."
24
+ echo "To skip this check (not recommended): git push --no-verify"
25
+ exit 1
26
+ fi
27
+
28
+ run() {
29
+ case "$MODE" in
30
+ local) bundle exec "$@" ;;
31
+ devcontainer) devcontainer exec --workspace-folder "$ROOT" bundle exec "$@" ;;
32
+ compose) docker compose -f .devcontainer/compose.yaml exec -T app bundle exec "$@" ;;
33
+ esac
34
+ }
35
+
36
+ if [ -n "${PRE_PUSH_SKIP_TESTS:-}" ]; then
37
+ echo "Running RuboCop ($MODE)..."
38
+ run rake rubocop
39
+ else
40
+ echo "Running RuboCop and tests ($MODE)..."
41
+ run rake ci
42
+ fi
43
+
44
+ if [ $? -ne 0 ]; then
45
+ echo "Pre-push checks failed. Push aborted."
46
+ echo "To skip this check (not recommended): git push --no-verify"
47
+ exit 1
48
+ fi
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resizing
4
+ module CarrierWave
5
+ # The file handed to CarrierWave's :cache callbacks (extension_allowlist /
6
+ # extension_denylist / content_type_allowlist / content_type_denylist / size_range).
7
+ #
8
+ # The extension used by the callbacks is derived from the content type whenever the
9
+ # content type is known, instead of being taken from the file name as plain
10
+ # CarrierWave does.
11
+ #
12
+ # A file downloaded from a Resizing URL (e.g. when it is assigned through
13
+ # `remote_<column>_url=`) is the typical case: the last segment of the URL is the
14
+ # version string (`.../images/<image_id>/v<version>`), which contains dots, so the
15
+ # "extension" CarrierWave finds in the file name is a meaningless part of the version
16
+ # (`Fv_Q` for `/v0cRq7Yb2mZt9LxA3wN8eKd5pHs1.Fv_Q`), and extension_allowlist would
17
+ # reject every such file. The HTTP response carries the real content type, so it is
18
+ # the reliable source.
19
+ #
20
+ # When the content type is unknown (`application/octet-stream`, or a type that no
21
+ # extension is registered for), the file name is used as is, as in plain CarrierWave.
22
+ class CallbackFile < ::CarrierWave::SanitizedFile
23
+ # Content type given to files whose real type could not be determined.
24
+ # Carries no information about the extension, so it is treated as unknown.
25
+ UNKNOWN_CONTENT_TYPE = 'application/octet-stream'
26
+
27
+ def original_filename
28
+ name = super
29
+ return name if name.nil?
30
+
31
+ extension = extension_for_content_type
32
+ return name if extension.nil?
33
+
34
+ basename, current_extension = split_extension(name)
35
+ return name if extensions_for_content_type.include?(current_extension.downcase)
36
+
37
+ "#{basename}.#{extension}"
38
+ end
39
+
40
+ private
41
+
42
+ # The preferred extension of the content type, or nil when the content type is unknown.
43
+ def extension_for_content_type
44
+ extensions_for_content_type.first
45
+ end
46
+
47
+ # Every extension registered for the content type (lower case), or [] when unknown.
48
+ # Neither library raises on an unregistered or malformed content type: MIME::Types
49
+ # returns an empty list and MiniMime returns nil.
50
+ #
51
+ # MIME::Types (a dependency of CarrierWave 1.x and of fog) is preferred because it
52
+ # knows every alias of an extension (jpeg / jpg / jpe for image/jpeg); MiniMime
53
+ # (a dependency of CarrierWave 2.x+) knows only one extension per content type.
54
+ def extensions_for_content_type
55
+ @extensions_for_content_type ||= begin
56
+ # Drop the parameters (`image/jpeg; charset=binary`), which MiniMime cannot handle
57
+ type = content_type.to_s.split(';').first.to_s.strip.downcase
58
+ if type.empty? || type == UNKNOWN_CONTENT_TYPE
59
+ []
60
+ else
61
+ lookup_extensions(type)
62
+ end
63
+ end
64
+ end
65
+
66
+ def lookup_extensions(type)
67
+ if mime_types_available?
68
+ ::MIME::Types[type].flat_map(&:extensions).map(&:downcase).uniq
69
+ elsif mini_mime_available?
70
+ extension = ::MiniMime.lookup_by_content_type(type)&.extension
71
+ extension.nil? ? [] : [extension.downcase]
72
+ else
73
+ []
74
+ end
75
+ end
76
+
77
+ # Which lookup library is loaded depends on the application (CarrierWave 1.x loads
78
+ # MIME::Types, 2.x+ loads MiniMime); the tests stub these to exercise each path.
79
+ def mime_types_available?
80
+ !defined?(::MIME::Types).nil?
81
+ end
82
+
83
+ def mini_mime_available?
84
+ !defined?(::MiniMime).nil?
85
+ end
86
+ end
87
+ end
88
+ end
@@ -7,29 +7,40 @@ module Resizing
7
7
  class File
8
8
  include ::CarrierWave::Utilities::Uri
9
9
 
10
+ # Fallback content type used when the type cannot be determined from the file name.
11
+ DEFAULT_CONTENT_TYPE = 'application/octet-stream'
12
+
10
13
  attr_reader :public_id
11
14
 
12
15
  def initialize(uploader, identifier = nil)
13
16
  @uploader = uploader
14
17
  @content_type = nil
15
18
  @public_id = Resizing::PublicId.new(identifier)
16
- @file = nil
17
19
  @metadata = nil
20
+ @metadata_fetched = false
18
21
  end
19
22
 
23
+ # Metadata of the stored image, as returned by the Resizing metadata API.
24
+ #
25
+ # === Returns
26
+ #
27
+ # [Hash, nil] metadata hash, or nil when the image is unknown/unreachable
20
28
  def attributes
21
- file.attributes
29
+ metadata
22
30
  end
23
31
 
24
32
  def authenticated_url(_options = {})
25
33
  nil
26
34
  end
27
35
 
36
+ # NOTE: Called through CarrierWave's Uploader::Proxy#content_type, so it must not
37
+ # raise. Falls back to the metadata API when the content type is not known locally
38
+ # (e.g. the model was reloaded from the DB), and returns nil if it cannot be fetched.
28
39
  def content_type
29
- @content_type || file.try(:content_type)
40
+ @content_type ||= metadata && metadata['content_type']
30
41
  end
31
42
 
32
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
43
+ # rubocop:disable Metrics/AbcSize
33
44
  def delete
34
45
  # Use the identifier from constructor if available, otherwise try to get from model
35
46
  if @public_id.present?
@@ -44,23 +55,27 @@ module Resizing
44
55
  @public_id = Resizing::PublicId.new(column_value)
45
56
  end
46
57
 
47
- return if @public_id.empty? # do nothing
58
+ return if @public_id.empty?
48
59
 
49
- # 以下、既存のコード(変更なし)
50
60
  resp = client.delete(@public_id.image_id)
61
+
62
+ # NOTE: 削除時のカラムクリアは以下の理由で必要:
63
+ # - 画像更新時: 古い画像IDと新しい画像IDが異なるため、古い画像削除時に新しいIDを消さないようにする
64
+ # - 明示的なremove!時: カラムをnilにする必要がある
65
+ # - clear_column_if_current_imageは削除される画像IDと現在のカラム値を比較して判断
51
66
  if resp['error'] == 'ActiveRecord::RecordNotFound' # 404 not found
52
- model.send :write_attribute, serialization_column, nil unless model.destroyed?
67
+ clear_column_if_current_image
53
68
  return
54
69
  end
55
70
 
56
71
  if @public_id.image_id == resp['id']
57
- model.send :write_attribute, serialization_column, nil unless model.destroyed?
72
+ clear_column_if_current_image
58
73
  return
59
74
  end
60
75
 
61
76
  raise APIError, "raise someone error:#{resp.inspect}"
62
77
  end
63
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
78
+ # rubocop:enable Metrics/AbcSize
64
79
 
65
80
  def extension
66
81
  raise NotImplementedError, 'this method is do not used. maybe'
@@ -69,32 +84,32 @@ module Resizing
69
84
  ##
70
85
  # Read content of file from service
71
86
  #
72
- # === Returns
73
- #
74
- # [String] contents of file
87
+ # NOTE: Resizing keeps no local copy of the stored image and this gem has no API to
88
+ # download the binary (Resizing::Client#get is not implemented either), so reading the
89
+ # content would require a new download API. It is not called by the regular CarrierWave
90
+ # flow (only through Uploader::Proxy#read, i.e. explicitly by the application), so it
91
+ # raises instead of silently returning nil.
75
92
  def read
76
- file_body = file.body
77
-
78
- return if file_body.nil?
79
- return file_body unless file_body.is_a?(::File)
80
-
81
- # Fog::Storage::XXX::File#body could return the source file which was upoloaded to the remote server.
82
- read_source_file(file_body) if ::File.exist?(file_body.path)
83
-
84
- # If the source file doesn't exist, the remote content is read
85
- @file = nil
86
- file.body
93
+ raise NotImplementedError, 'Resizing does not support reading the stored image content'
87
94
  end
88
95
 
96
+ # NOTE: Called through CarrierWave's Uploader::Proxy#size, so it must not raise.
97
+ # The size is only known to the Resizing service, so it comes from the metadata API.
98
+ # Returns 0 when it cannot be fetched, following Uploader::Proxy#size, which itself
99
+ # falls back to 0, so that the return value is always an Integer.
89
100
  def size
90
- file.nil? ? 0 : file.content_length
101
+ (metadata && metadata['size']) || 0
91
102
  end
92
103
 
93
104
  def exists?
94
- !!file
105
+ !metadata.nil?
95
106
  end
96
107
 
97
108
  def current_path
109
+ # Return the path from @public_id if set (for retrieve scenarios),
110
+ # otherwise fall back to reading from model
111
+ return @public_id.to_s if @public_id.present?
112
+
98
113
  @current_path = model.send :read_attribute, serialization_column
99
114
  end
100
115
  alias path current_path
@@ -110,8 +125,10 @@ module Resizing
110
125
  @content_type ||= if new_file.respond_to? :content_type
111
126
  new_file.content_type
112
127
  else
113
- # guess content-type from extension
114
- MIME::Types.type_for(new_file.path).first.content_type
128
+ # Guess content-type from extension. MIME::Types returns an empty
129
+ # list for extensions it does not know (e.g. `.url`), so fall back
130
+ # to the generic binary type instead of raising NoMethodError.
131
+ guess_content_type(new_file.path)
115
132
  end
116
133
 
117
134
  original_filename = new_file.try(:original_filename) || new_file.try(:filename) || new_file.try(:path)
@@ -131,11 +148,14 @@ module Resizing
131
148
  @public_id = Resizing::PublicId.new(@response['public_id'])
132
149
  @content_type = @response['content_type']
133
150
 
134
- # force update column
135
- # model_class
136
- # .where(primary_key_name => model.send(primary_key_name))
137
- # .update_all(serialization_column=>@public_id)
138
-
151
+ # NOTE: 理想的にはStorage::File内でモデルのカラムをいじらず、CarrierWaveに任せるべきだが、
152
+ # 現在の実装では以下の理由で必要:
153
+ # - CarrierWaveは write_uploader(column, mounter.identifiers.first) でカラムを更新
154
+ # - mounter.identifiers -> uploaders.map(&:identifier) -> storage.identifier -> uploader.filename
155
+ # - resizing-gemの filenameメソッドは read_column を返す(既存のカラム値)
156
+ # - そのため、CarrierWaveに任せると旧い値が書き戻されてしまう
157
+ # TODO: これを修正するには、Remote#identifierをオーバーライドして@public_id.to_sを返すか、
158
+ # uploader.filenameの実装を変更する必要がある
139
159
  # save new value to model class
140
160
  model.send :write_attribute, serialization_column, @public_id.to_s
141
161
 
@@ -153,8 +173,12 @@ module Resizing
153
173
  # CarrierWave::Storage::Fog::File.new(@uploader, @base, new_path)
154
174
  # end
155
175
 
176
+ # NOTE: Resizing::CarrierWave#file calls this on every access, so the cached metadata
177
+ # is only dropped when the identifier actually changes.
156
178
  def retrieve(identifier)
157
- @public_id = Resizing::PublicId.new(identifier)
179
+ new_public_id = Resizing::PublicId.new(identifier)
180
+ reset_metadata unless new_public_id.to_s == @public_id.to_s
181
+ @public_id = new_public_id
158
182
  end
159
183
 
160
184
  private
@@ -177,6 +201,20 @@ module Resizing
177
201
  @serialization_column ||= model.send(:_mounter, uploader.mounted_as).send(:serialization_column)
178
202
  end
179
203
 
204
+ # Only clear the column if the deleted image is the current one
205
+ # (not when deleting an old image during update)
206
+ def clear_column_if_current_image
207
+ return if model.destroyed?
208
+
209
+ current_value = model.send(:read_attribute, serialization_column)
210
+ current_public_id = Resizing::PublicId.new(current_value)
211
+
212
+ # Only clear if the deleted image is the same as the current one
213
+ return unless current_public_id.image_id == @public_id.image_id
214
+
215
+ model.send :write_attribute, serialization_column, nil
216
+ end
217
+
180
218
  ##
181
219
  # client of Resizing
182
220
  def client
@@ -188,25 +226,60 @@ module Resizing
188
226
  end
189
227
 
190
228
  ##
191
- # lookup file
229
+ # Metadata of the stored image, fetched lazily from the Resizing metadata API
230
+ # and memoized (including a failed lookup, so it is fetched at most once).
192
231
  #
193
- # === Returns
232
+ # This replaces the Fog-derived `file` method (`directory.files.head(path)`) that this
233
+ # class used to rely on: Resizing has no equivalent of a remote file handle, and the
234
+ # metadata endpoint is the only source of size / content_type / filename.
194
235
  #
195
- # [Fog::#{provider}::File] file data from remote service
236
+ # === Returns
196
237
  #
197
- # def file
198
- # @file ||= directory.files.head(path)
199
- # end
238
+ # [Hash, nil] metadata hash, or nil when there is no image or it cannot be fetched
239
+ def metadata
240
+ return @metadata if @metadata_fetched
200
241
 
201
- def read_source_file(file_body)
202
- return unless ::File.exist?(file_body.path)
242
+ @metadata_fetched = true
243
+ @metadata = fetch_metadata
244
+ end
203
245
 
204
- begin
205
- file_body = ::File.open(file_body.path) if file_body.closed? # Reopen if it's already closed
206
- file_body.read
207
- ensure
208
- file_body.close
209
- end
246
+ def reset_metadata
247
+ @metadata = nil
248
+ @metadata_fetched = false
249
+ end
250
+
251
+ # NOTE: Never raises. These values are read on ordinary code paths (CarrierWave's
252
+ # Uploader::Proxy delegates size / content_type to this object), so a transient API
253
+ # failure must not break the caller; the absence of metadata is reported as nil.
254
+ def fetch_metadata
255
+ image_id = metadata_image_id
256
+ return nil if image_id.nil? || image_id.empty?
257
+
258
+ result = Resizing.metadata(image_id, {})
259
+ return nil if !result.is_a?(Hash) || result['error']
260
+
261
+ result
262
+ rescue StandardError
263
+ nil
264
+ end
265
+
266
+ # The image to look up: the identifier this instance was built with/retrieved,
267
+ # falling back to the value currently stored in the model column.
268
+ def metadata_image_id
269
+ return @public_id.image_id if @public_id.present?
270
+
271
+ Resizing::PublicId.new(model.send(:read_attribute, serialization_column)).image_id
272
+ rescue StandardError
273
+ nil
274
+ end
275
+
276
+ # Guess the content type from a file name/path.
277
+ # Returns DEFAULT_CONTENT_TYPE when the extension is unknown to MIME::Types,
278
+ # which is the same convention CarrierWave/Rack use for unidentifiable files.
279
+ def guess_content_type(path)
280
+ return DEFAULT_CONTENT_TYPE if path.nil?
281
+
282
+ MIME::Types.type_for(path.to_s).first&.content_type || DEFAULT_CONTENT_TYPE
210
283
  end
211
284
 
212
285
  def url_options_supported?(local_file)
@@ -12,9 +12,13 @@ module Resizing
12
12
  f
13
13
  end
14
14
 
15
- def remove!(file)
16
- f = Resizing::CarrierWave::Storage::File.new(uploader)
17
- f.delete(file)
15
+ # NOTE: CarrierWave 2.2.x never reaches here (Uploader::Remove#remove! calls
16
+ # @file.delete directly), but newer versions may remove through the storage.
17
+ # File#delete takes no argument and resolves the image to delete by itself, so
18
+ # reuse the given file when it is already ours instead of building a new one.
19
+ def remove!(file = nil)
20
+ f = file.is_a?(Resizing::CarrierWave::Storage::File) ? file : Resizing::CarrierWave::Storage::File.new(uploader)
21
+ f.delete
18
22
  f
19
23
  end
20
24
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'resizing/carrier_wave/callback_file'
3
4
  require 'resizing/carrier_wave/storage/file'
4
5
  require 'resizing/carrier_wave/storage/remote'
5
6
 
@@ -29,11 +30,20 @@ module Resizing
29
30
  def initialize(*args)
30
31
  @requested_format = nil
31
32
  @default_format = nil
33
+ @retrieved_identifier = nil
32
34
  super
33
35
  end
34
36
 
37
+ # Override to store the identifier and set up @file for later use
38
+ def retrieve_from_store!(identifier)
39
+ @retrieved_identifier = identifier
40
+ super
41
+ # Ensure @file is set up so that remove! can call @file.delete
42
+ file
43
+ end
44
+
35
45
  def file
36
- file_identifier = identifier || read_column
46
+ file_identifier = @retrieved_identifier || identifier || read_column
37
47
 
38
48
  return nil if file_identifier.blank?
39
49
 
@@ -95,16 +105,50 @@ module Resizing
95
105
  @transform.push(:resize_to_fit, *args)
96
106
  end
97
107
 
108
+ # Resizing has no cache storage, so the file is uploaded to Resizing directly here
109
+ # instead of being copied to a local workfile and handed to cache_storage.
110
+ #
111
+ # The :cache callbacks are still run, so that the uploader settings hooked on them
112
+ # (extension_allowlist / extension_denylist / content_type_allowlist /
113
+ # content_type_denylist / size_range) are honoured before the upload happens.
114
+ # The callbacks that do not fit the Resizing flow (process! and cache_versions!)
115
+ # are disabled below.
98
116
  def cache!(new_file)
99
117
  return if new_file.nil?
100
118
 
101
- file = storage.store!(new_file)
102
- # do not assign @cache_id, bacause resizing do not support cache
103
- # save to resizing directly
104
- # @cache_id = file.public_id
119
+ # CallbackFile responds to #extension / #content_type / #size, which the
120
+ # before :cache callbacks need, with the extension derived from the content type
121
+ # (see CallbackFile). The original object is handed to the storage, because it
122
+ # carries the original filename sent to Resizing.
123
+ sanitized = CallbackFile.new(new_file)
124
+ return if sanitized.empty?
125
+
126
+ with_callbacks(:cache, sanitized) do
127
+ file = storage.store!(new_file)
128
+ # do not assign @cache_id, bacause resizing do not support cache
129
+ # save to resizing directly
130
+ # @cache_id = file.public_id
131
+
132
+ @filename = file.public_id.to_s
133
+ @file = file
134
+ end
135
+ end
105
136
 
106
- @filename = file.public_id.to_s
107
- @file = file
137
+ # process! is called before :cache
138
+ # Disable on Resizing, because `process` is repurposed as a definition of the
139
+ # transformation applied when the browser fetches the image URL (see #transform_string),
140
+ # so the processors must not be run as actual image processing on upload.
141
+ # https://github.com/carrierwaveuploader/carrierwave/blob/28190e99299a6131c0424a5d10205f471e39f3cd/lib/carrierwave/uploader/processing.rb#L12
142
+ def process!(*args)
143
+ # NOP
144
+ end
145
+
146
+ # cache_versions! is called after :cache
147
+ # Disable on Resizing, because each version would upload the same image again and
148
+ # the resulting public_id would be stored nowhere, leaving orphan images behind.
149
+ # https://github.com/carrierwaveuploader/carrierwave/blob/28190e99299a6131c0424a5d10205f471e39f3cd/lib/carrierwave/uploader/versions.rb#L17
150
+ def cache_versions!(*args)
151
+ # NOP
108
152
  end
109
153
 
110
154
  def filename
@@ -6,7 +6,6 @@ module Resizing
6
6
  # usage.
7
7
  # options = {
8
8
  # image_host: 'https://img.resizing.net',
9
- # video_host: 'https://video.resizing.net',
10
9
  # project_id: '098a2a0d-0000-0000-0000-000000000000',
11
10
  # secret_token: '4g1cshg......rbs6'
12
11
  # }
@@ -6,7 +6,7 @@ module Resizing
6
6
  mod.send(:attr_reader, :config)
7
7
  end
8
8
 
9
- def initialize_config *attrs
9
+ def initialize_config(*attrs)
10
10
  config = if attrs.first.is_a? Configuration
11
11
  attrs.first
12
12
  elsif attrs.first.nil?
@@ -15,10 +15,17 @@ module Resizing
15
15
  # Resizing::Client.new(configuration)
16
16
  #++
17
17
  class Configuration
18
- attr_reader :image_host, :video_host, :project_id, :secret_token, :open_timeout, :response_timeout, :enable_mock
18
+ attr_reader :image_host, :project_id, :secret_token, :open_timeout, :response_timeout, :enable_mock
19
19
  DEFAULT_HOST = 'https://img.resizing.net'
20
20
  DEFAULT_IMAGE_HOST = 'https://img.resizing.net'
21
- DEFAULT_VIDEO_HOST = 'https://video.resizing.net'
21
+ VIDEO_HOST_FALLBACK = 'https://video.resizing.net'
22
+ private_constant :VIDEO_HOST_FALLBACK
23
+ # 動画 API はサービス側で廃止されたため非推奨。将来のバージョンで削除する
24
+ DEFAULT_VIDEO_HOST = VIDEO_HOST_FALLBACK
25
+ deprecate_constant :DEFAULT_VIDEO_HOST
26
+ DEPRECATED_VIDEO_HOST_MESSAGE = '[resizing] video_host is deprecated: ' \
27
+ 'the video API has been removed from Resizing and this value is no longer used. ' \
28
+ 'It will be deleted in a future version.'
22
29
  DEFAULT_OPEN_TIMEOUT = 2
23
30
  DEFAULT_RESPONSE_TIMEOUT = 10
24
31
 
@@ -28,7 +35,7 @@ module Resizing
28
35
  case attr = attrs.first
29
36
  when Hash
30
37
  raise_configiration_error if attr[:project_id].nil? || attr[:secret_token].nil?
31
- raise_configiration_error if attr[:host].present?
38
+ raise_deprecated_host_error unless blank_value?(attr[:host])
32
39
 
33
40
  initialize_by_hash attr
34
41
  return
@@ -79,32 +86,55 @@ module Resizing
79
86
  ::SecureRandom.uuid
80
87
  end
81
88
 
89
+ # 動画 API の廃止により非推奨。値は保持するが利用されない
90
+ def video_host
91
+ warn_video_host_deprecation
92
+ @video_host
93
+ end
94
+
82
95
  def ==(other)
83
96
  return false unless self.class == other.class
84
97
 
85
- %i[image_host video_host project_id secret_token open_timeout response_timeout].all? do |name|
98
+ return false unless @video_host == other.instance_variable_get(:@video_host)
99
+
100
+ %i[image_host project_id secret_token open_timeout response_timeout].all? do |name|
86
101
  send(name) == other.send(name)
87
102
  end
88
103
  end
89
104
 
90
105
  private
91
106
 
107
+ # ActiveSupport の Object#present? / #blank? に依存しないための素の Ruby 実装。
108
+ # このgemはRails外からも利用されるため、activesupportがロードされていない前提で動作する必要がある。
109
+ def blank_value?(value)
110
+ value.nil? || value.to_s.strip.empty?
111
+ end
112
+
113
+ def raise_deprecated_host_error
114
+ raise ConfigurationError, 'The host on configuration is deprecated. Use image_host'
115
+ end
116
+
117
+ def deprecated_video_host(value)
118
+ warn_video_host_deprecation unless value.nil?
119
+ value.dup.freeze || VIDEO_HOST_FALLBACK
120
+ end
121
+
122
+ def warn_video_host_deprecation
123
+ warn DEPRECATED_VIDEO_HOST_MESSAGE
124
+ end
125
+
92
126
  def raise_configiration_error
93
- raise ConfigurationError, 'need hash and some keys like :image_host, video_host, :project_id, :secret_token'
127
+ raise ConfigurationError, 'need hash and some keys like :image_host, :project_id, :secret_token'
94
128
  end
95
129
 
96
- # rubocop:disable Metrics/AbcSize
97
130
  def initialize_by_hash(attr)
98
- raise 'The host on configuration is deprecated. Use image_host, video_host' if attr[:host].present?
99
-
100
131
  @image_host = attr[:image_host].dup.freeze || DEFAULT_IMAGE_HOST
101
- @video_host = attr[:video_host].dup.freeze || DEFAULT_VIDEO_HOST
132
+ @video_host = deprecated_video_host(attr[:video_host])
102
133
  @project_id = attr[:project_id].dup.freeze
103
134
  @secret_token = attr[:secret_token].dup.freeze
104
135
  @open_timeout = attr[:open_timeout] || DEFAULT_OPEN_TIMEOUT
105
136
  @response_timeout = attr[:response_timeout] || DEFAULT_RESPONSE_TIMEOUT
106
137
  @enable_mock = attr[:enable_mock] || false
107
138
  end
108
- # rubocop:enable Metrics/AbcSize
109
139
  end
110
140
  end