boring-backup 0.5.0 → 0.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +19 -6
- data/lib/boring_backup/cli.rb +2 -14
- data/lib/boring_backup/commands/backup.rb +7 -5
- data/lib/boring_backup/commands/doctor.rb +24 -38
- data/lib/boring_backup/configuration.rb +15 -5
- data/lib/boring_backup/engine.rb +46 -0
- data/lib/boring_backup/notifiers/sentinel.rb +4 -3
- data/lib/boring_backup/stores/r2.rb +24 -0
- data/lib/boring_backup/stores/s3.rb +30 -28
- data/lib/boring_backup/stores/store.rb +1 -1
- data/lib/boring_backup/version.rb +1 -1
- data/lib/boring_backup.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 343fecddfbccfc03c5216673a667596b7e6af0ccd4e7edc6c03e864d80f7727b
|
|
4
|
+
data.tar.gz: cff420aa2b49fcae03d06d03c95d6f5e4d5813b112e66464b1829ef4c069ec0b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2f9c315742cb0c19493947d542e7657af569eb1ddc78890839cf239300276707e3b060ff9d02161f3535c5e687aa8facf14d684c859cb14d2d7187af69344a78
|
|
7
|
+
data.tar.gz: 1dbff91a051d57ab40703c9e760c435789a05c8b671742b795714847dd5385ac4308a803b6f4c36e6fa99f4d7cce5e63a4404ce9fbc56090d028f454c6181016
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.6.1] - 2026-08-12
|
|
4
|
+
|
|
5
|
+
- The `:r2` store now defaults to the `standard` storage class instead of inheriting `standard_ia` from
|
|
6
|
+
`:s3`. R2's free tier does not cover Infrequent Access, and its per-request charges round up to a full
|
|
7
|
+
million, so a single backup a month cost $9.00.
|
|
8
|
+
|
|
9
|
+
## [0.6.0] - 2026-07-23
|
|
10
|
+
|
|
11
|
+
- `bb doctor` - improved scheduler checks (now supports `sidekiq-crosidekiq-cron`)
|
|
12
|
+
- Support `PG*` env vars
|
|
13
|
+
- Support for Cloudflare R2 using their S3-compatible API
|
|
14
|
+
|
|
3
15
|
## [0.5.0] - 2026-07-14
|
|
4
16
|
|
|
5
17
|
- **`aws-sdk-s3` is no longer a dependency of the gem.** apps using the `:s3` store must add `gem
|
data/README.md
CHANGED
|
@@ -21,12 +21,18 @@ gem install boring-backup
|
|
|
21
21
|
The gem requires `pg_dump` to be installed on the machine that is running it.
|
|
22
22
|
|
|
23
23
|
Storage backends bring their own dependencies, which are not installed by default. To use
|
|
24
|
-
the `:s3`
|
|
24
|
+
the `:s3` and `:r2` stores, add the AWS SDK to your `Gemfile` as well:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
bundle add aws-sdk-s3
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
Run the install command to set up the initializer:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bundle exec bb install
|
|
34
|
+
```
|
|
35
|
+
|
|
30
36
|
## Usage
|
|
31
37
|
|
|
32
38
|
### Automatic
|
|
@@ -66,9 +72,11 @@ anything to disk. Use any scheduler or even cron to run it periodically.
|
|
|
66
72
|
All configuration options can be edited in the initializer:
|
|
67
73
|
|
|
68
74
|
```rb
|
|
69
|
-
# config/initializers/
|
|
75
|
+
# config/initializers/boring_backup.rb
|
|
70
76
|
|
|
71
77
|
BoringBackup.configure do |config|
|
|
78
|
+
config.sentinel_key = '12345678-abcd-1234-abcd-abcdef123456'
|
|
79
|
+
|
|
72
80
|
config.register(:s3) do |store|
|
|
73
81
|
store.bucket = Settings.aws.bucket
|
|
74
82
|
store.region = Settings.aws.region
|
|
@@ -76,6 +84,13 @@ BoringBackup.configure do |config|
|
|
|
76
84
|
store.secret_access_key = Settings.aws.secret_access_key
|
|
77
85
|
end
|
|
78
86
|
|
|
87
|
+
config.register(:r2) do |store|
|
|
88
|
+
store.endpoint = Settings.r2.endpoint
|
|
89
|
+
store.bucket = Settings.r2.bucket
|
|
90
|
+
store.access_key_id = Settings.r2.access_key_id
|
|
91
|
+
store.secret_access_key = Settings.r2.secret_access_key
|
|
92
|
+
end
|
|
93
|
+
|
|
79
94
|
config.notifier :slack do |slack|
|
|
80
95
|
slack.webhook_url = 'https://hooks.slack.com/services/whatever'
|
|
81
96
|
end
|
|
@@ -108,12 +123,10 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/gmitre
|
|
|
108
123
|
|
|
109
124
|
## Wishlist
|
|
110
125
|
|
|
111
|
-
- [
|
|
126
|
+
- [X] S3-compatible backends
|
|
127
|
+
- [X] doctor command
|
|
112
128
|
- [ ] file backend
|
|
113
|
-
- [ ] email notifier
|
|
114
|
-
- [ ] SMS notifier
|
|
115
129
|
- [ ] restore command
|
|
116
|
-
- [ ] test command
|
|
117
130
|
- [ ] encryption
|
|
118
131
|
- [ ] delete stale
|
|
119
132
|
|
data/lib/boring_backup/cli.rb
CHANGED
|
@@ -140,7 +140,7 @@ module BoringBackup
|
|
|
140
140
|
def check_line(check, width)
|
|
141
141
|
mark, colour = CHECK_MARKS.fetch(check.status)
|
|
142
142
|
|
|
143
|
-
"#{pastel.decorate(mark, colour)} #{pastel.bold(check.name.ljust(width))} #{pastel.dim(check.detail.to_s)}"
|
|
143
|
+
"#{pastel.decorate(mark, colour)} #{pastel.bold(check.name.to_s.ljust(width))} #{pastel.dim(check.detail.to_s)}"
|
|
144
144
|
end
|
|
145
145
|
|
|
146
146
|
def doctor_verdict(result)
|
|
@@ -210,19 +210,12 @@ module BoringBackup
|
|
|
210
210
|
clear_line
|
|
211
211
|
end
|
|
212
212
|
|
|
213
|
-
def env_store?
|
|
214
|
-
!ENV["AWS_S3_BUCKET"].to_s.empty?
|
|
215
|
-
end
|
|
216
|
-
|
|
217
213
|
def configured_store_summary
|
|
218
|
-
return "S3 — s3://#{ENV["AWS_S3_BUCKET"]} (from ENV)" if env_store?
|
|
219
|
-
|
|
220
214
|
"S3 — #{INITIALIZER}" if File.exist?(INITIALIZER)
|
|
221
215
|
end
|
|
222
216
|
|
|
223
217
|
def configure_store
|
|
224
218
|
return if File.exist?(INITIALIZER)
|
|
225
|
-
return if env_store? && prompt.yes?("Use the S3 bucket already in ENV (#{ENV["AWS_S3_BUCKET"]})?")
|
|
226
219
|
|
|
227
220
|
prompt.select("Where should backups go?") do |menu|
|
|
228
221
|
menu.choice "S3 (or S3-compatible: R2, MinIO, Spaces)", :s3
|
|
@@ -236,12 +229,7 @@ module BoringBackup
|
|
|
236
229
|
|
|
237
230
|
def write_initializer(store)
|
|
238
231
|
unless store
|
|
239
|
-
|
|
240
|
-
say_status :identical, INITIALIZER, :blue
|
|
241
|
-
else
|
|
242
|
-
say_status :skip, "#{INITIALIZER} not needed — the S3 store reads its config from ENV", :blue
|
|
243
|
-
end
|
|
244
|
-
|
|
232
|
+
say_status :identical, INITIALIZER, :blue
|
|
245
233
|
return
|
|
246
234
|
end
|
|
247
235
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require "open3"
|
|
2
2
|
|
|
3
3
|
module BoringBackup::Commands
|
|
4
|
-
CommandResult = Struct.new(:error, :store_results, keyword_init: true) do
|
|
4
|
+
CommandResult = Struct.new(:error, :store_results, :engine, keyword_init: true) do
|
|
5
5
|
def success?
|
|
6
6
|
status == :success
|
|
7
7
|
end
|
|
@@ -42,6 +42,8 @@ module BoringBackup::Commands
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def execute
|
|
45
|
+
@engine = BoringBackup::Engine.info
|
|
46
|
+
|
|
45
47
|
perform_sanity_check!
|
|
46
48
|
|
|
47
49
|
commands = [config.dump_command]
|
|
@@ -57,7 +59,7 @@ module BoringBackup::Commands
|
|
|
57
59
|
store.backup!(@key, reader)
|
|
58
60
|
rescue => e
|
|
59
61
|
# *always* return a Result, even if unexpected. Add store name for debugging.
|
|
60
|
-
BoringBackup::Stores::Result.new(success: false, error: e, store: store.
|
|
62
|
+
BoringBackup::Stores::Result.new(success: false, error: e, store: store.name, key: @key)
|
|
61
63
|
ensure
|
|
62
64
|
reader.close
|
|
63
65
|
end
|
|
@@ -80,7 +82,7 @@ module BoringBackup::Commands
|
|
|
80
82
|
raise BoringBackup::DumpFailedError, "pipeline failed" unless wait_threads.all? { |t| t.value.success? }
|
|
81
83
|
end
|
|
82
84
|
|
|
83
|
-
CommandResult.new(store_results: @store_results)
|
|
85
|
+
CommandResult.new(store_results: @store_results, engine: @engine)
|
|
84
86
|
rescue BoringBackup::DumpFailedError => error
|
|
85
87
|
# The dump stream itself failed, so uploads that finished are truncated copies of a bad stream — delete them.
|
|
86
88
|
# Collect the results first: if copy_stream raised, the collection line above never ran. Sinks are already
|
|
@@ -89,9 +91,9 @@ module BoringBackup::Commands
|
|
|
89
91
|
|
|
90
92
|
cleanup_uploaded_stores!
|
|
91
93
|
|
|
92
|
-
CommandResult.new(error:, store_results: @store_results)
|
|
94
|
+
CommandResult.new(error:, store_results: @store_results, engine: @engine)
|
|
93
95
|
rescue => error
|
|
94
|
-
CommandResult.new(error:, store_results: @store_results)
|
|
96
|
+
CommandResult.new(error:, store_results: @store_results, engine: @engine)
|
|
95
97
|
end
|
|
96
98
|
|
|
97
99
|
private
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module BoringBackup::Commands
|
|
2
2
|
Check = Struct.new(:name, :status, :detail, keyword_init: true) do
|
|
3
3
|
def failed? = status == :fail
|
|
4
|
+
def ok? = status == :ok
|
|
4
5
|
end
|
|
5
6
|
|
|
6
7
|
DoctorResult = Struct.new(:checks, keyword_init: true) do
|
|
@@ -10,8 +11,8 @@ module BoringBackup::Commands
|
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
class Doctor
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
SOLID_QUEUE_SCHEDULE_FILE = "config/recurring.yml"
|
|
15
|
+
SIDEKIQ_CRON_SCHEDULE_FILE = "config/schedule.yml"
|
|
15
16
|
def self.execute = new.execute
|
|
16
17
|
|
|
17
18
|
def execute
|
|
@@ -21,7 +22,7 @@ module BoringBackup::Commands
|
|
|
21
22
|
database_resolved,
|
|
22
23
|
stores_registered,
|
|
23
24
|
*config.stores.flat_map { |store| store_checks(store) },
|
|
24
|
-
|
|
25
|
+
*scheduler_checks
|
|
25
26
|
]
|
|
26
27
|
|
|
27
28
|
DoctorResult.new(checks: checks.compact)
|
|
@@ -42,39 +43,27 @@ module BoringBackup::Commands
|
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
def pg_dump_path
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@pg_dump_path = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR)
|
|
48
|
-
.map { |dir| File.join(dir, "pg_dump") }
|
|
49
|
-
.find { |path| File.executable?(path) }
|
|
46
|
+
BoringBackup::Engine.pg_dump_path
|
|
50
47
|
end
|
|
51
48
|
|
|
52
49
|
def pg_dump_version
|
|
53
50
|
return unless pg_dump_path
|
|
54
51
|
|
|
55
|
-
client =
|
|
56
|
-
server =
|
|
52
|
+
client = BoringBackup::Engine.client_version
|
|
53
|
+
server = BoringBackup::Engine.server_version
|
|
57
54
|
|
|
58
55
|
return check("versions", :skip, "could not read server version") unless server
|
|
59
56
|
return check("versions", :skip, "could not read pg_dump version") unless client
|
|
60
57
|
|
|
61
|
-
if client >= server
|
|
58
|
+
if major(client) >= major(server)
|
|
62
59
|
check("versions", :ok, "pg_dump #{client}, server #{server}")
|
|
63
60
|
else
|
|
64
61
|
check("versions", :fail, "pg_dump #{client} is older than server #{server} — the dump may not restore")
|
|
65
62
|
end
|
|
66
63
|
end
|
|
67
64
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def server_version
|
|
73
|
-
return unless defined?(::ActiveRecord)
|
|
74
|
-
|
|
75
|
-
::ActiveRecord::Base.connection.select_value("SHOW server_version")
|
|
76
|
-
rescue
|
|
77
|
-
nil
|
|
65
|
+
def major(version)
|
|
66
|
+
Gem::Version.new(version).segments.first
|
|
78
67
|
end
|
|
79
68
|
|
|
80
69
|
def database_resolved
|
|
@@ -103,29 +92,26 @@ module BoringBackup::Commands
|
|
|
103
92
|
[check(store.name, :fail, e.message)]
|
|
104
93
|
end
|
|
105
94
|
|
|
106
|
-
def
|
|
107
|
-
|
|
95
|
+
def scheduler_checks
|
|
96
|
+
checks = [solid_queue_check, sidekiq_cron_check].compact
|
|
108
97
|
|
|
109
|
-
|
|
110
|
-
|
|
98
|
+
if checks.none?(&:ok?)
|
|
99
|
+
[check("scheduler detected", :skip, "no schedulers detected. Make sure BackupJob is executed in a cron or within a scheduler.")]
|
|
100
|
+
else
|
|
101
|
+
checks.select(&:ok?)
|
|
111
102
|
end
|
|
103
|
+
end
|
|
112
104
|
|
|
113
|
-
|
|
114
|
-
|
|
105
|
+
def solid_queue_check
|
|
106
|
+
if File.exist?(SOLID_QUEUE_SCHEDULE_FILE) && File.read(SOLID_QUEUE_SCHEDULE_FILE, mode: "r:UTF-8").include?("BoringBackup::BackupJob")
|
|
107
|
+
check("scheduler detected", :ok, "solid-queue: #{SOLID_QUEUE_SCHEDULE_FILE}")
|
|
115
108
|
end
|
|
116
|
-
|
|
117
|
-
check("schedule", :ok, "#{RECURRING} runs BoringBackup::BackupJob")
|
|
118
109
|
end
|
|
119
110
|
|
|
120
|
-
def
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
require "boring_backup/jobs/backup_job"
|
|
125
|
-
|
|
126
|
-
true
|
|
127
|
-
rescue LoadError, NameError
|
|
128
|
-
false
|
|
111
|
+
def sidekiq_cron_check
|
|
112
|
+
if File.exist?(SIDEKIQ_CRON_SCHEDULE_FILE) && File.read(SIDEKIQ_CRON_SCHEDULE_FILE, mode: "r:UTF-8").include?("BoringBackup::BackupJob")
|
|
113
|
+
check("scheduler detected", :ok, "sidekiq-cron: #{SIDEKIQ_CRON_SCHEDULE_FILE}")
|
|
114
|
+
end
|
|
129
115
|
end
|
|
130
116
|
|
|
131
117
|
def config
|
|
@@ -69,6 +69,7 @@ module BoringBackup
|
|
|
69
69
|
store =
|
|
70
70
|
case store_type.to_sym
|
|
71
71
|
when :s3 then build_s3_store
|
|
72
|
+
when :r2 then build_r2_store
|
|
72
73
|
else raise BoringBackup::ConfigurationError, "unknown store type: #{store_type}"
|
|
73
74
|
end
|
|
74
75
|
|
|
@@ -100,13 +101,22 @@ module BoringBackup
|
|
|
100
101
|
"the :s3 store needs the aws-sdk-s3 gem. Add `gem \"aws-sdk-s3\"` to your Gemfile."
|
|
101
102
|
end
|
|
102
103
|
|
|
104
|
+
def build_r2_store
|
|
105
|
+
require_relative "stores/r2"
|
|
106
|
+
|
|
107
|
+
BoringBackup::Stores::R2.new
|
|
108
|
+
rescue LoadError
|
|
109
|
+
raise BoringBackup::ConfigurationError,
|
|
110
|
+
"the :r2 store needs the aws-sdk-s3 gem. Add `gem \"aws-sdk-s3\"` to your Gemfile."
|
|
111
|
+
end
|
|
112
|
+
|
|
103
113
|
def pg_env
|
|
104
114
|
{
|
|
105
|
-
"PGHOST" => (pg_host || db_config[:host])&.to_s,
|
|
106
|
-
"PGPORT" => (pg_port || db_config[:port])&.to_s,
|
|
107
|
-
"PGUSER" => (pg_user || db_config[:username])&.to_s,
|
|
108
|
-
"PGPASSWORD" => (pg_password || db_config[:password])&.to_s,
|
|
109
|
-
"PGDATABASE" => (pg_database || db_config[:database])&.to_s
|
|
115
|
+
"PGHOST" => (pg_host || db_config[:host] || ENV["PGHOST"])&.to_s,
|
|
116
|
+
"PGPORT" => (pg_port || db_config[:port] || ENV["PGPORT"])&.to_s,
|
|
117
|
+
"PGUSER" => (pg_user || db_config[:username] || ENV["PGUSER"])&.to_s,
|
|
118
|
+
"PGPASSWORD" => (pg_password || db_config[:password] || ENV["PGPASSWORD"])&.to_s,
|
|
119
|
+
"PGDATABASE" => (pg_database || db_config[:database] || ENV["PGDATABASE"])&.to_s
|
|
110
120
|
}.compact
|
|
111
121
|
end
|
|
112
122
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module BoringBackup
|
|
2
|
+
module Engine
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
NAME = "postgresql"
|
|
6
|
+
|
|
7
|
+
def info
|
|
8
|
+
client = client_version
|
|
9
|
+
server = server_version
|
|
10
|
+
|
|
11
|
+
return if client.nil? && server.nil?
|
|
12
|
+
|
|
13
|
+
{name: NAME, client: client, server: server}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def client_version
|
|
17
|
+
return unless pg_dump_path
|
|
18
|
+
|
|
19
|
+
version(`#{pg_dump_path} --version`)
|
|
20
|
+
rescue
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def server_version
|
|
25
|
+
return unless defined?(::ActiveRecord)
|
|
26
|
+
|
|
27
|
+
version(::ActiveRecord::Base.connection.select_value("SHOW server_version"))
|
|
28
|
+
rescue
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def pg_dump_path
|
|
33
|
+
return @pg_dump_path if defined?(@pg_dump_path)
|
|
34
|
+
|
|
35
|
+
@pg_dump_path = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR)
|
|
36
|
+
.map { |dir| File.join(dir, "pg_dump") }
|
|
37
|
+
.find { |path| File.executable?(path) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def version(string)
|
|
43
|
+
string.to_s[/\d+(?:\.\d+)*/]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -52,11 +52,11 @@ module BoringBackup::Notifiers
|
|
|
52
52
|
private
|
|
53
53
|
|
|
54
54
|
def normalized_host
|
|
55
|
-
|
|
55
|
+
return host if host.include?("://")
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
scheme = LOCAL_HOSTS.include?(host.split(":").first) ? "http" : "https"
|
|
58
58
|
|
|
59
|
-
"#{
|
|
59
|
+
"#{scheme}://#{host}"
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def post(body)
|
|
@@ -85,6 +85,7 @@ module BoringBackup::Notifiers
|
|
|
85
85
|
status: result.status,
|
|
86
86
|
error: result.error&.message,
|
|
87
87
|
database: BoringBackup.config.pg_env["PGDATABASE"],
|
|
88
|
+
engine: result.engine,
|
|
88
89
|
bytes: store_results.filter_map(&:bytes).max,
|
|
89
90
|
duration: store_results.filter_map(&:duration).max&.round(3),
|
|
90
91
|
stores: store_results.map do |store_result|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require "aws-sdk-s3"
|
|
2
|
+
require_relative "s3"
|
|
3
|
+
|
|
4
|
+
module BoringBackup::Stores
|
|
5
|
+
class R2 < S3
|
|
6
|
+
def name = :r2
|
|
7
|
+
|
|
8
|
+
def default_storage_class = :standard
|
|
9
|
+
|
|
10
|
+
def storage_classes = %i[standard standard_ia]
|
|
11
|
+
|
|
12
|
+
def region
|
|
13
|
+
value = super
|
|
14
|
+
|
|
15
|
+
value.to_s.empty? ? "auto" : value
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate!
|
|
19
|
+
super
|
|
20
|
+
|
|
21
|
+
raise BoringBackup::ConfigurationError, "endpoint is not configured" if endpoint.to_s.empty?
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -2,33 +2,16 @@ require "aws-sdk-s3"
|
|
|
2
2
|
|
|
3
3
|
module BoringBackup::Stores
|
|
4
4
|
class S3 < Store
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
STORAGE_CLASSES = %i[
|
|
8
|
-
standard
|
|
9
|
-
standard_ia
|
|
10
|
-
onezone_ia
|
|
11
|
-
intelligent_tiering
|
|
12
|
-
glacier_ir
|
|
13
|
-
glacier
|
|
14
|
-
deep_archive
|
|
15
|
-
reduced_redundancy
|
|
16
|
-
express_onezone
|
|
17
|
-
outposts
|
|
18
|
-
snow
|
|
19
|
-
].freeze
|
|
20
|
-
|
|
21
|
-
attr_accessor :bucket, :region, :access_key_id, :secret_access_key, :part_size, :thread_count
|
|
5
|
+
attr_accessor :bucket, :region, :access_key_id, :secret_access_key, :part_size, :thread_count, :endpoint
|
|
22
6
|
attr_writer :storage_class
|
|
23
7
|
|
|
24
8
|
def initialize
|
|
25
|
-
@bucket = ENV["AWS_S3_BUCKET"]
|
|
26
9
|
@region = ENV["AWS_REGION"]
|
|
27
10
|
@access_key_id = ENV["AWS_ACCESS_KEY_ID"]
|
|
28
11
|
@secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"]
|
|
29
12
|
@part_size = ENV.fetch("BB_S3_PART_SIZE", 8 * 1024 * 1024).to_i
|
|
30
13
|
@thread_count = ENV.fetch("BB_S3_THREAD_COUNT", 2).to_i
|
|
31
|
-
@storage_class = ENV.fetch("BB_S3_STORAGE_CLASS",
|
|
14
|
+
@storage_class = ENV.fetch("BB_S3_STORAGE_CLASS", default_storage_class)
|
|
32
15
|
end
|
|
33
16
|
|
|
34
17
|
def storage_class
|
|
@@ -37,10 +20,28 @@ module BoringBackup::Stores
|
|
|
37
20
|
value.empty? ? nil : value.to_sym
|
|
38
21
|
end
|
|
39
22
|
|
|
40
|
-
def
|
|
23
|
+
def default_storage_class = :standard_ia
|
|
24
|
+
|
|
25
|
+
def storage_classes
|
|
26
|
+
%i[
|
|
27
|
+
standard
|
|
28
|
+
standard_ia
|
|
29
|
+
onezone_ia
|
|
30
|
+
intelligent_tiering
|
|
31
|
+
glacier_ir
|
|
32
|
+
glacier
|
|
33
|
+
deep_archive
|
|
34
|
+
reduced_redundancy
|
|
35
|
+
express_onezone
|
|
36
|
+
outposts
|
|
37
|
+
snow
|
|
38
|
+
]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def name = :s3
|
|
41
42
|
|
|
42
43
|
def description
|
|
43
|
-
[bucket.to_s.empty? ? "no bucket" : "
|
|
44
|
+
[bucket.to_s.empty? ? "no bucket" : "#{name}://#{bucket}", region].compact.join(" ")
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
# Prefer Aws::S3::TransferManager for streaming uploads if available.
|
|
@@ -73,13 +74,13 @@ module BoringBackup::Stores
|
|
|
73
74
|
raise BoringBackup::DumpTooSmallError, "backup too small: #{BoringBackup.utils.human_size(bytes)} < min_size (#{BoringBackup.utils.human_size(config.min_size)})"
|
|
74
75
|
end
|
|
75
76
|
|
|
76
|
-
Result.new(success: true, store:
|
|
77
|
+
Result.new(success: true, store: name, bytes:, key:, duration:)
|
|
77
78
|
rescue => e
|
|
78
79
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
79
80
|
|
|
80
81
|
cleanup!(key) if upload_attempted
|
|
81
82
|
|
|
82
|
-
Result.new(success: false, error: e, store:
|
|
83
|
+
Result.new(success: false, error: e, store: name, bytes:, key:, duration:)
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
def validate!
|
|
@@ -90,9 +91,9 @@ module BoringBackup::Stores
|
|
|
90
91
|
"access_key_id and secret_access_key must both be set, or both left blank to use the default AWS credential chain"
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
if storage_class && !
|
|
94
|
+
if storage_class && !storage_classes.include?(storage_class)
|
|
94
95
|
raise BoringBackup::ConfigurationError,
|
|
95
|
-
"unknown storage class: #{storage_class.inspect} (expected one of: #{
|
|
96
|
+
"unknown storage class: #{storage_class.inspect} (expected one of: #{storage_classes.map(&:inspect).join(", ")}, or nil to use the bucket default)"
|
|
96
97
|
end
|
|
97
98
|
end
|
|
98
99
|
|
|
@@ -116,9 +117,10 @@ module BoringBackup::Stores
|
|
|
116
117
|
|
|
117
118
|
def s3_config
|
|
118
119
|
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
endpoint:,
|
|
121
|
+
region:,
|
|
122
|
+
access_key_id:,
|
|
123
|
+
secret_access_key:
|
|
122
124
|
}.compact
|
|
123
125
|
end
|
|
124
126
|
end
|
data/lib/boring_backup.rb
CHANGED
|
@@ -2,6 +2,7 @@ require_relative "boring_backup/version"
|
|
|
2
2
|
require_relative "boring_backup/configuration"
|
|
3
3
|
require_relative "boring_backup/tee"
|
|
4
4
|
require_relative "boring_backup/utils"
|
|
5
|
+
require_relative "boring_backup/engine"
|
|
5
6
|
require_relative "boring_backup/stores"
|
|
6
7
|
require_relative "boring_backup/commands/backup"
|
|
7
8
|
require_relative "boring_backup/commands/doctor"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: boring-backup
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Georgi Mitrev
|
|
@@ -85,12 +85,14 @@ files:
|
|
|
85
85
|
- lib/boring_backup/commands/backup.rb
|
|
86
86
|
- lib/boring_backup/commands/doctor.rb
|
|
87
87
|
- lib/boring_backup/configuration.rb
|
|
88
|
+
- lib/boring_backup/engine.rb
|
|
88
89
|
- lib/boring_backup/jobs/backup_job.rb
|
|
89
90
|
- lib/boring_backup/notifiers/sentinel.rb
|
|
90
91
|
- lib/boring_backup/notifiers/slack.rb
|
|
91
92
|
- lib/boring_backup/notifiers/stdout.rb
|
|
92
93
|
- lib/boring_backup/plugins/rails.rb
|
|
93
94
|
- lib/boring_backup/stores.rb
|
|
95
|
+
- lib/boring_backup/stores/r2.rb
|
|
94
96
|
- lib/boring_backup/stores/result.rb
|
|
95
97
|
- lib/boring_backup/stores/s3.rb
|
|
96
98
|
- lib/boring_backup/stores/store.rb
|
|
@@ -120,7 +122,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
120
122
|
- !ruby/object:Gem::Version
|
|
121
123
|
version: '0'
|
|
122
124
|
requirements: []
|
|
123
|
-
rubygems_version: 4.0.
|
|
125
|
+
rubygems_version: 4.0.17
|
|
124
126
|
specification_version: 4
|
|
125
127
|
summary: Hassle-free database backups
|
|
126
128
|
test_files: []
|