bard-backup 0.13.0 → 0.15.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: 51001d22a2e69c99f9bb208bca62d65796d6380836e4591046d202a435352760
4
- data.tar.gz: b7d759d4be19c813749b8861b974a56acbf986dcc0fcf1542e29bd6bf3816527
3
+ metadata.gz: d27a5d0b3ccda89bf0b15345414f6143ba1ef13e2b388545f32bbe980f36eb2f
4
+ data.tar.gz: 7ac24670444a407c21b9ce062feed27580da64d6f62f02326db65882a14d86b4
5
5
  SHA512:
6
- metadata.gz: e04e6a895738ea48bb2d60dbbd357d33ff5941c9ad0039b30726168c35aa5f4662522cd295965cd63c6d45cbfc4251766128ce548daf8d60d37dd02f05a5db37
7
- data.tar.gz: 34f4cb5549b78961a9dc29f11818b98f5f45f2f6ec0d80aa9c17ca80a4ab9a88bd98fb96889122096fdd0c9f347d0b4a900d075448ded24384555fea50f7a550
6
+ metadata.gz: 1adf405bb06825fe7fd2e176dc1bd4565a2200977a5dead6e2d3c92bb7415b7144a9c2f08bd6995e60afa6b0069c77428bd6e509ddbca1a470b04cfd2708f0de
7
+ data.tar.gz: f5870aa10fcb9df65c920c8b7516ab9f82577ad263cd5578002cec49d19c2ae223837441f1b3ec7cd71ab1df85cea5cf7fd5f46f87be62b57d41fd58edbbbee5
data/CLAUDE.md CHANGED
@@ -45,19 +45,20 @@ Tests require AWS credentials at `spec/support/credentials.json`. In CI, this is
45
45
  **Entry points**:
46
46
  - `Bard::Backup.create!` accepts destination configs (or reads from `Bard::Config`) and delegates to destination strategies. Returns a `Bard::Backup` instance with timestamp/size/destinations.
47
47
  - `Bard::Backup::FileTree.create!` syncs configured data directories to S3.
48
- - `Bard::Backup.restore!(at:)` downloads the backup nearest `at` (or the latest when omitted), decrypts it if necessary, and loads it into the local database via `backhoe` (drop-and-create). `Bard::Backup.available` lists the available backup timestamps.
48
+ - `Bard::Backup.restore!(at:)` downloads the backup nearest `at` (or the latest when omitted), decrypts it if necessary, and loads it into the local database via `backhoe` (drop-and-create). `Bard::Backup.available` lists the available backups (as `Bard::Backup` objects carrying timestamp and size).
49
49
 
50
- **Destination strategy pattern**: `Destination.build(config)` is a factory that picks the right class based on `:type`. `Destination.resolve(hashes)` is the single entry point both `Database.create!` and `Finder` use — it falls back to the configured destinations, normalizes a lone Hash/Array, and builds them. Each destination resolves its own `encryption_key` (explicit config, falling back to `Bard::Config.current.backup.encryption_key`), so callers never inject it.
50
+ **Destination strategy pattern**: `Destination.build(config)` is a factory that picks the right class based on `:type`. `Destination.resolve(hashes)` is the single entry point both `Database.create!` and `Finder` use — it falls back to the configured destinations, normalizes a lone Hash/Array, and builds them. A destination's `encryption_key` comes from its config, which `tasks.rake` merges from the `bard_backup` Rails credentials entry; there is no other source.
51
51
  - `S3Destination` — dumps DB locally via backhoe, uploads to S3, runs `Deleter` for retention, verifies previous hour's backup
52
52
  - `UploadDestination` — dumps DB and uploads to presigned URLs (multi-threaded)
53
53
 
54
- **Config DSL** (loaded via `bard/plugins/backup` and `bard/plugins/encrypt`; `backup` extends `Bard::Config`, `encrypt` extends `Bard::BackupConfig`):
54
+ **Config DSL** (loaded via `bard/plugins/backup`; `backup` extends `Bard::Config`):
55
55
  ```ruby
56
56
  backup do
57
57
  s3 "primary", path: "bucket/subfolder", region: "us-west-2"
58
- encrypt true # reads key from config/master.key
59
58
  end
60
59
  ```
60
+ Encryption at rest is opt-in per destination via an `encryption_key` in the `bard_backup`
61
+ credentials entry — there is no DSL toggle.
61
62
 
