resizing 1.2.2 → 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 (48) 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/AGENTS.md +1 -0
  14. data/CHANGELOG.md +3 -0
  15. data/CLAUDE.md +68 -0
  16. data/Gemfile +9 -3
  17. data/README.md +114 -2
  18. data/Rakefile +5 -7
  19. data/bin/setup +3 -0
  20. data/git-hooks/pre-push +6 -0
  21. data/git-hooks/pre-push-ruby.sh +48 -0
  22. data/lib/resizing/carrier_wave/callback_file.rb +88 -0
  23. data/lib/resizing/carrier_wave/storage/file.rb +84 -36
  24. data/lib/resizing/carrier_wave/storage/remote.rb +7 -3
  25. data/lib/resizing/carrier_wave.rb +41 -6
  26. data/lib/resizing/client.rb +0 -1
  27. data/lib/resizing/configurable.rb +1 -1
  28. data/lib/resizing/configuration.rb +40 -10
  29. data/lib/resizing/mock_client.rb +23 -7
  30. data/lib/resizing/public_id.rb +3 -2
  31. data/lib/resizing/version.rb +1 -1
  32. data/lib/resizing.rb +0 -1
  33. data/resizing.gemspec +3 -3
  34. data/test/resizing/carrier_wave/storage/file_test.rb +455 -9
  35. data/test/resizing/carrier_wave/storage/remote_test.rb +91 -9
  36. data/test/resizing/carrier_wave_cache_callbacks_test.rb +224 -0
  37. data/test/resizing/carrier_wave_callback_file_test.rb +77 -0
  38. data/test/resizing/carrier_wave_test.rb +201 -26
  39. data/test/resizing/carrier_wave_uploader_test.rb +211 -0
  40. data/test/resizing/client_test.rb +324 -25
  41. data/test/resizing/configuration_test.rb +235 -10
  42. data/test/resizing/http_clientable_test.rb +27 -0
  43. data/test/resizing/mock_client_test.rb +111 -1
  44. data/test/resizing/public_id_test.rb +96 -0
  45. data/test/resizing_module_test.rb +110 -0
  46. data/test/resizing_test.rb +8 -0
  47. data/test/test_helper.rb +297 -9
  48. metadata +33 -9
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,26 +7,37 @@ 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
43
  # rubocop:disable Metrics/AbcSize
@@ -73,29 +84,25 @@ module Resizing
73
84
  ##
74
85
  # Read content of file from service
75
86
  #
76
- # === Returns
77
- #
78
- # [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.
79
92
  def read
80
- file_body = file.body
81
-
82
- return if file_body.nil?
83
- return file_body unless file_body.is_a?(::File)
84
-
85
- # Fog::Storage::XXX::File#body could return the source file which was upoloaded to the remote server.
86
- read_source_file(file_body) if ::File.exist?(file_body.path)
87
-
88
- # If the source file doesn't exist, the remote content is read
89
- @file = nil
90
- file.body
93
+ raise NotImplementedError, 'Resizing does not support reading the stored image content'
91
94
  end
92
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.
93
100
  def size
94
- file.nil? ? 0 : file.content_length
101
+ (metadata && metadata['size']) || 0
95
102
  end
96
103
 
97
104
  def exists?
98
- !!file
105
+ !metadata.nil?
99
106
  end
100
107
 
101
108
  def current_path
@@ -118,8 +125,10 @@ module Resizing
118
125
  @content_type ||= if new_file.respond_to? :content_type
119
126
  new_file.content_type
120
127
  else
121
- # guess content-type from extension
122
- 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)
123
132
  end
124
133
 
125
134
  original_filename = new_file.try(:original_filename) || new_file.try(:filename) || new_file.try(:path)
@@ -164,8 +173,12 @@ module Resizing
164
173
  # CarrierWave::Storage::Fog::File.new(@uploader, @base, new_path)
165
174
  # end
166
175
 
176
+ # NOTE: Resizing::CarrierWave#file calls this on every access, so the cached metadata
177
+ # is only dropped when the identifier actually changes.
167
178
  def retrieve(identifier)
168
- @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
169
182
  end
170
183
 
171
184
  private
@@ -213,25 +226,60 @@ module Resizing
213
226
  end
214
227
 
215
228
  ##
216
- # 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).
217
231
  #
218
- # === 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.
219
235
  #
220
- # [Fog::#{provider}::File] file data from remote service
236
+ # === Returns
221
237
  #
222
- # def file
223
- # @file ||= directory.files.head(path)
224
- # 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
241
+
242
+ @metadata_fetched = true
243
+ @metadata = fetch_metadata
244
+ end
225
245
 
226
- def read_source_file(file_body)
227
- return unless ::File.exist?(file_body.path)
246
+ def reset_metadata
247
+ @metadata = nil
248
+ @metadata_fetched = false
249
+ end
228
250
 
