bard-backup 0.15.2 → 0.16.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/CLAUDE.md +2 -1
- data/README.md +8 -2
- data/lib/bard/backup/destination/s3_destination.rb +6 -1
- data/lib/bard/backup/encryptor.rb +1 -0
- data/lib/bard/backup/file_tree.rb +13 -8
- data/lib/bard/backup/rekey.rb +63 -0
- data/lib/bard/backup/s3_tree.rb +9 -1
- data/lib/bard/backup/tasks.rake +14 -0
- data/lib/bard/backup/version.rb +1 -1
- data/lib/bard/backup.rb +19 -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: b04f7c45bf80b1e66501cde66ca37f02def6a887368710c18ebbb6b12e8dd906
|
|
4
|
+
data.tar.gz: 2ae3f5a68b9fd1d3166475917b3c0abfdecad4d4e9fdcdba6ebf0f8397f38659
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3647ba74dbeaad6cc519d8cb6a6e8a00166c53a8f885891435d3fd4e800d0a5388395176bdc5ca9b76af3d607de6138fadc845df6a946f32fd7591042c6fa1f1
|
|
7
|
+
data.tar.gz: d58dc757b0738f1f0dc1547fe15cf236e95d51f0843a204c1d5e2df8d671c560f4bf237ec27217991d26f241b6f03c036c6a8b8ef889466a55ceb127efb59874
|
data/CLAUDE.md
CHANGED
|
@@ -68,6 +68,7 @@ credentials entry — there is no DSL toggle.
|
|
|
68
68
|
- `LocalBackhoe` / `CachedLocalBackhoe` — database dump strategies (cached variant avoids conflicts when running parallel destinations)
|
|
69
69
|
- `Finder` — lists backups across destinations and selects one: by timestamp (nearest match), the latest, or the most-recent as a `Bard::Backup` (with size + destination info). `#available` returns one `Bard::Backup` per timestamp (with size) for listing. Backs `Bard::Backup.available`/`.latest` and `Restore`
|
|
70
70
|
- `Restore` — downloads the backup selected by `Finder`, decrypts it via `S3Tree#get`, and loads it into the local database via backhoe (drop-and-create). Backs `Bard::Backup.restore!`
|
|
71
|
+
- `Rekey` — rewrites every object under an S3 path so it is encrypted under a key (from plaintext, or from an old key via `from_key:`), skipping objects already under it and reading each rewrite back. Backs `Bard::Backup.encrypt_existing!` and `bard:backup:encrypt`
|
|
71
72
|
- `BackupConfig` — the `backup do ... end` DSL surface (`bard`, `disabled`, `s3 name, **kwargs`); `create!` reads `bard_config.backup.destinations` from it
|
|
72
|
-
- `Railtie` — loads `tasks.rake` which provides `bard:backup` (DB + data), `bard:backup:data` (data only), and `bard:backup:restore[at]` (restore/list) rake tasks in Rails apps
|
|
73
|
+
- `Railtie` — loads `tasks.rake` which provides `bard:backup` (DB + data), `bard:backup:data` (data only), `bard:backup:encrypt` (encrypt pre-key backups and data files), and `bard:backup:restore[at]` (restore/list) rake tasks in Rails apps
|
|
73
74
|
- `bard/plugins/backup_restore.rb` — registers the `bard backup restore [TIMESTAMP]` CLI subcommand (loaded by the `bard` CLI via `Gem.find_files`); shells out to the `bard:backup:restore` rake task
|
data/README.md
CHANGED
|
@@ -39,11 +39,17 @@ Adding `encryption_key` is what turns on encryption at rest; omit it and payload
|
|
|
39
39
|
in the clear. Changing its value makes existing backups undecryptable, so treat it as
|
|
40
40
|
permanent unless you re-key deliberately.
|
|
41
41
|
|
|
42
|
+
Adding a key only encrypts what is uploaded afterwards. Database dumps stored before it still
|
|
43
|
+
restore (they are recognized as plain gzip), and `rake bard:backup:encrypt` rewrites them and
|
|
44
|
+
the data file tree in place under the key, verifying each object as it goes. It skips anything
|
|
45
|
+
already encrypted, so it is safe to rerun.
|
|
46
|
+
|
|
42
47
|
Then run via the rake tasks provided by the bundled Railtie:
|
|
43
48
|
|
|
44
49
|
```bash
|
|
45
|
-
rake bard:backup
|
|
46
|
-
rake bard:backup:data
|
|
50
|
+
rake bard:backup # database backup + data file-tree sync
|
|
51
|
+
rake bard:backup:data # data file-tree sync only
|
|
52
|
+
rake bard:backup:encrypt # encrypt everything stored before encryption_key was added
|
|
47
53
|
```
|
|
48
54
|
|
|
49
55
|
## Restoring
|
|
@@ -14,7 +14,12 @@ module Bard
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def s3_tree
|
|
17
|
-
@s3_tree ||= S3Tree.new(**resolved_config.slice(:
|
|
17
|
+
@s3_tree ||= S3Tree.new(**s3_tree_config, **resolved_config.slice(:encryption_key).compact)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The connection alone, without the key, so a re-key can read objects as stored.
|
|
21
|
+
def s3_tree_config
|
|
22
|
+
resolved_config.slice(:endpoint, :path, :access_key_id, :secret_access_key, :session_token, :region).compact
|
|
18
23
|
end
|
|
19
24
|
|
|
20
25
|
def info
|
|
@@ -21,6 +21,7 @@ module Bard
|
|
|
21
21
|
|
|
22
22
|
def decrypt(data)
|
|
23
23
|
data = data.b if data.encoding != Encoding::BINARY
|
|
24
|
+
raise OpenSSL::Cipher::CipherError, "too short to be ciphertext" if data.bytesize < 28
|
|
24
25
|
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
25
26
|
cipher.decrypt
|
|
26
27
|
cipher.key = @encrypt_key
|
|
@@ -8,16 +8,21 @@ module Bard
|
|
|
8
8
|
MANIFEST_PATH = ".bard-file-tree-sync.json"
|
|
9
9
|
DEFAULT_BUCKET = "bard-data"
|
|
10
10
|
|
|
11
|
-
def self.create!(data_paths: nil,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
def self.create!(data_paths: nil, **options)
|
|
12
|
+
data_paths ||= configured_data_paths
|
|
13
|
+
tree_config = s3_tree_config(data_paths:, **options)
|
|
14
|
+
new(S3Tree.new(**tree_config), data_paths).call if tree_config
|
|
15
|
+
end
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
def self.s3_tree_config(data_paths: nil, project_name: nil, bucket: DEFAULT_BUCKET, **s3_config)
|
|
18
|
+
data_paths ||= configured_data_paths
|
|
19
|
+
return if data_paths.empty?
|
|
20
|
+
project_name ||= Bard::Config.current.project_name if defined?(Bard::Config)
|
|
21
|
+
{ path: "#{bucket}/#{project_name}", **s3_config }
|
|
22
|
+
end
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
def self.configured_data_paths
|
|
25
|
+
defined?(Bard::Config) ? Bard::Config.current.data : []
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
def initialize(s3_tree, data_paths)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require "bard/backup/s3_tree"
|
|
2
|
+
require "bard/backup/encryptor"
|
|
3
|
+
require "bard/backup/destination"
|
|
4
|
+
|
|
5
|
+
module Bard
|
|
6
|
+
class Backup
|
|
7
|
+
class Rekey < Struct.new(:tree_config, :to_key, :from_key, keyword_init: true)
|
|
8
|
+
def self.call(...)
|
|
9
|
+
new(...).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
result = { path: tree_config[:path], encrypted: [], skipped: [] }
|
|
14
|
+
stored_tree.files.each do |file|
|
|
15
|
+
stored = stored_tree.get(file).b
|
|
16
|
+
if under_target_key?(stored)
|
|
17
|
+
result[:skipped] << file
|
|
18
|
+
else
|
|
19
|
+
rewrite(file, stored)
|
|
20
|
+
result[:encrypted] << file
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def rewrite(file, stored)
|
|
29
|
+
body = from_key ? Encryptor.new(from_key).decrypt(stored) : stored
|
|
30
|
+
target_tree.put_body(file, body)
|
|
31
|
+
return if reads_back?(file, body)
|
|
32
|
+
|
|
33
|
+
stored_tree.put_body(file, stored)
|
|
34
|
+
raise Error, "#{file} did not read back intact after encrypting, so its original was put back"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reads_back?(file, body)
|
|
38
|
+
target_tree.get(file).b == body
|
|
39
|
+
rescue OpenSSL::Cipher::CipherError
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def under_target_key?(stored)
|
|
44
|
+
target_encryptor.decrypt(stored)
|
|
45
|
+
true
|
|
46
|
+
rescue OpenSSL::Cipher::CipherError
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def stored_tree
|
|
51
|
+
@stored_tree ||= S3Tree.new(**tree_config)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def target_tree
|
|
55
|
+
@target_tree ||= S3Tree.new(**tree_config, encryption_key: to_key)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def target_encryptor
|
|
59
|
+
@target_encryptor ||= Encryptor.new(to_key)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/bard/backup/s3_tree.rb
CHANGED
|
@@ -72,7 +72,7 @@ module Bard
|
|
|
72
72
|
key: [folder_prefix, remote_key].compact.join("/"),
|
|
73
73
|
})
|
|
74
74
|
body = response.body.read
|
|
75
|
-
body =
|
|
75
|
+
body = decrypt(body) if encryptor
|
|
76
76
|
body
|
|
77
77
|
end
|
|
78
78
|
|
|
@@ -113,6 +113,14 @@ module Bard
|
|
|
113
113
|
Encryptor.new(encryption_key) if encryption_key
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
def decrypt(body)
|
|
117
|
+
encryptor.decrypt(body)
|
|
118
|
+
rescue OpenSSL::Cipher::CipherError
|
|
119
|
+
body = body.b
|
|
120
|
+
raise unless body.start_with?("\x1f\x8b".b) # a database dump stored before encryption was enabled
|
|
121
|
+
body
|
|
122
|
+
end
|
|
123
|
+
|
|
116
124
|
def client
|
|
117
125
|
Aws::S3::Client.new({
|
|
118
126
|
endpoint: endpoint,
|
data/lib/bard/backup/tasks.rake
CHANGED
|
@@ -19,6 +19,20 @@ namespace :bard do
|
|
|
19
19
|
Bard::Backup::FileTree.create!(**Bard::Backup::RailsCredentials.find)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
desc "Encrypt the backups and data files stored before encryption was enabled"
|
|
23
|
+
task :encrypt => :environment do
|
|
24
|
+
require "bard/backup"
|
|
25
|
+
require "bard/backup/rails_credentials"
|
|
26
|
+
|
|
27
|
+
destinations = Bard::Config.current.backup.destinations.map do |dest|
|
|
28
|
+
Bard::Backup::RailsCredentials.find(name: dest[:name]).merge(dest)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Bard::Backup.encrypt_existing!(destinations, **Bard::Backup::RailsCredentials.find).each do |result|
|
|
32
|
+
puts "#{result[:path]}: encrypted #{result[:encrypted].size}, already encrypted #{result[:skipped].size}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
22
36
|
desc "Restore the database backup nearest a timestamp (no timestamp lists available backups)"
|
|
23
37
|
task :restore, [:at] => :environment do |_task, args|
|
|
24
38
|
require "bard/backup"
|
data/lib/bard/backup/version.rb
CHANGED
data/lib/bard/backup.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "bard/backup/database"
|
|
|
4
4
|
require "bard/backup/file_tree"
|
|
5
5
|
require "bard/backup/finder"
|
|
6
6
|
require "bard/backup/restore"
|
|
7
|
+
require "bard/backup/rekey"
|
|
7
8
|
require "bard/backup/railtie" if defined?(Rails)
|
|
8
9
|
|
|
9
10
|
module Bard
|
|
@@ -16,6 +17,24 @@ module Bard
|
|
|
16
17
|
backup
|
|
17
18
|
end
|
|
18
19
|
|
|
20
|
+
def self.encrypt_existing!(destination_hashes = nil, **config)
|
|
21
|
+
destination_hashes = [config] if destination_hashes.nil? && !config.empty?
|
|
22
|
+
trees = Destination.resolve(destination_hashes)
|
|
23
|
+
.select { |destination| destination.respond_to?(:s3_tree_config) }
|
|
24
|
+
.map { |destination| [destination.s3_tree_config, destination.encryption_key, destination.info[:name]] }
|
|
25
|
+
|
|
26
|
+
file_tree_config = FileTree.s3_tree_config(**config.slice(*FILE_TREE_KEYS, :data_paths, :project_name, :bucket))
|
|
27
|
+
if file_tree_config
|
|
28
|
+
key = file_tree_config.delete(:encryption_key)
|
|
29
|
+
trees << [file_tree_config, key, file_tree_config[:path]]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
missing = trees.reject { |_, key, _| key }.map(&:last)
|
|
33
|
+
raise Error, "No encryption_key for #{missing.join(", ")}" if missing.any?
|
|
34
|
+
|
|
35
|
+
trees.map { |tree_config, key, _| Rekey.call(tree_config:, to_key: key) }
|
|
36
|
+
end
|
|
37
|
+
|
|
19
38
|
def self.latest
|
|
20
39
|
Finder.new.latest
|
|
21
40
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bard-backup
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Micah Geisel
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-11-
|
|
10
|
+
date: 2026-11-13 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: backhoe
|
|
@@ -107,6 +107,7 @@ files:
|
|
|
107
107
|
- lib/bard/backup/local_backhoe.rb
|
|
108
108
|
- lib/bard/backup/rails_credentials.rb
|
|
109
109
|
- lib/bard/backup/railtie.rb
|
|
110
|
+
- lib/bard/backup/rekey.rb
|
|
110
111
|
- lib/bard/backup/restore.rb
|
|
111
112
|
- lib/bard/backup/s3_tree.rb
|
|
112
113
|
- lib/bard/backup/tasks.rake
|