boring-backup 0.6.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 +8 -2
- data/README.md +1 -1
- data/lib/boring_backup/commands/backup.rb +6 -4
- data/lib/boring_backup/commands/doctor.rb +6 -18
- data/lib/boring_backup/engine.rb +46 -0
- data/lib/boring_backup/notifiers/sentinel.rb +1 -0
- data/lib/boring_backup/stores/r2.rb +4 -0
- data/lib/boring_backup/stores/s3.rb +21 -19
- data/lib/boring_backup/version.rb +1 -1
- data/lib/boring_backup.rb +1 -0
- metadata +3 -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,10 +1,16 @@
|
|
|
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
|
+
|
|
3
9
|
## [0.6.0] - 2026-07-23
|
|
4
10
|
|
|
5
11
|
- `bb doctor` - improved scheduler checks (now supports `sidekiq-crosidekiq-cron`)
|
|
6
|
-
-
|
|
7
|
-
-
|
|
12
|
+
- Support `PG*` env vars
|
|
13
|
+
- Support for Cloudflare R2 using their S3-compatible API
|
|
8
14
|
|
|
9
15
|
## [0.5.0] - 2026-07-14
|
|
10
16
|
|
data/README.md
CHANGED
|
@@ -72,7 +72,7 @@ anything to disk. Use any scheduler or even cron to run it periodically.
|
|
|
72
72
|
All configuration options can be edited in the initializer:
|
|
73
73
|
|
|
74
74
|
```rb
|
|
75
|
-
# config/initializers/
|
|
75
|
+
# config/initializers/boring_backup.rb
|
|
76
76
|
|
|
77
77
|
BoringBackup.configure do |config|
|
|
78
78
|
config.sentinel_key = '12345678-abcd-1234-abcd-abcdef123456'
|
|
@@ -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]
|
|
@@ -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
|
|
@@ -43,39 +43,27 @@ module BoringBackup::Commands
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def pg_dump_path
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@pg_dump_path = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR)
|
|
49
|
-
.map { |dir| File.join(dir, "pg_dump") }
|
|
50
|
-
.find { |path| File.executable?(path) }
|
|
46
|
+
BoringBackup::Engine.pg_dump_path
|
|
51
47
|
end
|
|
52
48
|
|
|
53
49
|
def pg_dump_version
|
|
54
50
|
return unless pg_dump_path
|
|
55
51
|
|
|
56
|
-
client =
|
|
57
|
-
server =
|
|
52
|
+
client = BoringBackup::Engine.client_version
|
|
53
|
+
server = BoringBackup::Engine.server_version
|
|
58
54
|
|
|
59
55
|
return check("versions", :skip, "could not read server version") unless server
|
|
60
56
|
return check("versions", :skip, "could not read pg_dump version") unless client
|
|
61
57
|
|
|
62
|
-
if client >= server
|
|
58
|
+
if major(client) >= major(server)
|
|
63
59
|
check("versions", :ok, "pg_dump #{client}, server #{server}")
|
|
64
60
|
else
|
|
65
61
|
check("versions", :fail, "pg_dump #{client} is older than server #{server} — the dump may not restore")
|
|
66
62
|
end
|
|
67
63
|
end
|
|
68
64
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def server_version
|
|
74
|
-
return unless defined?(::ActiveRecord)
|
|
75
|
-
|
|
76
|
-
::ActiveRecord::Base.connection.select_value("SHOW server_version")
|
|
77
|
-
rescue
|
|
78
|
-
nil
|
|
65
|
+
def major(version)
|
|
66
|
+
Gem::Version.new(version).segments.first
|
|
79
67
|
end
|
|
80
68
|
|
|
81
69
|
def database_resolved
|
|
@@ -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
|
|
@@ -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|
|
|
@@ -2,22 +2,6 @@ require "aws-sdk-s3"
|
|
|
2
2
|
|
|
3
3
|
module BoringBackup::Stores
|
|
4
4
|
class S3 < Store
|
|
5
|
-
DEFAULT_STORAGE_CLASS = :standard_ia
|
|
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
5
|
attr_accessor :bucket, :region, :access_key_id, :secret_access_key, :part_size, :thread_count, :endpoint
|
|
22
6
|
attr_writer :storage_class
|
|
23
7
|
|
|
@@ -27,7 +11,7 @@ module BoringBackup::Stores
|
|
|
27
11
|
@secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"]
|
|
28
12
|
@part_size = ENV.fetch("BB_S3_PART_SIZE", 8 * 1024 * 1024).to_i
|
|
29
13
|
@thread_count = ENV.fetch("BB_S3_THREAD_COUNT", 2).to_i
|
|
30
|
-
@storage_class = ENV.fetch("BB_S3_STORAGE_CLASS",
|
|
14
|
+
@storage_class = ENV.fetch("BB_S3_STORAGE_CLASS", default_storage_class)
|
|
31
15
|
end
|
|
32
16
|
|
|
33
17
|
def storage_class
|
|
@@ -36,6 +20,24 @@ module BoringBackup::Stores
|
|
|
36
20
|
value.empty? ? nil : value.to_sym
|
|
37
21
|
end
|
|
38
22
|
|
|
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
|
+
|
|
39
41
|
def name = :s3
|
|
40
42
|
|
|
41
43
|
def description
|
|
@@ -89,9 +91,9 @@ module BoringBackup::Stores
|
|
|
89
91
|
"access_key_id and secret_access_key must both be set, or both left blank to use the default AWS credential chain"
|
|
90
92
|
end
|
|
91
93
|
|
|
92
|
-
if storage_class && !
|
|
94
|
+
if storage_class && !storage_classes.include?(storage_class)
|
|
93
95
|
raise BoringBackup::ConfigurationError,
|
|
94
|
-
"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)"
|
|
95
97
|
end
|
|
96
98
|
end
|
|
97
99
|
|
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.6.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Georgi Mitrev
|
|
@@ -85,6 +85,7 @@ 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
|
|
@@ -121,7 +122,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
121
122
|
- !ruby/object:Gem::Version
|
|
122
123
|
version: '0'
|
|
123
124
|
requirements: []
|
|
124
|
-
rubygems_version: 4.0.
|
|
125
|
+
rubygems_version: 4.0.17
|
|
125
126
|
specification_version: 4
|
|
126
127
|
summary: Hassle-free database backups
|
|
127
128
|
test_files: []
|