bard-backup 0.11.4 → 0.12.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 +6 -3
- data/README.md +33 -0
- data/lib/bard/backup/database.rb +8 -18
- data/lib/bard/backup/destination/s3_destination.rb +12 -10
- data/lib/bard/backup/destination/upload_destination.rb +2 -2
- data/lib/bard/backup/destination.rb +18 -0
- data/lib/bard/backup/finder.rb +68 -0
- data/lib/bard/backup/restore.rb +36 -0
- data/lib/bard/backup/tasks.rake +18 -2
- data/lib/bard/backup/version.rb +1 -1
- data/lib/bard/backup.rb +11 -2
- data/lib/bard/plugins/backup_restore.rb +35 -0
- metadata +5 -3
- data/lib/bard/backup/latest_finder.rb +0 -47
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f051915564aff1edf80617a2f83d6eb0c5fbf392ef0f68b5cd4d082c9d0a6450
|
|
4
|
+
data.tar.gz: 274a30cc6ac3579045ffb1c7be96a62016f63497e177fc09483122535c72329a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 998e90d23806ee850d5440b7e63c89ef357749697dae25f5cb89b20c77b32ccb644da5c232ecebdbdc57d7e4a2b53562acdb9c081d02b34624ce533d03efb29f
|
|
7
|
+
data.tar.gz: bd5eb49fa3a1676ea5f1c6805450f0146c0d18b9ee26139fcee74314d0bfcb9984d27076b7af510ff959f5fa4c7e1eb31d157e7be52795c20e138be75551a4fb
|
data/CLAUDE.md
CHANGED
|
@@ -45,8 +45,9 @@ 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
49
|
|
|
49
|
-
**Destination strategy pattern**: `Destination.build(config)` is a factory that picks the right class based on `:type
|
|
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
51
|
- `S3Destination` — dumps DB locally via backhoe, uploads to S3, runs `Deleter` for retention, verifies previous hour's backup
|
|
51
52
|
- `UploadDestination` — dumps DB and uploads to presigned URLs (multi-threaded)
|
|
52
53
|
|
|
@@ -64,6 +65,8 @@ end
|
|
|
64
65
|
- `Encryptor` — AES-256-GCM with HKDF-derived keys and a deterministic IV (HMAC of plaintext), enabling content-addressable encryption
|
|
65
66
|
- `Deleter` — implements the retention policy via `Filter` structs that check time-based granularities
|
|
66
67
|
- `LocalBackhoe` / `CachedLocalBackhoe` — database dump strategies (cached variant avoids conflicts when running parallel destinations)
|
|
67
|
-
- `
|
|
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
|
+
- `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!`
|
|
68
70
|
- `BackupConfig` — the `backup do ... end` DSL surface (`bard`, `disabled`, `s3 name, **kwargs`); `create!` reads `bard_config.backup.destinations` from it
|
|
69
|
-
- `Railtie` — loads `tasks.rake` which provides `bard:backup` (DB + data)
|
|
71
|
+
- `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
|
|
72
|
+
- `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
|
@@ -42,6 +42,39 @@ rake bard:backup # database backup + data file-tree sync
|
|
|
42
42
|
rake bard:backup:data # data file-tree sync only
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
## Restoring
|
|
46
|
+
|
|
47
|
+
Restore a database backup from S3 into the local database. The backup nearest the
|
|
48
|
+
requested timestamp is selected (backups run hourly, so an exact match isn't
|
|
49
|
+
guaranteed), downloaded, decrypted if necessary, and loaded via `backhoe`,
|
|
50
|
+
dropping and recreating the local database.
|
|
51
|
+
|
|
52
|
+
From the `bard` CLI:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
bard backup restore # list the available backup timestamps
|
|
56
|
+
bard backup restore 2026-06-05T16:00:00Z # restore the backup nearest this UTC time
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or via the rake task directly:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
rake "bard:backup:restore" # list available backups
|
|
63
|
+
rake "bard:backup:restore[2026-06-05T16:00:00Z]" # restore nearest this time
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or programmatically:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
Bard::Backup.available # => [Time, Time, ...]
|
|
70
|
+
Bard::Backup.restore! # restore the latest backup
|
|
71
|
+
Bard::Backup.restore!(at: "2026-06-05T16:00:00Z") # restore nearest this time
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Timestamps are UTC (the `Z`-suffixed ISO-8601 form the backups are named with).
|
|
75
|
+
Restore requires self-managed destinations configured via the `backup do ... end`
|
|
76
|
+
DSL; bard-managed backups are not yet supported.
|
|
77
|
+
|
|
45
78
|
Or call programmatically:
|
|
46
79
|
|
|
47
80
|
```ruby
|
data/lib/bard/backup/database.rb
CHANGED
|
@@ -8,25 +8,15 @@ module Bard
|
|
|
8
8
|
destination_hashes = [config]
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
now = Time.now.utc
|
|
12
|
+
backups = Destination.resolve(destination_hashes, now: now).map(&:call)
|
|
13
|
+
return nil if backups.empty?
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
encryption_key = bard_config&.backup&.encryption_key
|
|
21
|
-
if encryption_key
|
|
22
|
-
destinations = destinations.map { |h| { encryption_key: encryption_key, **h } }
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
result = nil
|
|
26
|
-
destinations.each do |hash|
|
|
27
|
-
result = Backup::Destination.build(hash).call
|
|
28
|
-
end
|
|
29
|
-
result
|
|
15
|
+
Bard::Backup.new(
|
|
16
|
+
timestamp: backups.first.timestamp,
|
|
17
|
+
size: backups.first.size,
|
|
18
|
+
destinations: backups.flat_map(&:destinations),
|
|
19
|
+
)
|
|
30
20
|
end
|
|
31
21
|
end
|
|
32
22
|
end
|
|
@@ -14,28 +14,30 @@ module Bard
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def s3_tree
|
|
17
|
-
@s3_tree ||= S3Tree.new(**
|
|
17
|
+
@s3_tree ||= S3Tree.new(**resolved_config.slice(:endpoint, :path, :access_key_id, :secret_access_key, :session_token, :region, :encryption_key).compact)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def info
|
|
21
|
-
|
|
21
|
+
resolved_config.slice(:name, :type, :path, :region)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
# The raw config enriched with Rails credentials (by name), defaults, the
|
|
27
|
+
# computed endpoint, and the resolved encryption key.
|
|
28
|
+
def resolved_config
|
|
29
|
+
@resolved_config ||= begin
|
|
30
|
+
credentials = RailsCredentials.find(name: config[:name])
|
|
31
|
+
resolved = { type: :s3, region: "us-west-2" }.merge(credentials).merge(config)
|
|
32
|
+
resolved[:endpoint] ||= "https://s3.#{resolved[:region]}.amazonaws.com"
|
|
33
|
+
resolved[:encryption_key] ||= Bard::Config.current.backup.encryption_key
|
|
34
|
+
resolved
|
|
33
35
|
end
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def strategy
|
|
37
39
|
return @strategy if @strategy
|
|
38
|
-
@strategy =
|
|
40
|
+
@strategy = resolved_config.fetch(:strategy, LocalBackhoe)
|
|
39
41
|
@strategy = Bard::Backup.const_get(@strategy) if @strategy.is_a?(String)
|
|
40
42
|
@strategy
|
|
41
43
|
end
|
|
@@ -55,8 +55,8 @@ module Bard
|
|
|
55
55
|
|
|
56
56
|
File.open(file_path, "rb") do |file|
|
|
57
57
|
body = file.read
|
|
58
|
-
if
|
|
59
|
-
body = Encryptor.new(
|
|
58
|
+
if (key = encryption_key)
|
|
59
|
+
body = Encryptor.new(key).encrypt(body)
|
|
60
60
|
end
|
|
61
61
|
request = Net::HTTP::Put.new(uri)
|
|
62
62
|
request.body = body
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "bard/config"
|
|
2
|
+
|
|
1
3
|
module Bard
|
|
2
4
|
class Backup
|
|
3
5
|
class Destination < Struct.new(:config)
|
|
@@ -6,12 +8,28 @@ module Bard
|
|
|
6
8
|
klass.new(config)
|
|
7
9
|
end
|
|
8
10
|
|
|
11
|
+
# Normalizes destination config into built destinations: falls back to the
|
|
12
|
+
# configured destinations when none are given, and accepts a lone Hash or
|
|
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.
|
|
16
|
+
def self.resolve(destination_hashes = nil, now: nil)
|
|
17
|
+
hashes = destination_hashes || Bard::Config.current.backup.destinations
|
|
18
|
+
hashes = hashes.is_a?(Hash) ? [hashes] : Array(hashes)
|
|
19
|
+
hashes = hashes.map { |hash| { now:, **hash } } if now
|
|
20
|
+
hashes.map { |hash| build(hash) }
|
|
21
|
+
end
|
|
22
|
+
|
|
9
23
|
def call
|
|
10
24
|
raise NotImplementedError
|
|
11
25
|
end
|
|
12
26
|
|
|
13
27
|
private
|
|
14
28
|
|
|
29
|
+
def encryption_key
|
|
30
|
+
config[:encryption_key] || Bard::Config.current.backup.encryption_key
|
|
31
|
+
end
|
|
32
|
+
|
|
15
33
|
def now
|
|
16
34
|
@now ||= config.fetch(:now, Time.now.utc)
|
|
17
35
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
require "bard/config"
|
|
3
|
+
require "bard/backup/destination"
|
|
4
|
+
|
|
5
|
+
module Bard
|
|
6
|
+
class Backup
|
|
7
|
+
class NotFound < StandardError; end
|
|
8
|
+
|
|
9
|
+
# Locates database backups across the configured destinations and selects one by timestamp.
|
|
10
|
+
class Finder
|
|
11
|
+
def initialize(destinations = nil)
|
|
12
|
+
@destinations = Destination.resolve(destinations)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def all
|
|
16
|
+
@destinations.flat_map do |destination|
|
|
17
|
+
destination.s3_tree.list_objects.keys.filter_map do |filename|
|
|
18
|
+
timestamp = parse_timestamp(filename)
|
|
19
|
+
next unless timestamp
|
|
20
|
+
{ timestamp:, destination:, filename: }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def timestamps
|
|
26
|
+
all.map { |backup| backup[:timestamp] }.uniq.sort
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def find(at: nil)
|
|
30
|
+
backups = all
|
|
31
|
+
raise NotFound, "No backups found" if backups.empty?
|
|
32
|
+
|
|
33
|
+
if at.nil?
|
|
34
|
+
backups.max_by { |backup| backup[:timestamp] }
|
|
35
|
+
else
|
|
36
|
+
# returns nearest backup (exact timestamp match is not guaranteed)
|
|
37
|
+
target = at.is_a?(Time) ? at : Time.parse(at.to_s)
|
|
38
|
+
backups.min_by { |backup| (backup[:timestamp] - target).abs }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def latest
|
|
43
|
+
backups = all
|
|
44
|
+
raise NotFound, "No backups found" if backups.empty?
|
|
45
|
+
|
|
46
|
+
newest = backups.max_by { |backup| backup[:timestamp] }
|
|
47
|
+
Bard::Backup.new(
|
|
48
|
+
timestamp: newest[:timestamp],
|
|
49
|
+
size: file_size(newest[:destination].s3_tree, newest[:filename]),
|
|
50
|
+
destinations: backups
|
|
51
|
+
.select { |backup| backup[:timestamp] == newest[:timestamp] }
|
|
52
|
+
.map { |backup| backup[:destination].info },
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def file_size(s3_tree, filename)
|
|
59
|
+
key = [s3_tree.folder_prefix, filename].compact.join("/")
|
|
60
|
+
s3_tree.send(:client).head_object(bucket: s3_tree.bucket_name, key: key).content_length
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parse_timestamp(filename)
|
|
64
|
+
filename =~ /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)/ ? Time.parse($1) : nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require "backhoe"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "bard/backup/finder"
|
|
4
|
+
|
|
5
|
+
module Bard
|
|
6
|
+
class Backup
|
|
7
|
+
# Downloads a database backup from S3 and loads it into the local database.
|
|
8
|
+
# The backup nearest +at+ is selected, decrypted if necessary, and loaded
|
|
9
|
+
# via backhoe.
|
|
10
|
+
class Restore < Struct.new(:at, :drop_and_create, :destinations, keyword_init: true)
|
|
11
|
+
def self.call(...)
|
|
12
|
+
new(...).call
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(at:, drop_and_create: true, destinations: nil)
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
backup = Finder.new(destinations).find(at: at)
|
|
21
|
+
body = backup[:destination].s3_tree.get(backup[:filename])
|
|
22
|
+
path = "/tmp/bard-backup-restore-#{File.basename(backup[:filename])}"
|
|
23
|
+
File.binwrite(path, body)
|
|
24
|
+
Backhoe.load(path, drop_and_create: drop_and_create)
|
|
25
|
+
|
|
26
|
+
Bard::Backup.new(
|
|
27
|
+
timestamp: backup[:timestamp],
|
|
28
|
+
size: body.bytesize,
|
|
29
|
+
destinations: [backup[:destination].info],
|
|
30
|
+
)
|
|
31
|
+
ensure
|
|
32
|
+
FileUtils.rm_f(path) if path
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/bard/backup/tasks.rake
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
require "bard/backup/rails_credentials"
|
|
2
|
-
|
|
3
1
|
namespace :bard do
|
|
4
2
|
desc "Backup the database and file trees to configured destinations"
|
|
5
3
|
task :backup => :environment do
|
|
6
4
|
require "bard/backup"
|
|
5
|
+
require "bard/backup/rails_credentials"
|
|
7
6
|
|
|
8
7
|
destinations = Bard::Config.current.backup.destinations.map do |dest|
|
|
9
8
|
Bard::Backup::RailsCredentials.find(name: dest[:name]).merge(dest)
|
|
@@ -16,7 +15,24 @@ namespace :bard do
|
|
|
16
15
|
desc "Backup file trees to S3"
|
|
17
16
|
task :data => :environment do
|
|
18
17
|
require "bard/backup"
|
|
18
|
+
require "bard/backup/rails_credentials"
|
|
19
19
|
Bard::Backup::FileTree.create!(**Bard::Backup::RailsCredentials.find)
|
|
20
20
|
end
|
|
21
|
+
|
|
22
|
+
desc "Restore the database backup nearest a timestamp (no timestamp lists available backups)"
|
|
23
|
+
task :restore, [:at] => :environment do |_task, args|
|
|
24
|
+
require "bard/backup"
|
|
25
|
+
require "bard/backup/rails_credentials"
|
|
26
|
+
|
|
27
|
+
destinations = Bard::Config.current.backup.destinations
|
|
28
|
+
if args[:at].to_s.strip.empty?
|
|
29
|
+
timestamps = Bard::Backup.available(destinations: destinations)
|
|
30
|
+
puts "Available backups:"
|
|
31
|
+
timestamps.each { |timestamp| puts " #{timestamp.iso8601}" }
|
|
32
|
+
else
|
|
33
|
+
backup = Bard::Backup.restore!(at: args[:at], destinations: destinations)
|
|
34
|
+
puts "Restored database from backup #{backup.timestamp.iso8601}."
|
|
35
|
+
end
|
|
36
|
+
end
|
|
21
37
|
end
|
|
22
38
|
end
|
data/lib/bard/backup/version.rb
CHANGED
data/lib/bard/backup.rb
CHANGED
|
@@ -3,7 +3,8 @@ require "bard/plugins/backup"
|
|
|
3
3
|
require "bard/plugins/encrypt"
|
|
4
4
|
require "bard/backup/database"
|
|
5
5
|
require "bard/backup/file_tree"
|
|
6
|
-
require "bard/backup/
|
|
6
|
+
require "bard/backup/finder"
|
|
7
|
+
require "bard/backup/restore"
|
|
7
8
|
require "bard/backup/railtie" if defined?(Rails)
|
|
8
9
|
|
|
9
10
|
module Bard
|
|
@@ -17,7 +18,15 @@ module Bard
|
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def self.latest
|
|
20
|
-
|
|
21
|
+
Finder.new.latest
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.available(destinations: nil)
|
|
25
|
+
Finder.new(destinations).timestamps
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.restore!(at:, drop_and_create: true, destinations: nil)
|
|
29
|
+
Restore.call(at: at, drop_and_create: drop_and_create, destinations: destinations)
|
|
21
30
|
end
|
|
22
31
|
|
|
23
32
|
attr_reader :timestamp, :size, :destinations
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Registers the `bard backup restore` CLI subcommand. The actual work runs in
|
|
2
|
+
# the `bard:backup:restore` rake task, which boots Rails so it has the AWS
|
|
3
|
+
# credentials and database config it needs (mirroring how `bard data` shells out
|
|
4
|
+
# to `bin/rake db:load`).
|
|
5
|
+
require "bard/command"
|
|
6
|
+
|
|
7
|
+
class Bard::CLI
|
|
8
|
+
class BackupCommands < Thor
|
|
9
|
+
desc "restore [TIMESTAMP]", "restore the database backup nearest TIMESTAMP into the local database"
|
|
10
|
+
long_desc <<~DESC
|
|
11
|
+
Restores the database backup nearest TIMESTAMP (ISO-8601 UTC, e.g.
|
|
12
|
+
2026-06-05T16:00:00Z) into the LOCAL database, dropping and recreating it.
|
|
13
|
+
|
|
14
|
+
With no TIMESTAMP, lists the available backups instead of restoring.
|
|
15
|
+
DESC
|
|
16
|
+
def restore(timestamp = nil)
|
|
17
|
+
if timestamp.nil?
|
|
18
|
+
Bard::Command.run! "bin/rake bard:backup:restore", verbose: true
|
|
19
|
+
else
|
|
20
|
+
say "This will DROP and reload your LOCAL database from the backup nearest #{timestamp}.", :yellow
|
|
21
|
+
unless yes?("Continue? (y/N)")
|
|
22
|
+
say "Aborted."
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
Bard::Command.run! %(bin/rake "bard:backup:restore[#{timestamp}]"), verbose: true
|
|
26
|
+
end
|
|
27
|
+
rescue Bard::Command::Error => e
|
|
28
|
+
say "!!! Running command failed: #{e.message}", :red
|
|
29
|
+
exit 1
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
desc "backup SUBCOMMAND", "database backup operations"
|
|
34
|
+
subcommand "backup", BackupCommands
|
|
35
|
+
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.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Micah Geisel
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-06-10 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: backhoe
|
|
@@ -103,14 +103,16 @@ files:
|
|
|
103
103
|
- lib/bard/backup/destination/upload_destination.rb
|
|
104
104
|
- lib/bard/backup/encryptor.rb
|
|
105
105
|
- lib/bard/backup/file_tree.rb
|
|
106
|
-
- lib/bard/backup/
|
|
106
|
+
- lib/bard/backup/finder.rb
|
|
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/restore.rb
|
|
110
111
|
- lib/bard/backup/s3_tree.rb
|
|
111
112
|
- lib/bard/backup/tasks.rake
|
|
112
113
|
- lib/bard/backup/version.rb
|
|
113
114
|
- lib/bard/plugins/backup.rb
|
|
115
|
+
- lib/bard/plugins/backup_restore.rb
|
|
114
116
|
- lib/bard/plugins/encrypt.rb
|
|
115
117
|
- sig/bard/backup.rbs
|
|
116
118
|
homepage: https://github.com/botandrose/bard-backup
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
require "bard/config"
|
|
2
|
-
|
|
3
|
-
module Bard
|
|
4
|
-
class Backup
|
|
5
|
-
class NotFound < StandardError; end
|
|
6
|
-
|
|
7
|
-
class LatestFinder
|
|
8
|
-
def call
|
|
9
|
-
destinations = Bard::Config.current.backup.destinations.map do |hash|
|
|
10
|
-
Destination.build(hash)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
all_backups = destinations.flat_map do |dest|
|
|
14
|
-
dest.s3_tree.list_objects.keys.filter_map do |filename|
|
|
15
|
-
timestamp = parse_timestamp(filename)
|
|
16
|
-
next unless timestamp
|
|
17
|
-
|
|
18
|
-
{ timestamp: timestamp, destination: dest, filename: filename }
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
raise NotFound, "No backups found" if all_backups.empty?
|
|
23
|
-
|
|
24
|
-
latest = all_backups.max_by { |b| b[:timestamp] }
|
|
25
|
-
|
|
26
|
-
Bard::Backup.new(
|
|
27
|
-
timestamp: latest[:timestamp],
|
|
28
|
-
size: get_file_size(latest[:destination].s3_tree, latest[:filename]),
|
|
29
|
-
destinations: all_backups
|
|
30
|
-
.select { |b| b[:timestamp] == latest[:timestamp] }
|
|
31
|
-
.map { |b| b[:destination].info }
|
|
32
|
-
)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def parse_timestamp(filename)
|
|
38
|
-
filename =~ /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)/ ? Time.parse($1) : nil
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def get_file_size(s3_tree, filename)
|
|
42
|
-
key = [s3_tree.folder_prefix, filename].compact.join("/")
|
|
43
|
-
s3_tree.send(:client).head_object(bucket: s3_tree.bucket_name, key: key).content_length
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|