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,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "enqueuer"
5
+ require_relative "planner"
6
+ require_relative "verifier"
7
+
8
+ module ArchiveStorage
9
+ class Migrator
10
+ def initialize(planner: nil)
11
+ @planner = planner
12
+ end
13
+
14
+ def enqueue_or_migrate!(inline: false)
15
+ count = 0
16
+
17
+ planner.each_candidate do |candidate|
18
+ file_record = ArchiveStorage.registry.claim_candidate(candidate)
19
+ next unless file_record
20
+
21
+ if inline
22
+ migrate_record!(file_record)
23
+ else
24
+ Enqueuer.new.enqueue_migration(file_record.id)
25
+ end
26
+
27
+ count += 1
28
+ end
29
+
30
+ count
31
+ end
32
+
33
+ def migrate_record!(file_record)
34
+ file_record.with_lock do
35
+ source_storage = (file_record.source_storage || file_record.current_storage).to_sym
36
+ target_storage = file_record.target_storage.to_sym
37
+ source_key = file_record_source_key(file_record)
38
+ target_key = file_record_target_key(file_record)
39
+
40
+ return file_record if source_storage == target_storage && file_record.verified_at
41
+
42
+ file_record.update!(
43
+ migration_started_at: Time.now,
44
+ attempts: file_record.attempts.to_i + 1,
45
+ last_error: nil
46
+ )
47
+
48
+ source = ArchiveStorage.adapter(source_storage)
49
+ target = ArchiveStorage.adapter(target_storage)
50
+
51
+ target.copy_from(source, source_key, target_key)
52
+ verification = Verifier.new.verify!(
53
+ source_adapter: source,
54
+ target_adapter: target,
55
+ source_key: source_key,
56
+ target_key: target_key
57
+ )
58
+ target_metadata = verification.target_metadata
59
+
60
+ file_record.update!(
61
+ current_storage: target_storage.to_s,
62
+ source_storage: source_storage.to_s,
63
+ target_storage: target_storage.to_s,
64
+ storage_key: target_key,
65
+ byte_size: target_metadata.byte_size,
66
+ content_type: target_metadata.content_type,
67
+ checksum: target_metadata.checksum || target_metadata.etag,
68
+ migrated_at: Time.now,
69
+ verified_at: Time.now,
70
+ source_delete_pending: source_storage != target_storage,
71
+ last_error: nil
72
+ )
73
+ end
74
+
75
+ file_record
76
+ rescue StandardError => error
77
+ safe_update_error(file_record, error)
78
+ raise
79
+ end
80
+
81
+ def verify_record!(file_record)
82
+ target_storage = (file_record.target_storage || file_record.current_storage).to_sym
83
+ metadata = ArchiveStorage.adapter(target_storage).head(file_record.storage_key)
84
+
85
+ file_record.update!(
86
+ byte_size: metadata.byte_size,
87
+ content_type: metadata.content_type,
88
+ checksum: metadata.etag,
89
+ verified_at: Time.now,
90
+ last_error: nil
91
+ )
92
+ end
93
+
94
+ def cleanup_source!(file_record)
95
+ return false unless cleanup_ready?(file_record)
96
+
97
+ source_storage = file_record.source_storage.to_sym
98
+ ArchiveStorage.adapter(source_storage).delete(file_record_source_key(file_record))
99
+ file_record.update!(source_deleted_at: Time.now, source_delete_pending: false)
100
+ true
101
+ end
102
+
103
+ private
104
+
105
+ attr_reader :planner
106
+
107
+ def cleanup_ready?(file_record)
108
+ return false unless ArchiveStorage.configuration.delete_source_enabled
109
+ return false unless file_record.source_storage
110
+ return false if file_record.source_deleted_at
111
+
112
+ policy = policy_for(file_record)
113
+ policy_delay = cleanup_delay_for(file_record)
114
+ requires_verification = policy.nil? || policy.delete_requires_verification
115
+ verified = !file_record.verified_at.nil?
116
+ reference_time = requires_verification ? file_record.verified_at : (file_record.verified_at || file_record.migrated_at)
117
+ old_enough = reference_time && reference_time <= Time.now - policy_delay
118
+
119
+ (!requires_verification || verified) && old_enough
120
+ end
121
+
122
+ def cleanup_delay_for(file_record)
123
+ policy_for(file_record)&.delete_source_delay&.to_i ||
124
+ ArchiveStorage.configuration.default_cleanup_delay
125
+ end
126
+
127
+ def policy_for(file_record)
128
+ ArchiveStorage.policy_for_record(file_record.record_type, file_record.mounted_as)
129
+ rescue StandardError
130
+ nil
131
+ end
132
+
133
+ def safe_update_error(file_record, error)
134
+ file_record.update!(last_error: "#{error.class}: #{error.message}") if file_record.respond_to?(:update!)
135
+ rescue StandardError
136
+ nil
137
+ end
138
+
139
+ def file_record_source_key(file_record)
140
+ return file_record.source_storage_key if file_record.respond_to?(:source_storage_key) && file_record.source_storage_key
141
+
142
+ file_record.storage_key
143
+ end
144
+
145
+ def file_record_target_key(file_record)
146
+ return file_record.target_storage_key if file_record.respond_to?(:target_storage_key) && file_record.target_storage_key
147
+
148
+ file_record.storage_key
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArchiveStorage
4
+ module Model
5
+ def archive_storage_for(mounted_as, &block)
6
+ raise ArgumentError, "archive_storage_for requires a block" unless block
7
+
8
+ policy = PolicyBuilder.build(&block)
9
+ uploader_class = archive_storage_uploader_for(mounted_as)
10
+
11
+ ArchiveStorage.wire_carrierwave_uploader!(uploader_class)
12
+ ArchiveStorage.register_mount(self, mounted_as, uploader: uploader_class, policy: policy)
13
+
14
+ archive_storage_policies[mounted_as.to_sym] = policy
15
+ end
16
+
17
+ def archive_storage_policy_for(mounted_as)
18
+ archive_storage_policies[mounted_as.to_sym]
19
+ end
20
+
21
+ def archive_storage_policies
22
+ @archive_storage_policies ||= {}
23
+ end
24
+
25
+ private
26
+
27
+ def archive_storage_uploader_for(mounted_as)
28
+ if respond_to?(:uploaders) && uploaders[mounted_as.to_sym]
29
+ uploaders[mounted_as.to_sym]
30
+ else
31
+ raise ConfigurationError, "cannot find CarrierWave uploader for #{name}##{mounted_as}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "active_record"
5
+ rescue LoadError
6
+ # ActiveRecord is loaded in Rails apps. The registry object handles absence.
7
+ end
8
+
9
+ module ArchiveStorage
10
+ module Models
11
+ if defined?(::ActiveRecord::Base)
12
+ class FileRecord < ::ActiveRecord::Base
13
+ self.table_name = "archive_storage_files"
14
+
15
+ scope :verified, -> { where.not(verified_at: nil) }
16
+ scope :pending_cleanup, -> {
17
+ verified.where(source_delete_pending: true, source_deleted_at: nil).where.not(source_storage: nil)
18
+ }
19
+
20
+ def verified?
21
+ !!verified_at
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArchiveStorage
4
+ class MountConfig
5
+ attr_reader :model, :mounted_as, :uploader, :policy
6
+
7
+ def initialize(model, mounted_as, uploader: nil, policy: nil)
8
+ @model = model
9
+ @mounted_as = mounted_as.to_sym
10
+ @uploader = uploader
11
+ @policy = policy
12
+ end
13
+
14
+ def model_class
15
+ constantize(model)
16
+ end
17
+
18
+ def model_name
19
+ model.respond_to?(:name) ? model.name : model.to_s
20
+ end
21
+
22
+ def uploader_class
23
+ constantize(uploader) if uploader
24
+ end
25
+
26
+ def matches_model?(value, mounted_as_value = nil)
27
+ model_matches = model_name == (value.respond_to?(:name) ? value.name : value.to_s)
28
+ mount_matches = mounted_as_value.nil? || mounted_as == mounted_as_value.to_sym
29
+
30
+ model_matches && mount_matches
31
+ end
32
+
33
+ def matches_uploader?(value)
34
+ return true if value.nil?
35
+
36
+ expected = uploader_class || value
37
+ expected.to_s == value.to_s ||
38
+ (expected.respond_to?(:name) && expected.name == value.to_s)
39
+ end
40
+
41
+ private
42
+
43
+ def constantize(value)
44
+ return value if value.respond_to?(:name) && value.respond_to?(:all)
45
+ return value if value.is_a?(Class)
46
+
47
+ value.to_s.split("::").inject(Object) { |namespace, name| namespace.const_get(name) }
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArchiveStorage
4
+ class PlanResult
5
+ attr_accessor :uploader_name, :destination
6
+ attr_reader :candidates, :byte_size, :by_model
7
+
8
+ def initialize(uploader_name: nil, destination: nil)
9
+ @uploader_name = uploader_name
10
+ @destination = destination
11
+ @candidates = 0
12
+ @byte_size = 0
13
+ @by_model = Hash.new { |hash, key| hash[key] = { count: 0, byte_size: 0 } }
14
+ end
15
+
16
+ def add(candidate)
17
+ @candidates += 1
18
+ @byte_size += candidate.byte_size.to_i
19
+ row = by_model[candidate.record.class.name]
20
+ row[:count] += 1
21
+ row[:byte_size] += candidate.byte_size.to_i
22
+ end
23
+
24
+ def to_text
25
+ lines = []
26
+ lines << "ArchiveStorage Migration Plan"
27
+ lines << ""
28
+ lines << "Uploader: #{uploader_name || "all registered uploaders"}"
29
+ lines << "Candidates: #{candidates}"
30
+ lines << "Estimated size: #{format_bytes(byte_size)}"
31
+ lines << ""
32
+
33
+ if by_model.any?
34
+ lines << "By model:"
35
+ by_model.each do |model, stats|
36
+ lines << "- #{model}: #{stats[:count]} files, #{format_bytes(stats[:byte_size])}"
37
+ end
38
+ lines << ""
39
+ end
40
+
41
+ lines << "Destination: #{destination}" if destination
42
+ lines << "No files were moved."
43
+ lines.join("\n")
44
+ end
45
+
46
+ private
47
+
48
+ def format_bytes(value)
49
+ units = %w[B KB MB GB TB PB]
50
+ size = value.to_f
51
+ unit = units.shift
52
+
53
+ while size >= 1024 && units.any?
54
+ size /= 1024.0
55
+ unit = units.shift
56
+ end
57
+
58
+ "#{format("%.2f", size)} #{unit}"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "duration_parser"
4
+ require_relative "plan_result"
5
+
6
+ module ArchiveStorage
7
+ Candidate = Struct.new(
8
+ :record,
9
+ :mounted_as,
10
+ :uploader,
11
+ :identifier,
12
+ :storage_key,
13
+ :source_storage_key,
14
+ :target_storage_key,
15
+ :current_storage,
16
+ :target_storage,
17
+ :byte_size,
18
+ :content_type,
19
+ keyword_init: true
20
+ )
21
+
22
+ class Planner
23
+ attr_reader :uploader_name,
24
+ :model_name,
25
+ :mounted_as,
26
+ :older_than,
27
+ :limit,
28
+ :estimate_sizes
29
+
30
+ def initialize(uploader: nil, model: nil, mounted_as: nil, older_than: nil, limit: nil, estimate_sizes: false)
31
+ @uploader_name = uploader
32
+ @model_name = model
33
+ @mounted_as = mounted_as&.to_sym
34
+ @older_than = DurationParser.parse(older_than)
35
+ @limit = limit&.to_i
36
+ @estimate_sizes = estimate_sizes
37
+ end
38
+
39
+ def call
40
+ result = PlanResult.new(uploader_name: uploader_name)
41
+ each_candidate { |candidate| result.add(candidate) }
42
+ result
43
+ end
44
+
45
+ def each_candidate
46
+ return enum_for(:each_candidate) unless block_given?
47
+
48
+ count = 0
49
+ mounts.each do |mount|
50
+ each_record(mount) do |record|
51
+ candidates_for(record, mount).each do |candidate|
52
+ yield candidate
53
+ count += 1
54
+ return if limit && count >= limit
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def mounts
63
+ if model_name && mounted_as
64
+ [ArchiveStorage.configuration.find_mount(model_name, mounted_as) ||
65
+ MountConfig.new(model_name, mounted_as, uploader: uploader_name)]
66
+ else
67
+ ArchiveStorage.configuration.mounts.select do |mount|
68
+ mount.matches_uploader?(uploader_name)
69
+ end
70
+ end
71
+ end
72
+
73
+ def each_record(mount)
74
+ klass = mount.model_class
75
+ scope = scoped_records_for(klass, mount)
76
+
77
+ if klass.respond_to?(:column_names) && klass.column_names.include?(mount.mounted_as.to_s)
78
+ scope = scope.where.not(mount.mounted_as => [nil, ""])
79
+ end
80
+
81
+ scope.find_each(batch_size: ArchiveStorage.configuration.default_batch_size) do |record|
82
+ yield record
83
+ end
84
+ end
85
+
86
+ def candidates_for(record, mount)
87
+ return [] if too_young?(record)
88
+
89
+ uploader = record.public_send(mount.mounted_as)
90
+ identifier = identifier_for(record, uploader, mount)
91
+ return [] if identifier.nil? || identifier == ""
92
+
93
+ policy = policy_for(record, mount, uploader)
94
+ return [] unless policy
95
+
96
+ uploaders_for(uploader, policy).filter_map do |mounted_uploader|
97
+ candidate_for_uploader(record, mount, mounted_uploader, identifier, policy)
98
+ end
99
+ end
100
+
101
+ def scoped_records_for(klass, mount)
102
+ scope = klass.all
103
+ policy = ArchiveStorage.policy_for_mount(klass, mount.mounted_as) || uploader_policy_for(mount)
104
+
105
+ policy ? policy.apply_rule_scopes(scope) : scope
106
+ end
107
+
108
+ def uploader_policy_for(mount)
109
+ uploader_class = mount.uploader_class
110
+ uploader_class.archive_storage_policy if uploader_class.respond_to?(:archive_storage_policy)
111
+ rescue NameError
112
+ nil
113
+ end
114
+
115
+ def policy_for(record, mount, uploader)
116
+ mount.policy ||
117
+ ArchiveStorage.policy_for_mount(record.class, mount.mounted_as) ||
118
+ ArchiveStorage.policy_for_uploader(uploader)
119
+ end
120
+
121
+ def candidate_for_uploader(record, mount, uploader, identifier, policy)
122
+ storage_key = uploader.store_path(identifier)
123
+ current_storage = ArchiveStorage.registry.current_storage_for(
124
+ uploader,
125
+ identifier: identifier,
126
+ storage_key: storage_key,
127
+ default: policy.primary_storage_key
128
+ )
129
+ target_storage = policy.target_storage_for(record)
130
+ return nil unless target_storage
131
+ return nil if current_storage.to_sym == target_storage.to_sym
132
+
133
+ metadata = estimate_metadata(target_storage, policy.primary_storage_key, storage_key)
134
+
135
+ Candidate.new(
136
+ record: record,
137
+ mounted_as: mount.mounted_as,
138
+ uploader: uploader,
139
+ identifier: identifier,
140
+ storage_key: storage_key,
141
+ source_storage_key: storage_key,
142
+ target_storage_key: storage_key,
143
+ current_storage: current_storage,
144
+ target_storage: target_storage,
145
+ byte_size: metadata&.byte_size,
146
+ content_type: metadata&.content_type
147
+ )
148
+ end
149
+
150
+ def uploaders_for(uploader, policy)
151
+ uploaders = [uploader]
152
+ return uploaders unless uploader.respond_to?(:versions)
153
+
154
+ version_names = policy.selected_versions
155
+ version_names ||= uploader.versions.keys if policy.include_versions
156
+ return uploaders unless version_names
157
+
158
+ version_names.each do |name|
159
+ next unless uploader.versions.key?(name.to_sym)
160
+ next if uploader.respond_to?(:version_active?) && !uploader.version_active?(name)
161
+
162
+ uploaders << uploader.versions.fetch(name.to_sym)
163
+ end
164
+
165
+ uploaders
166
+ end
167
+
168
+ def too_young?(record)
169
+ return false unless older_than
170
+
171
+ timestamp = record.created_at if record.respond_to?(:created_at)
172
+ timestamp && timestamp > Time.now - older_than
173
+ end
174
+
175
+ def identifier_for(record, uploader, mount)
176
+ return uploader.identifier if uploader.respond_to?(:identifier) && uploader.identifier
177
+ return record.public_send(mount.mounted_as) if record.respond_to?(mount.mounted_as)
178
+
179
+ nil
180
+ end
181
+
182
+ def estimate_metadata(_target_storage, source_storage, storage_key)
183
+ return nil unless estimate_sizes
184
+
185
+ ArchiveStorage.adapter(source_storage).head(storage_key)
186
+ rescue StandardError
187
+ nil
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "storage_rule"
4
+
5
+ module ArchiveStorage
6
+ class Policy
7
+ attr_accessor :primary_storage,
8
+ :rules,
9
+ :read_fallbacks,
10
+ :delete_source_delay,
11
+ :delete_requires_verification,
12
+ :include_versions,
13
+ :selected_versions,
14
+ :timestamp_attribute
15
+
16
+ def initialize
17
+ @rules = []
18
+ @read_fallbacks = []
19
+ @delete_source_delay = nil
20
+ @delete_requires_verification = true
21
+ @include_versions = false
22
+ @selected_versions = nil
23
+ @timestamp_attribute = :created_at
24
+ end
25
+
26
+ def primary_storage_key
27
+ primary_storage&.storage_key
28
+ end
29
+
30
+ def target_storage_for(record, now: Time.now)
31
+ eligible_rules = rules.select do |rule|
32
+ rule.eligible?(record, now: now, timestamp_attribute: timestamp_attribute)
33
+ end
34
+
35
+ eligible_rules.last&.storage_key
36
+ end
37
+
38
+ def apply_rule_scopes(scope)
39
+ scoped_rules = rules.select(&:scoped?)
40
+ return scope if scoped_rules.empty?
41
+
42
+ relations = scoped_rules.map { |rule| rule.apply_scope(scope) }
43
+ relations.reduce do |combined, relation|
44
+ combined.respond_to?(:or) ? combined.or(relation) : combined
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "policy"
4
+ require_relative "storage_rule"
5
+ require_relative "errors"
6
+
7
+ module ArchiveStorage
8
+ class PolicyBuilder
9
+ def self.build(&block)
10
+ new.tap { |builder| builder.instance_eval(&block) }.tap(&:validate!).policy
11
+ end
12
+
13
+ attr_reader :policy
14
+
15
+ def initialize
16
+ @policy = Policy.new
17
+ end
18
+
19
+ def validate!
20
+ raise ConfigurationError, "archive_storage requires a primary storage" unless policy.primary_storage_key
21
+ end
22
+
23
+ def primary(name)
24
+ policy.primary_storage = StorageRule.new(:primary, name)
25
+ end
26
+
27
+ alias hot primary
28
+
29
+ def warm(name, **options)
30
+ rule(:warm, name, **options)
31
+ end
32
+
33
+ def archive(name, **options)
34
+ rule(:archive, name, **options)
35
+ end
36
+
37
+ def storage(name, **options)
38
+ rule(:storage, name, **options)
39
+ end
40
+
41
+ def rule(role, name, **options)
42
+ policy.rules << StorageRule.new(
43
+ role,
44
+ name,
45
+ after: options[:after],
46
+ condition: options[:if],
47
+ scope: options[:scope]
48
+ )
49
+ end
50
+
51
+ def read_fallbacks(*names)
52
+ policy.read_fallbacks = names.flatten.compact.map(&:to_sym)
53
+ end
54
+
55
+ def delete_source_after(verification: true, delay:)
56
+ policy.delete_requires_verification = verification
57
+ policy.delete_source_delay = delay
58
+ end
59
+
60
+ def include_versions(value = true)
61
+ policy.include_versions = value
62
+ end
63
+
64
+ def versions(*names)
65
+ policy.selected_versions = names.flatten.compact.map(&:to_sym)
66
+ end
67
+
68
+ def timestamp_attribute(name)
69
+ policy.timestamp_attribute = name.to_sym
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module ArchiveStorage
6
+ class Railtie < ::Rails::Railtie
7
+ initializer "archive_storage.active_record" do
8
+ ActiveSupport.on_load(:active_record) do
9
+ extend ArchiveStorage::Model
10
+ end
11
+ end
12
+
13
+ initializer "archive_storage.install_scheduled_jobs" do |app|
14
+ app.config.after_initialize do
15
+ ArchiveStorage.install_scheduled_jobs!(rails_config: app.config)
16
+ end
17
+ end
18
+
19
+ rake_tasks do
20
+ load File.expand_path("tasks.rake", __dir__)
21
+ end
22
+ end
23
+ end