archive_storage 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +348 -0
  3. data/archive_storage.gemspec +42 -0
  4. data/lib/archive_storage/adapters/filesystem.rb +135 -0
  5. data/lib/archive_storage/adapters/memory.rb +101 -0
  6. data/lib/archive_storage/adapters/metadata.rb +23 -0
  7. data/lib/archive_storage/adapters/s3.rb +186 -0
  8. data/lib/archive_storage/configuration.rb +115 -0
  9. data/lib/archive_storage/duration_parser.rb +26 -0
  10. data/lib/archive_storage/enqueuer.rb +29 -0
  11. data/lib/archive_storage/errors.rb +9 -0
  12. data/lib/archive_storage/jobs/migration_job.rb +28 -0
  13. data/lib/archive_storage/jobs/queue_job.rb +65 -0
  14. data/lib/archive_storage/jobs/sidekiq_migration_worker.rb +28 -0
  15. data/lib/archive_storage/jobs/sidekiq_queue_worker.rb +65 -0
  16. data/lib/archive_storage/migration_rate.rb +16 -0
  17. data/lib/archive_storage/migrator.rb +151 -0
  18. data/lib/archive_storage/model.rb +35 -0
  19. data/lib/archive_storage/models/file_record.rb +26 -0
  20. data/lib/archive_storage/mount_config.rb +50 -0
  21. data/lib/archive_storage/plan_result.rb +61 -0
  22. data/lib/archive_storage/planner.rb +190 -0
  23. data/lib/archive_storage/policy.rb +48 -0
  24. data/lib/archive_storage/policy_builder.rb +72 -0
  25. data/lib/archive_storage/railtie.rb +23 -0
  26. data/lib/archive_storage/registry.rb +109 -0
  27. data/lib/archive_storage/schedule_config.rb +79 -0
  28. data/lib/archive_storage/scheduler.rb +93 -0
  29. data/lib/archive_storage/storage.rb +91 -0
  30. data/lib/archive_storage/storage_config.rb +37 -0
  31. data/lib/archive_storage/storage_rule.rb +57 -0
  32. data/lib/archive_storage/stored_file.rb +94 -0
  33. data/lib/archive_storage/tasks.rake +82 -0
  34. data/lib/archive_storage/verification_result.rb +11 -0
  35. data/lib/archive_storage/verifier.rb +144 -0
  36. data/lib/archive_storage/version.rb +5 -0
  37. data/lib/archive_storage.rb +148 -0
  38. data/lib/generators/archive_storage/install_generator.rb +28 -0
  39. data/lib/generators/archive_storage/templates/create_archive_storage_files.rb +53 -0
  40. metadata +227 -0
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "adapters/metadata"
5
+ require_relative "verification_result"
6
+
7
+ module ArchiveStorage
8
+ class Verifier
9
+ def initialize(strategy: ArchiveStorage.configuration.verification_strategy)
10
+ @strategy = strategy.to_sym
11
+ end
12
+
13
+ def verify!(source_adapter:, target_adapter:, source_key:, target_key:)
14
+ source_metadata = source_adapter.head(source_key)
15
+ target_metadata = target_adapter.head(target_key)
16
+
17
+ verify_metadata!(source_metadata, target_metadata)
18
+ verify_bytes!(source_adapter, target_adapter, source_key, target_key) if strategy == :byte_compare
19
+
20
+ VerificationResult.new(
21
+ strategy: strategy,
22
+ matched_by: matched_by(source_metadata, target_metadata),
23
+ source_metadata: source_metadata,
24
+ target_metadata: target_metadata
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :strategy
31
+
32
+ def verify_metadata!(source_metadata, target_metadata)
33
+ verify_size!(source_metadata, target_metadata)
34
+
35
+ case strategy
36
+ when :auto
37
+ verify_auto!(source_metadata, target_metadata)
38
+ when :size
39
+ true
40
+ when :etag
41
+ verify_etag!(source_metadata, target_metadata, allow_multipart: true)
42
+ when :safe_etag
43
+ verify_safe_etag!(source_metadata, target_metadata)
44
+ when :checksum
45
+ verify_checksum!(source_metadata, target_metadata)
46
+ when :byte_compare
47
+ true
48
+ else
49
+ raise ConfigurationError, "unknown verification strategy #{strategy.inspect}"
50
+ end
51
+ end
52
+
53
+ def verify_size!(source_metadata, target_metadata)
54
+ return if source_metadata.byte_size == target_metadata.byte_size
55
+
56
+ raise VerificationError,
57
+ "byte size mismatch: #{source_metadata.byte_size} != #{target_metadata.byte_size}"
58
+ end
59
+
60
+ def verify_auto!(source_metadata, target_metadata)
61
+ if comparable_checksums?(source_metadata, target_metadata)
62
+ verify_checksum!(source_metadata, target_metadata)
63
+ elsif comparable_safe_etags?(source_metadata, target_metadata)
64
+ verify_safe_etag!(source_metadata, target_metadata)
65
+ else
66
+ true
67
+ end
68
+ end
69
+
70
+ def verify_checksum!(source_metadata, target_metadata)
71
+ unless comparable_checksums?(source_metadata, target_metadata)
72
+ raise VerificationError, "checksums are not available or use different algorithms"
73
+ end
74
+
75
+ return if source_metadata.checksum == target_metadata.checksum
76
+
77
+ raise VerificationError,
78
+ "checksum mismatch: #{source_metadata.checksum.inspect} != #{target_metadata.checksum.inspect}"
79
+ end
80
+
81
+ def verify_safe_etag!(source_metadata, target_metadata)
82
+ unless comparable_safe_etags?(source_metadata, target_metadata)
83
+ raise VerificationError, "safe etags are not available"
84
+ end
85
+
86
+ verify_etag!(source_metadata, target_metadata, allow_multipart: false)
87
+ end
88
+
89
+ def verify_etag!(source_metadata, target_metadata, allow_multipart:)
90
+ unless source_metadata.etag && target_metadata.etag
91
+ raise VerificationError, "etags are not available"
92
+ end
93
+
94
+ if !allow_multipart && (source_metadata.multipart_etag? || target_metadata.multipart_etag?)
95
+ raise VerificationError, "multipart etags are not stable content checksums"
96
+ end
97
+
98
+ return if source_metadata.etag == target_metadata.etag
99
+
100
+ raise VerificationError,
101
+ "etag mismatch: #{source_metadata.etag.inspect} != #{target_metadata.etag.inspect}"
102
+ end
103
+
104
+ def verify_bytes!(source_adapter, target_adapter, source_key, target_key)
105
+ source_body = source_adapter.read(source_key)
106
+ target_body = target_adapter.read(target_key)
107
+ return if source_body == target_body
108
+
109
+ raise VerificationError, "byte comparison mismatch"
110
+ end
111
+
112
+ def comparable_checksums?(source_metadata, target_metadata)
113
+ source_metadata.checksum &&
114
+ target_metadata.checksum &&
115
+ source_metadata.checksum_algorithm &&
116
+ source_metadata.checksum_algorithm == target_metadata.checksum_algorithm
117
+ end
118
+
119
+ def comparable_safe_etags?(source_metadata, target_metadata)
120
+ source_metadata.safe_etag? &&
121
+ target_metadata.safe_etag?
122
+ end
123
+
124
+ def matched_by(source_metadata, target_metadata)
125
+ case strategy
126
+ when :checksum
127
+ :checksum
128
+ when :etag
129
+ :etag
130
+ when :safe_etag
131
+ :safe_etag
132
+ when :byte_compare
133
+ :byte_compare
134
+ when :auto
135
+ return :checksum if comparable_checksums?(source_metadata, target_metadata)
136
+ return :safe_etag if comparable_safe_etags?(source_metadata, target_metadata)
137
+
138
+ :size
139
+ else
140
+ :size
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArchiveStorage
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "carrierwave"
5
+ rescue LoadError
6
+ # ArchiveStorage can be loaded without CarrierWave. CarrierWave integration is optional at runtime.
7
+ end
8
+
9
+ require_relative "archive_storage/version"
10
+ require_relative "archive_storage/errors"
11
+ require_relative "archive_storage/configuration"
12
+ require_relative "archive_storage/policy_builder"
13
+ require_relative "archive_storage/model"
14
+ require_relative "archive_storage/registry"
15
+ require_relative "archive_storage/storage"
16
+ require_relative "archive_storage/planner"
17
+ require_relative "archive_storage/enqueuer"
18
+ require_relative "archive_storage/verifier"
19
+ require_relative "archive_storage/migrator"
20
+ require_relative "archive_storage/jobs/queue_job"
21
+ require_relative "archive_storage/jobs/migration_job"
22
+
23
+ module ArchiveStorage
24
+ class << self
25
+ attr_writer :configuration, :registry
26
+
27
+ def configuration
28
+ @configuration ||= Configuration.new
29
+ end
30
+
31
+ def configure
32
+ yield configuration
33
+ end
34
+
35
+ def reset_configuration!
36
+ @configuration = Configuration.new
37
+ @registry = Registry.new
38
+ end
39
+
40
+ def adapter(name)
41
+ configuration.adapter(name)
42
+ end
43
+
44
+ def registry
45
+ @registry ||= Registry.new
46
+ end
47
+
48
+ def register_uploader(uploader_class)
49
+ uploaders << uploader_class
50
+ end
51
+
52
+ def register_mount(model, mounted_as, uploader:, policy:)
53
+ configuration.mount(model, mounted_as, uploader: uploader, policy: policy)
54
+ end
55
+
56
+ def wire_carrierwave_uploader!(uploader_class)
57
+ return unless uploader_class
58
+
59
+ uploader_class.include(CarrierWave) unless uploader_class < CarrierWave
60
+ uploader_class.storage(:archive_storage) if uploader_class.respond_to?(:storage)
61
+ end
62
+
63
+ def policy_for_uploader(uploader)
64
+ mount_policy_for_uploader(uploader) ||
65
+ (uploader.class.archive_storage_policy if uploader.class.respond_to?(:archive_storage_policy))
66
+ end
67
+
68
+ def policy_for_mount(model, mounted_as)
69
+ configuration.find_mount(model, mounted_as)&.policy ||
70
+ model_policy(model, mounted_as)
71
+ end
72
+
73
+ def policy_for_record(record_type, mounted_as)
74
+ policy_for_mount(record_type, mounted_as)
75
+ end
76
+
77
+ def uploaders
78
+ @uploaders ||= []
79
+ end
80
+
81
+ def good_job_cron
82
+ configuration.schedules.each_with_object({}) do |schedule, entries|
83
+ entries[schedule.entry_name] = schedule.good_job_entry
84
+ end
85
+ end
86
+
87
+ def sidekiq_cron
88
+ configuration.schedules.each_with_object({}) do |schedule, entries|
89
+ entries[schedule.entry_name.to_s] = schedule.sidekiq_cron_entry
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def model_policy(model, mounted_as)
96
+ model_class = model.is_a?(Class) ? model : constantize(model)
97
+ return nil unless model_class.respond_to?(:archive_storage_policy_for)
98
+
99
+ model_class.archive_storage_policy_for(mounted_as)
100
+ rescue NameError
101
+ nil
102
+ end
103
+
104
+ def mount_policy_for_uploader(uploader)
105
+ return nil unless uploader.respond_to?(:model) && uploader.model
106
+ return nil unless uploader.respond_to?(:mounted_as) && uploader.mounted_as
107
+
108
+ policy_for_mount(uploader.model.class, uploader.mounted_as)
109
+ end
110
+
111
+ def constantize(value)
112
+ value.to_s.split("::").inject(Object) { |namespace, name| namespace.const_get(name) }
113
+ end
114
+ end
115
+
116
+ module CarrierWave
117
+ def self.included(base)
118
+ ArchiveStorage.register_uploader(base)
119
+ base.extend(ClassMethods)
120
+ end
121
+
122
+ module ClassMethods
123
+ def archive_storage(&block)
124
+ if block
125
+ @archive_storage_policy = PolicyBuilder.build(&block)
126
+ else
127
+ archive_storage_policy
128
+ end
129
+ end
130
+
131
+ def archive_storage_policy
132
+ @archive_storage_policy ||
133
+ (superclass.archive_storage_policy if superclass.respond_to?(:archive_storage_policy))
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ if defined?(::CarrierWave) && ::CarrierWave.respond_to?(:configure)
140
+ ::CarrierWave.configure do |config|
141
+ if config.respond_to?(:storage_engines)
142
+ config.storage_engines[:archive_storage] = "ArchiveStorage::Storage"
143
+ end
144
+ end
145
+ end
146
+
147
+ require_relative "archive_storage/scheduler"
148
+ require_relative "archive_storage/railtie" if defined?(::Rails::Railtie)
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module ArchiveStorage
7
+ class InstallGenerator < ::Rails::Generators::Base
8
+ include ::Rails::Generators::Migration
9
+ namespace "archive_storage:install"
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ def copy_migration
14
+ migration_template(
15
+ "create_archive_storage_files.rb",
16
+ "db/migrate/create_archive_storage_files.rb"
17
+ )
18
+ end
19
+
20
+ def self.next_migration_number(dirname)
21
+ if ::ActiveRecord::Base.timestamped_migrations
22
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
23
+ else
24
+ "%.3d" % (current_migration_number(dirname) + 1)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateArchiveStorageFiles < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :archive_storage_files do |t|
6
+ t.string :record_type, null: false
7
+ t.bigint :record_id, null: false
8
+ t.string :mounted_as, null: false
9
+ t.string :uploader, null: false
10
+
11
+ t.string :identifier, null: false
12
+ t.string :storage_key, null: false
13
+ t.string :source_storage_key
14
+ t.string :target_storage_key
15
+
16
+ t.string :current_storage, null: false
17
+ t.string :source_storage
18
+ t.string :target_storage
19
+
20
+ t.bigint :byte_size
21
+ t.string :checksum
22
+ t.string :content_type
23
+
24
+ t.datetime :migration_started_at
25
+ t.datetime :enqueued_at
26
+ t.datetime :migrated_at
27
+ t.datetime :verified_at
28
+ t.datetime :source_deleted_at
29
+
30
+ t.boolean :source_delete_pending, null: false, default: false
31
+ t.string :last_error
32
+ t.integer :attempts, null: false, default: 0
33
+
34
+ t.timestamps
35
+ end
36
+
37
+ add_index :archive_storage_files,
38
+ [:record_type, :record_id, :mounted_as, :identifier],
39
+ name: "idx_archive_storage_identity"
40
+
41
+ add_index :archive_storage_files,
42
+ [:uploader, :current_storage],
43
+ name: "idx_archive_storage_uploader_storage"
44
+
45
+ add_index :archive_storage_files,
46
+ [:source_storage, :source_deleted_at],
47
+ name: "idx_archive_storage_cleanup"
48
+
49
+ add_index :archive_storage_files,
50
+ [:source_delete_pending, :source_deleted_at],
51
+ name: "idx_archive_storage_delete_pending"
52
+ end
53
+ end
metadata ADDED
@@ -0,0 +1,227 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: archive_storage
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - E. Tashkovyan
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-05-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activejob
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '9.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '6.1'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '9.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activerecord
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '6.1'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '9.0'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '6.1'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '9.0'
53
+ - !ruby/object:Gem::Dependency
54
+ name: activesupport
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '6.1'
60
+ - - "<"
61
+ - !ruby/object:Gem::Version
62
+ version: '9.0'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '6.1'
70
+ - - "<"
71
+ - !ruby/object:Gem::Version
72
+ version: '9.0'
73
+ - !ruby/object:Gem::Dependency
74
+ name: railties
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '6.1'
80
+ - - "<"
81
+ - !ruby/object:Gem::Version
82
+ version: '9.0'
83
+ type: :runtime
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '6.1'
90
+ - - "<"
91
+ - !ruby/object:Gem::Version
92
+ version: '9.0'
93
+ - !ruby/object:Gem::Dependency
94
+ name: aws-sdk-s3
95
+ requirement: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '1'
100
+ type: :development
101
+ prerelease: false
102
+ version_requirements: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - "~>"
105
+ - !ruby/object:Gem::Version
106
+ version: '1'
107
+ - !ruby/object:Gem::Dependency
108
+ name: carrierwave
109
+ requirement: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '2.2'
114
+ - - "<"
115
+ - !ruby/object:Gem::Version
116
+ version: '4.0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '2.2'
124
+ - - "<"
125
+ - !ruby/object:Gem::Version
126
+ version: '4.0'
127
+ - !ruby/object:Gem::Dependency
128
+ name: minitest
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - "~>"
132
+ - !ruby/object:Gem::Version
133
+ version: '5.0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - "~>"
139
+ - !ruby/object:Gem::Version
140
+ version: '5.0'
141
+ - !ruby/object:Gem::Dependency
142
+ name: rake
143
+ requirement: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - "~>"
146
+ - !ruby/object:Gem::Version
147
+ version: '13.0'
148
+ type: :development
149
+ prerelease: false
150
+ version_requirements: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - "~>"
153
+ - !ruby/object:Gem::Version
154
+ version: '13.0'
155
+ description: Move uploads across storage backends such as filesystem, NFS, MinIO,
156
+ and S3 without downtime.
157
+ email: []
158
+ executables: []
159
+ extensions: []
160
+ extra_rdoc_files: []
161
+ files:
162
+ - README.md
163
+ - archive_storage.gemspec
164
+ - lib/archive_storage.rb
165
+ - lib/archive_storage/adapters/filesystem.rb
166
+ - lib/archive_storage/adapters/memory.rb
167
+ - lib/archive_storage/adapters/metadata.rb
168
+ - lib/archive_storage/adapters/s3.rb
169
+ - lib/archive_storage/configuration.rb
170
+ - lib/archive_storage/duration_parser.rb
171
+ - lib/archive_storage/enqueuer.rb
172
+ - lib/archive_storage/errors.rb
173
+ - lib/archive_storage/jobs/migration_job.rb
174
+ - lib/archive_storage/jobs/queue_job.rb
175
+ - lib/archive_storage/jobs/sidekiq_migration_worker.rb
176
+ - lib/archive_storage/jobs/sidekiq_queue_worker.rb
177
+ - lib/archive_storage/migration_rate.rb
178
+ - lib/archive_storage/migrator.rb
179
+ - lib/archive_storage/model.rb
180
+ - lib/archive_storage/models/file_record.rb
181
+ - lib/archive_storage/mount_config.rb
182
+ - lib/archive_storage/plan_result.rb
183
+ - lib/archive_storage/planner.rb
184
+ - lib/archive_storage/policy.rb
185
+ - lib/archive_storage/policy_builder.rb
186
+ - lib/archive_storage/railtie.rb
187
+ - lib/archive_storage/registry.rb
188
+ - lib/archive_storage/schedule_config.rb
189
+ - lib/archive_storage/scheduler.rb
190
+ - lib/archive_storage/storage.rb
191
+ - lib/archive_storage/storage_config.rb
192
+ - lib/archive_storage/storage_rule.rb
193
+ - lib/archive_storage/stored_file.rb
194
+ - lib/archive_storage/tasks.rake
195
+ - lib/archive_storage/verification_result.rb
196
+ - lib/archive_storage/verifier.rb
197
+ - lib/archive_storage/version.rb
198
+ - lib/generators/archive_storage/install_generator.rb
199
+ - lib/generators/archive_storage/templates/create_archive_storage_files.rb
200
+ homepage: https://github.com/estashkovyan/archive_storage
201
+ licenses:
202
+ - MIT
203
+ metadata:
204
+ allowed_push_host: https://rubygems.org
205
+ homepage_uri: https://github.com/estashkovyan/archive_storage
206
+ source_code_uri: https://github.com/estashkovyan/archive_storage/tree/main
207
+ changelog_uri: https://github.com/estashkovyan/archive_storage/releases
208
+ post_install_message:
209
+ rdoc_options: []
210
+ require_paths:
211
+ - lib
212
+ required_ruby_version: !ruby/object:Gem::Requirement
213
+ requirements:
214
+ - - ">="
215
+ - !ruby/object:Gem::Version
216
+ version: 3.1.0
217
+ required_rubygems_version: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - ">="
220
+ - !ruby/object:Gem::Version
221
+ version: '0'
222
+ requirements: []
223
+ rubygems_version: 3.5.22
224
+ signing_key:
225
+ specification_version: 4
226
+ summary: Policy-based archive storage and zero-downtime file migration.
227
+ test_files: []