62
63
  **Key classes**:
63
64
  - `S3Tree` — `Data.define`-based S3 wrapper used by both `S3Destination` and `FileTree`. Methods: `list_objects`, `put_file`, `put_body`, `get`, `delete_keys`, `mv`, `empty!`. Supports encryption via `Encryptor` and STS `session_token`.
@@ -65,7 +66,7 @@ end
65
66
  - `Encryptor` — AES-256-GCM with HKDF-derived keys and a deterministic IV (HMAC of plaintext), enabling content-addressable encryption
66
67
  - `Deleter` — implements the retention policy via `Filter` structs that check time-based granularities
67
68
  - `LocalBackhoe` / `CachedLocalBackhoe` — database dump strategies (cached variant avoids conflicts when running parallel destinations)
68
- - `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). Backs `Bard::Backup.available`/`.latest` and `Restore`
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`
69
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!`
70
71
  - `BackupConfig` — the `backup do ... end` DSL surface (`bard`, `disabled`, `s3 name, **kwargs`); `create!` reads `bard_config.backup.destinations` from it
71
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
data/README.md CHANGED
@@ -22,7 +22,6 @@ In a Rails app, configure destinations in `config/bard.rb` using the `Bard::Conf
22
22
  ```ruby
23
23
  backup do
24
24
  s3 "primary", path: "my-bucket/my-project", region: "us-west-2"
25
- encrypt true # optional: encrypt payloads at rest. Reads key from config/master.key.
26
25
  end
27
26
  ```
28
27
 
@@ -33,8 +32,13 @@ bard_backup:
33
32
  - name: primary
34
33
  access_key_id: ...
35
34
  secret_access_key: ...
35
+ encryption_key: ... # optional: encrypts payloads at rest with AES-256-GCM
36
36
  ```
37
37
 
38
+ Adding `encryption_key` is what turns on encryption at rest; omit it and payloads are stored
39
+ in the clear. Changing its value makes existing backups undecryptable, so treat it as
40
+ permanent unless you re-key deliberately.
41
+
38
42
  Then run via the rake tasks provided by the bundled Railtie:
39
43
 
40
44
  ```bash
@@ -21,16 +21,20 @@ module Bard
21
21
  resolved_config.slice(:name, :type, :path, :region)
22
22
  end
23
23
 
24
+ # Credentials can supply the key, so resolve rather than reading raw config.
25
+ def encryption_key
26
+ resolved_config[:encryption_key]
27
+ end
28
+
24
29
  private
25
30
 
26
- # The raw config enriched with Rails credentials (by name), defaults, the
27
- # computed endpoint, and the resolved encryption key.
31
+ # The raw config enriched with Rails credentials (by name), defaults, and
32
+ # the computed endpoint.
28
33
  def resolved_config
29
34
  @resolved_config ||= begin
30
35
  credentials = RailsCredentials.find(name: config[:name])
31
36
  resolved = { type: :s3, region: "us-west-2" }.merge(credentials).merge(config)
32
37
  resolved[:endpoint] ||= "https://s3.#{resolved[:region]}.amazonaws.com"
33
- resolved[:encryption_key] ||= Bard::Config.current.backup.encryption_key
34
38
  resolved
35
39
  end
36
40
  end
@@ -11,8 +11,7 @@ module Bard
11
11
  # Normalizes destination config into built destinations: falls back to the
12
12
  # configured destinations when none are given, and accepts a lone Hash or
13
13
  # an Array. A +now+ stamps every destination with one run timestamp (an
14
- # explicit per-destination +now+ still wins). Each destination resolves its
15
- # own encryption key (see #encryption_key), so callers don't have to inject it.
14
+ # explicit per-destination +now+ still wins).
16
15
  def self.resolve(destination_hashes = nil, now: nil)
17
16
  hashes = destination_hashes || Bard::Config.current.backup.destinations
18
17
  hashes = hashes.is_a?(Hash) ? [hashes] : Array(hashes)
@@ -24,12 +23,12 @@ module Bard
24
23
  raise NotImplementedError
25
24
  end
26
25
 
27
- private
28
-
29
26
  def encryption_key
30
- config[:encryption_key] || Bard::Config.current.backup.encryption_key
27
+ config[:encryption_key]
31
28
  end
32
29
 
30
+ private
31
+
33
32
  def now