229
- begin
230
- file_body = ::File.open(file_body.path) if file_body.closed? # Reopen if it's already closed
231
- file_body.read
232
- ensure
233
- file_body.close
234
- end
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
235
283
  end
236
284
 
237
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
 
@@ -104,16 +105,50 @@ module Resizing
104
105
  @transform.push(:resize_to_fit, *args)
105
106
  end
106
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.
107
116
  def cache!(new_file)
108
117
  return if new_file.nil?
109
118
 
110
- file = storage.store!(new_file)
111
- # do not assign @cache_id, bacause resizing do not support cache
112
- # save to resizing directly
113
- # @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
136
+
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
114
145
 
115
- @filename = file.public_id.to_s
116
- @file = file
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
117
152
  end
118
153
 
119
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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'securerandom'
4
+
3
5
  module Resizing
4
6
  class MockClient
5
7
  def post(_file_or_binary, _options = {})
@@ -10,33 +12,47 @@ module Resizing
10
12
  def put(name, _file_or_binary, _options)
11
13
  r = load_yaml('test/vcr/client/put.yml')
12
14
  result = JSON.parse(r['string'])
13
- # replace name, public_id and version by name argument
15
+ # カセット内の値に依存せず、name と新しい version で id / public_id を組み立て直す
16
+ version = generate_version
14
17
  result['id'] = name
15
- result['public_id'].gsub!(/AWEaewfAreaweFAFASfwe/, name)
16
- result['public_id'].gsub!(/v6Ew3HmDAYfb3NMRdLxR45i_gXMbLlGyi/, "v#{Time.now.to_f}")
18
+ result['version'] = version
19
+ result['latest_version_id'] = version
20
+ result['public_id'] = build_public_id(result['public_id'], image_id: name, version: version)
17
21
  result
18
22
  end
19
23
 
20
24
  def delete(name)
21
25
  r = load_yaml('test/vcr/client/delete.yml')
22
26
  result = JSON.parse(r['string'])
23
- # replace name and public_id by name argument
27
+ # カセット内の値に依存せず、name で id / public_id を組み立て直す
24
28
  result['id'] = name
25
- result['public_id'].gsub!(/28c49144-c00d-4cb5-8619-98ce95977b9c/, name)
29
+ result['public_id'] = build_public_id(result['public_id'], image_id: name)
26
30
  result
27
31
  end
28
32
 
29
33
  def metadata(name, _options = {})
30
34
  r = load_yaml('test/vcr/client/metadata.yml')
31
35
  result = JSON.parse(r['string'])
32
- # replace name and public_id by name argument
36
+ # カセット内の値に依存せず、name で id / public_id を組み立て直す
33
37
  result['id'] = name
34
- result['public_id'].gsub!(/bfdaf2b3-7ec5-41f4-9caa-d53247dd9666/, name)
38
+ result['public_id'] = build_public_id(result['public_id'], image_id: name)
35
39
  result
36
40
  end
37
41
 
38
42
  private
39
43
 
44
+ # カセットの public_id を PublicId で分解し、image_id / version を差し替えて組み立て直す
45
+ def build_public_id(public_id, image_id:, version: nil)
46
+ parsed = Resizing::PublicId.new(public_id)
47
+ version ||= parsed.version
48
+ identifier = "/projects/#{parsed.project_id}/upload/images/#{image_id}"
49
+ version.nil? ? identifier : "#{identifier}/v#{version}"
50
+ end
51
+
52
+ def generate_version
53
+ SecureRandom.alphanumeric(32)
54
+ end
55
+
40
56
  def load_yaml(filename)
41
57
  path = "#{library_root}/#{filename}"
42
58
  YAML.load_file(path)['http_interactions'].first['response']['body']
@@ -7,8 +7,9 @@ module Resizing
7
7
  parsed
8
8
  end
9
9
 
10
+ # nil / 空文字 / 空白のみの文字列はいずれも「値なし」として扱う
10
11
  def empty?
11
- @public_id.to_s.empty?
12
+ @public_id.to_s.strip.empty?
12
13
  end
13
14
 
14
15
  def image_id
@@ -39,7 +40,7 @@ module Resizing
39
40
  private
40
41
 
41
42
  def parsed
42
- return nil if @public_id.nil?
43
+ return nil if empty?
43
44
 
44
45
  unless defined? @parsed
45
46
  @parsed = Resizing.separate_public_id(@public_id)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Resizing
4
- VERSION = '1.2.2'
4
+ VERSION = '1.3.1'
5
5
  end
data/lib/resizing.rb CHANGED
@@ -14,7 +14,6 @@ module Resizing
14
14
  autoload :Configuration, 'resizing/configuration'
15
15
  autoload :CarrierWave, 'resizing/carrier_wave'
16
16
  autoload :PublicId, 'resizing/public_id'
17
- autoload :Video, 'resizing/video'
18
17
  autoload :ActiveStorage, 'resizing/active_storage'
19
18
 
20
19
  class Error < StandardError; end