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,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "models/file_record"
|
|
5
|
+
|
|
6
|
+
module ArchiveStorage
|
|
7
|
+
class Registry
|
|
8
|
+
def available?
|
|
9
|
+
defined?(::ActiveRecord::Base) &&
|
|
10
|
+
::ActiveRecord::Base.connected? &&
|
|
11
|
+
record_class.table_exists?
|
|
12
|
+
rescue StandardError
|
|
13
|
+
false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def find_for_uploader(uploader, identifier:, storage_key:)
|
|
17
|
+
return nil unless available?
|
|
18
|
+
return nil unless uploader_identity_available?(uploader)
|
|
19
|
+
|
|
20
|
+
record_class.find_by(
|
|
21
|
+
record_type: uploader.model.class.name,
|
|
22
|
+
record_id: uploader.model.id,
|
|
23
|
+
mounted_as: uploader.mounted_as.to_s,
|
|
24
|
+
identifier: identifier.to_s,
|
|
25
|
+
storage_key: storage_key.to_s
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def current_storage_for(uploader, identifier:, storage_key:, default:)
|
|
30
|
+
find_for_uploader(
|
|
31
|
+
uploader,
|
|
32
|
+
identifier: identifier,
|
|
33
|
+
storage_key: storage_key
|
|
34
|
+
)&.current_storage&.to_sym || default
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def upsert_for_uploader(uploader, identifier:, storage_key:, current_storage:, metadata: {})
|
|
38
|
+
return nil unless available?
|
|
39
|
+
return nil unless uploader_identity_available?(uploader)
|
|
40
|
+
|
|
41
|
+
record = record_class.find_or_initialize_by(
|
|
42
|
+
record_type: uploader.model.class.name,
|
|
43
|
+
record_id: uploader.model.id,
|
|
44
|
+
mounted_as: uploader.mounted_as.to_s,
|
|
45
|
+
identifier: identifier.to_s,
|
|
46
|
+
storage_key: storage_key.to_s
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
record.uploader = uploader.class.name
|
|
50
|
+
record.current_storage = current_storage.to_s
|
|
51
|
+
record.byte_size ||= metadata[:byte_size]
|
|
52
|
+
record.content_type ||= metadata[:content_type]
|
|
53
|
+
record.checksum ||= metadata[:checksum]
|
|
54
|
+
record.save!
|
|
55
|
+
record
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def claim_candidate(candidate)
|
|
59
|
+
raise RegistryUnavailableError, "archive_storage_files table is not available" unless available?
|
|
60
|
+
|
|
61
|
+
record = record_class.find_or_initialize_by(
|
|
62
|
+
record_type: candidate.record.class.name,
|
|
63
|
+
record_id: candidate.record.id,
|
|
64
|
+
mounted_as: candidate.mounted_as.to_s,
|
|
65
|
+
identifier: candidate.identifier.to_s,
|
|
66
|
+
storage_key: candidate.storage_key.to_s
|
|
67
|
+
)
|
|
68
|
+
return nil unless claimable?(record)
|
|
69
|
+
|
|
70
|
+
record.uploader = candidate.uploader.class.name
|
|
71
|
+
record.current_storage = candidate.current_storage.to_s
|
|
72
|
+
record.source_storage = candidate.current_storage.to_s
|
|
73
|
+
record.target_storage = candidate.target_storage.to_s
|
|
74
|
+
record.source_storage_key = candidate.source_storage_key.to_s if record.respond_to?(:source_storage_key=)
|
|
75
|
+
record.target_storage_key = candidate.target_storage_key.to_s if record.respond_to?(:target_storage_key=)
|
|
76
|
+
record.enqueued_at = Time.now if record.respond_to?(:enqueued_at=)
|
|
77
|
+
record.source_delete_pending = false if record.respond_to?(:source_delete_pending=) && record.new_record?
|
|
78
|
+
record.byte_size ||= candidate.byte_size
|
|
79
|
+
record.content_type ||= candidate.content_type
|
|
80
|
+
record.save!
|
|
81
|
+
record
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
alias ensure_for_candidate claim_candidate
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def record_class
|
|
89
|
+
ArchiveStorage.configuration.registry_class
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def claimable?(record)
|
|
93
|
+
return false if record.respond_to?(:migrated_at) && record.migrated_at
|
|
94
|
+
return true unless record.respond_to?(:enqueued_at)
|
|
95
|
+
return true unless record.enqueued_at
|
|
96
|
+
|
|
97
|
+
record.enqueued_at <= Time.now - ArchiveStorage.configuration.enqueue_claim_ttl
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def uploader_identity_available?(uploader)
|
|
101
|
+
uploader.respond_to?(:model) &&
|
|
102
|
+
uploader.model &&
|
|
103
|
+
uploader.model.respond_to?(:id) &&
|
|
104
|
+
uploader.model.id &&
|
|
105
|
+
uploader.respond_to?(:mounted_as) &&
|
|
106
|
+
uploader.mounted_as
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "migration_rate"
|
|
5
|
+
|
|
6
|
+
module ArchiveStorage
|
|
7
|
+
class ScheduleConfig
|
|
8
|
+
attr_reader :name, :cron, :model, :mounted_as, :uploaders, :migration_rate
|
|
9
|
+
|
|
10
|
+
def initialize(name, cron:, model: nil, mounted_as: nil, uploaders: [], migration_rate: nil)
|
|
11
|
+
@name = name.to_sym
|
|
12
|
+
@cron = cron
|
|
13
|
+
@model = model
|
|
14
|
+
@mounted_as = mounted_as&.to_sym
|
|
15
|
+
@uploaders = uploaders.flatten.compact.map(&:to_s)
|
|
16
|
+
@migration_rate = MigrationRate.new(migration_rate) if migration_rate
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def entry_name
|
|
20
|
+
name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def job_arguments
|
|
24
|
+
{}.tap do |args|
|
|
25
|
+
if mount_schedule?
|
|
26
|
+
args[:model] = model_name
|
|
27
|
+
args[:mounted_as] = mounted_as.to_s
|
|
28
|
+
else
|
|
29
|
+
args[:uploaders] = uploaders
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
args[:migration_rate] = migration_rate.max_files_per_run if migration_rate
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def validate!
|
|
37
|
+
raise ConfigurationError, "archive_storage schedule #{name.inspect} requires cron" if cron.nil? || cron == ""
|
|
38
|
+
return if mount_schedule? || uploaders.any?
|
|
39
|
+
|
|
40
|
+
raise ConfigurationError, "archive_storage schedule #{name.inspect} requires model/mounted_as or uploader"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def good_job_entry
|
|
44
|
+
validate!
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
cron: cron,
|
|
48
|
+
class: "ArchiveStorage::Jobs::QueueJob",
|
|
49
|
+
set: { queue: ArchiveStorage.configuration.schedule_queue },
|
|
50
|
+
args: [job_arguments]
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def sidekiq_cron_entry
|
|
55
|
+
validate!
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
"cron" => cron,
|
|
59
|
+
"class" => "ArchiveStorage::Jobs::SidekiqQueueWorker",
|
|
60
|
+
"queue" => ArchiveStorage.configuration.schedule_queue.to_s,
|
|
61
|
+
"args" => [stringify_keys(job_arguments)]
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def mount_schedule?
|
|
68
|
+
model && mounted_as
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def model_name
|
|
72
|
+
model.respond_to?(:name) ? model.name : model.to_s
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def stringify_keys(hash)
|
|
76
|
+
hash.transform_keys(&:to_s)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArchiveStorage
|
|
4
|
+
class << self
|
|
5
|
+
def install_scheduled_jobs!(rails_config: nil)
|
|
6
|
+
if configuration.job_backend.to_sym == :sidekiq
|
|
7
|
+
install_sidekiq_schedules!
|
|
8
|
+
else
|
|
9
|
+
return false unless good_job_available?
|
|
10
|
+
|
|
11
|
+
install_good_job_cron!(rails_config: rails_config)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def install_good_job_cron!(rails_config: nil)
|
|
16
|
+
entries = good_job_cron
|
|
17
|
+
return false if entries.empty?
|
|
18
|
+
|
|
19
|
+
config = rails_config || rails_application_config
|
|
20
|
+
return false unless config&.respond_to?(:good_job)
|
|
21
|
+
|
|
22
|
+
good_job_config = config.good_job
|
|
23
|
+
good_job_config.cron = cron_hash(good_job_config.cron).merge(entries)
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def install_sidekiq_schedules!
|
|
28
|
+
install_sidekiq_cron! || install_sidekiq_scheduler!
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def install_sidekiq_cron!
|
|
32
|
+
require "sidekiq"
|
|
33
|
+
require "sidekiq-cron"
|
|
34
|
+
require_relative "jobs/sidekiq_queue_worker"
|
|
35
|
+
|
|
36
|
+
::Sidekiq.configure_server do |config|
|
|
37
|
+
config.on(:startup) do
|
|
38
|
+
entries = ArchiveStorage.sidekiq_cron
|
|
39
|
+
::Sidekiq::Cron::Job.load_from_hash(entries) if entries.any?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
true
|
|
44
|
+
rescue LoadError
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def install_sidekiq_scheduler!
|
|
49
|
+
require "sidekiq"
|
|
50
|
+
require "sidekiq-scheduler"
|
|
51
|
+
require_relative "jobs/sidekiq_queue_worker"
|
|
52
|
+
|
|
53
|
+
::Sidekiq.configure_server do |config|
|
|
54
|
+
config.on(:startup) do
|
|
55
|
+
entries = ArchiveStorage.sidekiq_cron
|
|
56
|
+
entries.each { |name, entry| ::Sidekiq.set_schedule(name, entry) }
|
|
57
|
+
reload_sidekiq_scheduler if entries.any?
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
true
|
|
62
|
+
rescue LoadError
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def cron_hash(value)
|
|
69
|
+
return {} if value.nil?
|
|
70
|
+
return value.to_h if value.respond_to?(:to_h)
|
|
71
|
+
|
|
72
|
+
{}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def good_job_available?
|
|
76
|
+
defined?(::GoodJob) || Gem.loaded_specs.key?("good_job")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def reload_sidekiq_scheduler
|
|
80
|
+
if defined?(::SidekiqScheduler::Scheduler)
|
|
81
|
+
::SidekiqScheduler::Scheduler.instance.reload_schedule!
|
|
82
|
+
elsif defined?(::Sidekiq::Scheduler) && ::Sidekiq::Scheduler.respond_to?(:reload_schedule!)
|
|
83
|
+
::Sidekiq::Scheduler.reload_schedule!
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def rails_application_config
|
|
88
|
+
return nil unless defined?(::Rails) && ::Rails.respond_to?(:application)
|
|
89
|
+
|
|
90
|
+
::Rails.application&.config
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "stored_file"
|
|
4
|
+
|
|
5
|
+
module ArchiveStorage
|
|
6
|
+
class Storage
|
|
7
|
+
attr_reader :uploader, :identifier
|
|
8
|
+
|
|
9
|
+
def initialize(uploader)
|
|
10
|
+
@uploader = uploader
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def store!(file)
|
|
14
|
+
identifier = identifier_for(file)
|
|
15
|
+
@identifier = identifier
|
|
16
|
+
storage_key = uploader.store_path(identifier)
|
|
17
|
+
primary_storage = policy.primary_storage_key
|
|
18
|
+
adapter = ArchiveStorage.adapter(primary_storage)
|
|
19
|
+
|
|
20
|
+
adapter.upload(storage_key, file)
|
|
21
|
+
metadata = safe_head(adapter, storage_key)
|
|
22
|
+
|
|
23
|
+
ArchiveStorage.registry.upsert_for_uploader(
|
|
24
|
+
uploader,
|
|
25
|
+
identifier: identifier,
|
|
26
|
+
storage_key: storage_key,
|
|
27
|
+
current_storage: primary_storage,
|
|
28
|
+
metadata: {
|
|
29
|
+
byte_size: metadata&.byte_size,
|
|
30
|
+
content_type: metadata&.content_type,
|
|
31
|
+
checksum: metadata&.etag
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
StoredFile.new(uploader, identifier, storage_key: storage_key)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def retrieve!(identifier)
|
|
39
|
+
@identifier = identifier
|
|
40
|
+
StoredFile.new(
|
|
41
|
+
uploader,
|
|
42
|
+
identifier,
|
|
43
|
+
storage_key: uploader.store_path(identifier)
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def cache!(new_file)
|
|
48
|
+
file_cache_storage.cache!(new_file)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def retrieve_from_cache!(identifier)
|
|
52
|
+
file_cache_storage.retrieve_from_cache!(identifier)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def delete_dir!(path)
|
|
56
|
+
file_cache_storage.delete_dir!(path)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def clean_cache!(seconds)
|
|
60
|
+
file_cache_storage.clean_cache!(seconds)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def identifier_for(file)
|
|
66
|
+
return file.filename if file.respond_to?(:filename) && file.filename
|
|
67
|
+
return uploader.filename if uploader.respond_to?(:filename) && uploader.filename
|
|
68
|
+
|
|
69
|
+
raise ArgumentError, "cannot infer upload identifier for archive storage upload"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def policy
|
|
73
|
+
ArchiveStorage.policy_for_uploader(uploader) ||
|
|
74
|
+
raise(ConfigurationError, "archive_storage policy is not configured for #{uploader.class.name}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def safe_head(adapter, storage_key)
|
|
78
|
+
adapter.head(storage_key)
|
|
79
|
+
rescue StandardError
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def file_cache_storage
|
|
84
|
+
unless defined?(::CarrierWave::Storage::File)
|
|
85
|
+
raise ConfigurationError, "CarrierWave file storage is required for archive_storage cache storage"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
@file_cache_storage ||= ::CarrierWave::Storage::File.new(uploader)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArchiveStorage
|
|
4
|
+
class StorageConfig
|
|
5
|
+
attr_accessor :name,
|
|
6
|
+
:provider,
|
|
7
|
+
:endpoint,
|
|
8
|
+
:bucket,
|
|
9
|
+
:access_key_id,
|
|
10
|
+
:secret_access_key,
|
|
11
|
+
:region,
|
|
12
|
+
:path_style,
|
|
13
|
+
:public,
|
|
14
|
+
:public_host,
|
|
15
|
+
:root_path,
|
|
16
|
+
:base_url,
|
|
17
|
+
:adapter,
|
|
18
|
+
:options
|
|
19
|
+
|
|
20
|
+
def initialize(name)
|
|
21
|
+
@name = name.to_sym
|
|
22
|
+
@provider = :s3
|
|
23
|
+
@region = "us-east-1"
|
|
24
|
+
@path_style = false
|
|
25
|
+
@public = false
|
|
26
|
+
@options = {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def path_style?
|
|
30
|
+
!!path_style
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def public?
|
|
34
|
+
!!public
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArchiveStorage
|
|
4
|
+
class StorageRule
|
|
5
|
+
attr_reader :role, :storage_key, :after, :condition, :scope
|
|
6
|
+
|
|
7
|
+
def initialize(role, storage_key, after: nil, condition: nil, scope: nil)
|
|
8
|
+
@role = role.to_sym
|
|
9
|
+
@storage_key = storage_key.to_sym
|
|
10
|
+
@after = after
|
|
11
|
+
@condition = condition
|
|
12
|
+
@scope = scope
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def eligible?(record, now:, timestamp_attribute:)
|
|
16
|
+
old_enough?(record, now: now, timestamp_attribute: timestamp_attribute) &&
|
|
17
|
+
condition_matches?(record)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def scoped?
|
|
21
|
+
!scope.nil?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def apply_scope(relation)
|
|
25
|
+
case scope
|
|
26
|
+
when Symbol, String
|
|
27
|
+
relation.public_send(scope)
|
|
28
|
+
when Proc
|
|
29
|
+
scope.arity.zero? ? relation.instance_exec(&scope) : scope.call(relation)
|
|
30
|
+
else
|
|
31
|
+
relation.respond_to?(:merge) ? relation.merge(scope) : scope
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def old_enough?(record, now:, timestamp_attribute:)
|
|
38
|
+
return true unless after
|
|
39
|
+
return false unless record
|
|
40
|
+
|
|
41
|
+
timestamp = record.public_send(timestamp_attribute) if record.respond_to?(timestamp_attribute)
|
|
42
|
+
return false unless timestamp
|
|
43
|
+
|
|
44
|
+
timestamp <= now - seconds_after
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def condition_matches?(record)
|
|
48
|
+
return true unless condition
|
|
49
|
+
|
|
50
|
+
condition.arity.zero? ? condition.call : condition.call(record)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def seconds_after
|
|
54
|
+
after.respond_to?(:to_i) ? after.to_i : after
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
5
|
+
module ArchiveStorage
|
|
6
|
+
class StoredFile
|
|
7
|
+
attr_reader :uploader, :identifier, :storage_key
|
|
8
|
+
|
|
9
|
+
def initialize(uploader, identifier, storage_key: nil)
|
|
10
|
+
@uploader = uploader
|
|
11
|
+
@identifier = identifier.to_s
|
|
12
|
+
@storage_key = storage_key || uploader.store_path(identifier)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def path
|
|
16
|
+
storage_key
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def filename
|
|
20
|
+
::File.basename(identifier)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def url(*args, **options)
|
|
24
|
+
positional_options = args.last.is_a?(Hash) ? args.pop : {}
|
|
25
|
+
url_options = positional_options.merge(options)
|
|
26
|
+
|
|
27
|
+
with_read_adapter { |adapter| adapter.url(storage_key, **url_options) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def read
|
|
31
|
+
with_read_adapter { |adapter| adapter.read(storage_key) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def size
|
|
35
|
+
with_read_adapter { |adapter| adapter.head(storage_key).byte_size }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def content_type
|
|
39
|
+
with_read_adapter { |adapter| adapter.head(storage_key).content_type }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def exists?
|
|
43
|
+
candidate_storages.any? do |storage|
|
|
44
|
+
ArchiveStorage.adapter(storage).exists?(storage_key)
|
|
45
|
+
rescue *fallback_errors
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def delete
|
|
51
|
+
adapter_for(current_storage).delete(storage_key)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def current_storage
|
|
55
|
+
ArchiveStorage.registry.current_storage_for(
|
|
56
|
+
uploader,
|
|
57
|
+
identifier: identifier,
|
|
58
|
+
storage_key: storage_key,
|
|
59
|
+
default: policy.primary_storage_key
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def candidate_storages
|
|
64
|
+
([current_storage] + policy.read_fallbacks + [policy.primary_storage_key]).compact.uniq
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def with_read_adapter
|
|
70
|
+
last_error = nil
|
|
71
|
+
|
|
72
|
+
candidate_storages.each do |storage|
|
|
73
|
+
return yield adapter_for(storage)
|
|
74
|
+
rescue *fallback_errors => error
|
|
75
|
+
last_error = error
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
raise(last_error || NotFoundError, "object #{storage_key.inspect} not found")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def adapter_for(storage)
|
|
82
|
+
ArchiveStorage.adapter(storage)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def policy
|
|
86
|
+
ArchiveStorage.policy_for_uploader(uploader) ||
|
|
87
|
+
raise(ConfigurationError, "archive_storage policy is not configured for #{uploader.class.name}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def fallback_errors
|
|
91
|
+
ArchiveStorage.configuration.fallback_on_read_errors
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "archive_storage/planner"
|
|
4
|
+
require "archive_storage/migrator"
|
|
5
|
+
require "archive_storage/jobs/migration_job"
|
|
6
|
+
|
|
7
|
+
namespace :archive_storage do
|
|
8
|
+
desc "Dry-run ArchiveStorage migration"
|
|
9
|
+
task plan: :environment do
|
|
10
|
+
planner = ArchiveStorage::Planner.new(
|
|
11
|
+
uploader: ENV["UPLOADER"],
|
|
12
|
+
model: ENV["MODEL"],
|
|
13
|
+
mounted_as: ENV["MOUNT"],
|
|
14
|
+
older_than: ENV["OLDER_THAN"],
|
|
15
|
+
limit: ENV["LIMIT"],
|
|
16
|
+
estimate_sizes: ENV.fetch("ESTIMATE_SIZES", "true") != "false"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
puts planner.call.to_text
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "Enqueue or run ArchiveStorage migration"
|
|
23
|
+
task migrate: :environment do
|
|
24
|
+
planner = ArchiveStorage::Planner.new(
|
|
25
|
+
uploader: ENV["UPLOADER"],
|
|
26
|
+
model: ENV["MODEL"],
|
|
27
|
+
mounted_as: ENV["MOUNT"],
|
|
28
|
+
older_than: ENV["OLDER_THAN"],
|
|
29
|
+
limit: ENV["LIMIT"],
|
|
30
|
+
estimate_sizes: true
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
inline = ENV.fetch("INLINE", "false") == "true"
|
|
34
|
+
count = ArchiveStorage::Migrator
|
|
35
|
+
.new(planner: planner)
|
|
36
|
+
.enqueue_or_migrate!(inline: inline)
|
|
37
|
+
|
|
38
|
+
puts "#{inline ? "Migrated" : "Enqueued"} #{count} files"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc "Enqueue ArchiveStorage migration jobs"
|
|
42
|
+
task enqueue: :migrate
|
|
43
|
+
|
|
44
|
+
desc "Verify migrated ArchiveStorage files"
|
|
45
|
+
task verify: :environment do
|
|
46
|
+
scope = ArchiveStorage.configuration.registry_class.where.not(migrated_at: nil)
|
|
47
|
+
count = 0
|
|
48
|
+
|
|
49
|
+
scope.find_each do |file_record|
|
|
50
|
+
ArchiveStorage::Migrator.new.verify_record!(file_record)
|
|
51
|
+
count += 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
puts "Verified #{count} files"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
desc "Delete verified source copies after cleanup delay"
|
|
58
|
+
task cleanup_source: :environment do
|
|
59
|
+
scope = ArchiveStorage.configuration.registry_class.pending_cleanup
|
|
60
|
+
count = 0
|
|
61
|
+
|
|
62
|
+
scope.find_each do |file_record|
|
|
63
|
+
count += 1 if ArchiveStorage::Migrator.new.cleanup_source!(file_record)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
puts "Deleted #{count} source copies"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
desc "Show ArchiveStorage migration status"
|
|
70
|
+
task status: :environment do
|
|
71
|
+
records = ArchiveStorage.configuration.registry_class
|
|
72
|
+
|
|
73
|
+
puts "ArchiveStorage Status"
|
|
74
|
+
puts ""
|
|
75
|
+
puts "Tracked: #{records.count}"
|
|
76
|
+
puts "Migrated: #{records.where.not(migrated_at: nil).count}"
|
|
77
|
+
puts "Verified: #{records.where.not(verified_at: nil).count}"
|
|
78
|
+
puts "Pending cleanup: #{records.pending_cleanup.count}"
|
|
79
|
+
puts "Source deleted: #{records.where.not(source_deleted_at: nil).count}"
|
|
80
|
+
puts "Failed: #{records.where.not(last_error: [nil, ""]).count}"
|
|
81
|
+
end
|
|
82
|
+
end
|