boring-backup 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +18 -5
- data/lib/boring_backup/cli.rb +2 -14
- data/lib/boring_backup/commands/backup.rb +1 -1
- data/lib/boring_backup/commands/doctor.rb +18 -20
- data/lib/boring_backup/configuration.rb +15 -5
- data/lib/boring_backup/notifiers/sentinel.rb +3 -3
- data/lib/boring_backup/stores/r2.rb +20 -0
- data/lib/boring_backup/stores/s3.rb +9 -9
- data/lib/boring_backup/stores/store.rb +1 -1
- data/lib/boring_backup/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 851d7814c77a411800884379a85ae1feeffb5c8183d3b524ab3851f1fc7d1dde
|
|
4
|
+
data.tar.gz: e54bf1a64c28d237753b4eaa5c4bd7114c067a6b6f957b73ad4526f5332fc393
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 682eb6a513c065b1078ca528bebb9ea5cbc83924a873d47965e5ca463c8d94210a8f6e020242e45ea6fb8817e8c23e8c51a999070c1d5f2c5c63cf0b9ecc605a
|
|
7
|
+
data.tar.gz: 7afd54ae737169eb4f44d4bfd575acd64b13177b8c1d77d33260d217742934cbe053822cd5ebf4a64aa00a92cbfa16751553042f8a4ff1e7da6861f245f99045
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.6.0] - 2026-07-23
|
|
4
|
+
|
|
5
|
+
- `bb doctor` - improved scheduler checks (now supports `sidekiq-crosidekiq-cron`)
|
|
6
|
+
- support `PG*` env vars
|
|
7
|
+
- support for Cloudflare R2 using their S3-compatible API
|
|
8
|
+
|
|
3
9
|
## [0.5.0] - 2026-07-14
|
|
4
10
|
|
|
5
11
|
- **`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
|
|
@@ -69,6 +75,8 @@ All configuration options can be edited in the initializer:
|
|
|
69
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
|
|
|
@@ -57,7 +57,7 @@ module BoringBackup::Commands
|
|
|
57
57
|
store.backup!(@key, reader)
|
|
58
58
|
rescue => e
|
|
59
59
|
# *always* return a Result, even if unexpected. Add store name for debugging.
|
|
60
|
-
BoringBackup::Stores::Result.new(success: false, error: e, store: store.
|
|
60
|
+
BoringBackup::Stores::Result.new(success: false, error: e, store: store.name, key: @key)
|
|
61
61
|
ensure
|
|
62
62
|
reader.close
|
|
63
63
|
end
|
|
@@ -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)
|
|
@@ -103,29 +104,26 @@ module BoringBackup::Commands
|
|
|
103
104
|
[check(store.name, :fail, e.message)]
|
|
104
105
|
end
|
|
105
106
|
|
|
106
|
-
def
|
|
107
|
-
|
|
107
|
+
def scheduler_checks
|
|
108
|
+
checks = [solid_queue_check, sidekiq_cron_check].compact
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
if checks.none?(&:ok?)
|
|
111
|
+
[check("scheduler detected", :skip, "no schedulers detected. Make sure BackupJob is executed in a cron or within a scheduler.")]
|
|
112
|
+
else
|
|
113
|
+
checks.select(&:ok?)
|
|
111
114
|
end
|
|
115
|
+
end
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
def solid_queue_check
|
|
118
|
+
if File.exist?(SOLID_QUEUE_SCHEDULE_FILE) && File.read(SOLID_QUEUE_SCHEDULE_FILE, mode: "r:UTF-8").include?("BoringBackup::BackupJob")
|
|
119
|
+
check("scheduler detected", :ok, "solid-queue: #{SOLID_QUEUE_SCHEDULE_FILE}")
|
|
115
120
|
end
|
|
116
|
-
|
|
117
|
-
check("schedule", :ok, "#{RECURRING} runs BoringBackup::BackupJob")
|
|
118
121
|
end
|
|
119
122
|
|
|
120
|
-
def
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
require "boring_backup/jobs/backup_job"
|
|
125
|
-
|
|
126
|
-
true
|
|
127
|
-
rescue LoadError, NameError
|
|
128
|
-
false
|
|
123
|
+
def sidekiq_cron_check
|
|
124
|
+
if File.exist?(SIDEKIQ_CRON_SCHEDULE_FILE) && File.read(SIDEKIQ_CRON_SCHEDULE_FILE, mode: "r:UTF-8").include?("BoringBackup::BackupJob")
|
|
125
|
+
check("scheduler detected", :ok, "sidekiq-cron: #{SIDEKIQ_CRON_SCHEDULE_FILE}")
|
|
126
|
+
end
|
|
129
127
|
end
|
|
130
128
|
|
|
131
129
|
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
|
|
|
@@ -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)
|
|
@@ -0,0 +1,20 @@
|
|
|
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 region
|
|
9
|
+
value = super
|
|
10
|
+
|
|
11
|
+
value.to_s.empty? ? "auto" : value
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def validate!
|
|
15
|
+
super
|
|
16
|
+
|
|
17
|
+
raise BoringBackup::ConfigurationError, "endpoint is not configured" if endpoint.to_s.empty?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -18,11 +18,10 @@ module BoringBackup::Stores
|
|
|
18
18
|
snow
|
|
19
19
|
].freeze
|
|
20
20
|
|
|
21
|
-
attr_accessor :bucket, :region, :access_key_id, :secret_access_key, :part_size, :thread_count
|
|
21
|
+
attr_accessor :bucket, :region, :access_key_id, :secret_access_key, :part_size, :thread_count, :endpoint
|
|
22
22
|
attr_writer :storage_class
|
|
23
23
|
|
|
24
24
|
def initialize
|
|
25
|
-
@bucket = ENV["AWS_S3_BUCKET"]
|
|
26
25
|
@region = ENV["AWS_REGION"]
|
|
27
26
|
@access_key_id = ENV["AWS_ACCESS_KEY_ID"]
|
|
28
27
|
@secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"]
|
|
@@ -37,10 +36,10 @@ module BoringBackup::Stores
|
|
|
37
36
|
value.empty? ? nil : value.to_sym
|
|
38
37
|
end
|
|
39
38
|
|
|
40
|
-
def name =
|
|
39
|
+
def name = :s3
|
|
41
40
|
|
|
42
41
|
def description
|
|
43
|
-
[bucket.to_s.empty? ? "no bucket" : "
|
|
42
|
+
[bucket.to_s.empty? ? "no bucket" : "#{name}://#{bucket}", region].compact.join(" ")
|
|
44
43
|
end
|
|
45
44
|
|
|
46
45
|
# Prefer Aws::S3::TransferManager for streaming uploads if available.
|
|
@@ -73,13 +72,13 @@ module BoringBackup::Stores
|
|
|
73
72
|
raise BoringBackup::DumpTooSmallError, "backup too small: #{BoringBackup.utils.human_size(bytes)} < min_size (#{BoringBackup.utils.human_size(config.min_size)})"
|
|
74
73
|
end
|
|
75
74
|
|
|
76
|
-
Result.new(success: true, store:
|
|
75
|
+
Result.new(success: true, store: name, bytes:, key:, duration:)
|
|
77
76
|
rescue => e
|
|
78
77
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
79
78
|
|
|
80
79
|
cleanup!(key) if upload_attempted
|
|
81
80
|
|
|
82
|
-
Result.new(success: false, error: e, store:
|
|
81
|
+
Result.new(success: false, error: e, store: name, bytes:, key:, duration:)
|
|
83
82
|
end
|
|
84
83
|
|
|
85
84
|
def validate!
|
|
@@ -116,9 +115,10 @@ module BoringBackup::Stores
|
|
|
116
115
|
|
|
117
116
|
def s3_config
|
|
118
117
|
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
endpoint:,
|
|
119
|
+
region:,
|
|
120
|
+
access_key_id:,
|
|
121
|
+
secret_access_key:
|
|
122
122
|
}.compact
|
|
123
123
|
end
|
|
124
124
|
end
|
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.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Georgi Mitrev
|
|
@@ -91,6 +91,7 @@ files:
|
|
|
91
91
|
- lib/boring_backup/notifiers/stdout.rb
|
|
92
92
|
- lib/boring_backup/plugins/rails.rb
|
|
93
93
|
- lib/boring_backup/stores.rb
|
|
94
|
+
- lib/boring_backup/stores/r2.rb
|
|
94
95
|
- lib/boring_backup/stores/result.rb
|
|
95
96
|
- lib/boring_backup/stores/s3.rb
|
|
96
97
|
- lib/boring_backup/stores/store.rb
|