34
33
  @now ||= config.fetch(:now, Time.now.utc)
35
34
  end
@@ -15,7 +15,6 @@ module Bard
15
15
  return if data_paths.empty?
16
16
 
17
17
  encryption_key = s3_config.delete(:encryption_key)
18
- encryption_key ||= bard_config&.backup&.encryption_key
19
18
 
20
19
  s3_tree = S3Tree.new(path: "#{bucket}/#{project_name}", encryption_key: encryption_key, **s3_config)
21
20
  new(s3_tree, data_paths).call
@@ -26,6 +26,19 @@ module Bard
26
26
  all.map { |backup| backup[:timestamp] }.uniq.sort
27
27
  end
28
28
 
29
+ def available
30
+ all
31
+ .group_by { |backup| backup[:timestamp] }
32
+ .sort_by { |timestamp, _| timestamp }
33
+ .map do |timestamp, backups|
34
+ Bard::Backup.new(
35
+ timestamp:,
36
+ size: file_size(backups.first[:destination].s3_tree, backups.first[:filename]),
37
+ destinations: backups.map { |backup| backup[:destination].info },
38
+ )
39
+ end
40
+ end
41
+
29
42
  def find(at: nil)
30
43
  backups = all
31
44
  raise NotFound, "No backups found" if backups.empty?
@@ -26,9 +26,11 @@ namespace :bard do
26
26
 
27
27
  destinations = Bard::Config.current.backup.destinations
28
28
  if args[:at].to_s.strip.empty?
29
- timestamps = Bard::Backup.available(destinations: destinations)
29
+ backups = Bard::Backup.available(destinations: destinations)
30
30
  puts "Available backups:"
31
- timestamps.each { |timestamp| puts " #{timestamp.iso8601}" }
31
+ backups.each do |backup|
32
+ puts " #{backup.timestamp.iso8601} #{backup.human_size}"
33
+ end
32
34
  else
33
35
  backup = Bard::Backup.restore!(at: args[:at], destinations: destinations)
34
36
  puts "Restored database from backup #{backup.timestamp.iso8601}."
@@ -1,6 +1,6 @@
1
1
  module Bard
2
2
  class Backup
3
- VERSION = "0.13.0"
3
+ VERSION = "0.15.0"
4
4
  end
5
5
  end
6
6
 
data/lib/bard/backup.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require "bard"
2
2
  require "bard/plugins/backup"
3
- require "bard/plugins/encrypt"
4
3
  require "bard/backup/database"
5
4
  require "bard/backup/file_tree"
6
5
  require "bard/backup/finder"
@@ -22,7 +21,7 @@ module Bard
22
21
  end
23
22
 
24
23
  def self.available(destinations: nil)
25
- Finder.new(destinations).timestamps
24
+ Finder.new(destinations).available
26
25
  end
27
26
 
28
27
  def self.restore!(at:, drop_and_create: true, destinations: nil)
@@ -44,5 +43,15 @@ module Bard
44
43
  destinations: destinations
45
44
  }.compact
46
45
  end
46
+
47
+ def human_size
48
+ units = %w[Bytes KB MB GB TB PB EB]
49
+ value, unit = size.to_f, 0
50
+ while value >= 1024 && unit < units.size - 1
51
+ value /= 1024
52
+ unit += 1
53
+ end
54
+ format("%5.2f %s", value, units[unit])
55
+ end
47
56
  end
48
57
  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.13.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micah Geisel
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-07-09 00:00:00.000000000 Z
10
+ date: 2026-08-08 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: backhoe
@@ -113,7 +113,6 @@ files:
113
113
  - lib/bard/backup/version.rb
114
114
  - lib/bard/plugins/backup.rb
115
115
  - lib/bard/plugins/backup_restore.rb
116
- - lib/bard/plugins/encrypt.rb
117
116
  - sig/bard/backup.rbs
118
117
  homepage: https://github.com/botandrose/bard-backup
119
118
  licenses:
@@ -1,18 +0,0 @@
1
- require "bard/plugins/backup"
2
-
3
- module Bard
4
- class BackupConfig
5
- def encrypt(value = nil)
6
- if value.nil?
7
- @encrypt
8
- else
9
- @encrypt = value
10
- end
11
- end
12
-
13
- def encryption_key
14
- return nil unless encrypt
15
- File.read("config/master.key").strip
16
- end
17
- end
18
- end