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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90fcf205730a58bf45284b04cd2a7be2604158b5bcd983c2bd3bdaada0fb06c0
4
- data.tar.gz: b17bae14e94295a24d68da3592b3baa238bc1a10fb76bcad464ae515bd7cc46c
3
+ metadata.gz: 851d7814c77a411800884379a85ae1feeffb5c8183d3b524ab3851f1fc7d1dde
4
+ data.tar.gz: e54bf1a64c28d237753b4eaa5c4bd7114c067a6b6f957b73ad4526f5332fc393
5
5
  SHA512:
6
- metadata.gz: 0317b2fbf1d5768a0b0cfe28e351817d51065579dca37007d6236956c07f474c529297237358c36d4cd572d017bcb1f79209b5c5c4827fb036b169bb2ebc1339
7
- data.tar.gz: a3481f64a9c95894830fb1bc92964c154a79796febe716c8e8c27ea09b5de110ce0613386adcdf3703d619a8794730a7470dd09189d504b55cc682b64bea3695
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` store, add the AWS SDK to your `Gemfile` as well:
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
- - [ ] S3-compatible backends
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
 
@@ -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
- if File.exist?(INITIALIZER)
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.class.name, key: @key)
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
- RECURRING = "config/recurring.yml"
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
- schedule_registered
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 schedule_registered
107
- return check("schedule", :skip, "no #{RECURRING} — using cron or another scheduler?") unless File.exist?(RECURRING)
107
+ def scheduler_checks
108
+ checks = [solid_queue_check, sidekiq_cron_check].compact
108
109
 
109
- unless File.read(RECURRING, mode: "r:UTF-8").include?("BoringBackup::BackupJob")
110
- return check("schedule", :fail, "#{RECURRING} does not run BoringBackup::BackupJob")
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
- unless backup_job_loadable?
114
- return check("schedule", :fail, "BoringBackup::BackupJob won't load — is activejob in the Gemfile?")
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 backup_job_loadable?
121
- return false unless defined?(::ActiveJob)
122
-
123
- Object.const_get("ActiveJob::Base")
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
- uri = URI.parse(host)
55
+ return host if host.include?("://")
56
56
 
57
- return host if uri.is_a?(URI::HTTP)
57
+ scheme = LOCAL_HOSTS.include?(host.split(":").first) ? "http" : "https"
58
58
 
59
- "#{LOCAL_HOSTS.include?(uri.scheme) ? "http" : "https"}://#{host}"
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 = "s3"
39
+ def name = :s3
41
40
 
42
41
  def description
43
- [bucket.to_s.empty? ? "no bucket" : "s3://#{bucket}", region].compact.join(" ")
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: :s3, bytes:, key:, duration:)
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: :s3, bytes:, key:, duration:)
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
- region: region,
120
- access_key_id: access_key_id,
121
- secret_access_key: secret_access_key
118
+ endpoint:,
119
+ region:,
120
+ access_key_id:,
121
+ secret_access_key:
122
122
  }.compact
123
123
  end
124
124
  end
@@ -1,7 +1,7 @@
1
1
  module BoringBackup::Stores
2
2
  class Store
3
3
  def name
4
- self.class.name.split("::").last.downcase
4
+ self.class.name.split("::").last.downcase.to_sym
5
5
  end
6
6
 
7
7
  def description
@@ -1,3 +1,3 @@
1
1
  module BoringBackup
2
- VERSION = "0.5.0"
2
+ VERSION = "0.6.0"
3
3
  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.5.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