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.
- checksums.yaml +7 -0
- data/README.md +348 -0
- data/archive_storage.gemspec +42 -0
- data/lib/archive_storage/adapters/filesystem.rb +135 -0
- data/lib/archive_storage/adapters/memory.rb +101 -0
- data/lib/archive_storage/adapters/metadata.rb +23 -0
- data/lib/archive_storage/adapters/s3.rb +186 -0
- data/lib/archive_storage/configuration.rb +115 -0
- data/lib/archive_storage/duration_parser.rb +26 -0
- data/lib/archive_storage/enqueuer.rb +29 -0
- data/lib/archive_storage/errors.rb +9 -0
- data/lib/archive_storage/jobs/migration_job.rb +28 -0
- data/lib/archive_storage/jobs/queue_job.rb +65 -0
- data/lib/archive_storage/jobs/sidekiq_migration_worker.rb +28 -0
- data/lib/archive_storage/jobs/sidekiq_queue_worker.rb +65 -0
- data/lib/archive_storage/migration_rate.rb +16 -0
- data/lib/archive_storage/migrator.rb +151 -0
- data/lib/archive_storage/model.rb +35 -0
- data/lib/archive_storage/models/file_record.rb +26 -0
- data/lib/archive_storage/mount_config.rb +50 -0
- data/lib/archive_storage/plan_result.rb +61 -0
- data/lib/archive_storage/planner.rb +190 -0
- data/lib/archive_storage/policy.rb +48 -0
- data/lib/archive_storage/policy_builder.rb +72 -0
- data/lib/archive_storage/railtie.rb +23 -0
- data/lib/archive_storage/registry.rb +109 -0
- data/lib/archive_storage/schedule_config.rb +79 -0
- data/lib/archive_storage/scheduler.rb +93 -0
- data/lib/archive_storage/storage.rb +91 -0
- data/lib/archive_storage/storage_config.rb +37 -0
- data/lib/archive_storage/storage_rule.rb +57 -0
- data/lib/archive_storage/stored_file.rb +94 -0
- data/lib/archive_storage/tasks.rake +82 -0
- data/lib/archive_storage/verification_result.rb +11 -0
- data/lib/archive_storage/verifier.rb +144 -0
- data/lib/archive_storage/version.rb +5 -0
- data/lib/archive_storage.rb +148 -0
- data/lib/generators/archive_storage/install_generator.rb +28 -0
- data/lib/generators/archive_storage/templates/create_archive_storage_files.rb +53 -0
- 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
|