bard-backup 0.7.0 → 0.9.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/.ruby-version +1 -1
- data/README.md +2 -2
- data/config/cucumber.yml +1 -0
- data/lib/bard/backup/cached_local_backhoe.rb +27 -0
- data/lib/bard/backup/deleter.rb +1 -1
- data/lib/bard/backup/destination/s3_destination.rb +48 -0
- data/lib/bard/backup/destination/upload_destination.rb +71 -0
- data/lib/bard/backup/destination.rb +23 -0
- data/lib/bard/backup/latest_finder.rb +47 -0
- data/lib/bard/backup/local_backhoe.rb +1 -1
- data/lib/bard/backup/railtie.rb +12 -0
- data/lib/bard/backup/s3_dir.rb +9 -5
- data/lib/bard/backup/tasks.rake +7 -0
- data/lib/bard/backup/version.rb +2 -2
- data/lib/bard/backup.rb +32 -12
- metadata +26 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 27e2f3a66518e1d3449f448b15a587fb1839d7fb8d978e969ef583821dc01f9e
|
|
4
|
+
data.tar.gz: 3deae6b4c09d9492aa194f5511dc91d478de9311a559bf8620fdc0a6ca339ea1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d941d50b9c96292732262452f71c127525e446b9b4aa10b146b8e1bf8488026d02f1816240bb1e5721d47947215d6dff566437a7b6bcf0c6675390b4d8453580
|
|
7
|
+
data.tar.gz: a9c8f3c8d09f8e38b738e78966bf8a585ea15851867e8ae837ced8ede3c8769d50248789c265bd604194a7e7e52557192e974aa2317c00aadfc59c398b52bedc
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby-3.
|
|
1
|
+
ruby-3.4.2
|
data/README.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Bard::Backup does 3 things in a bard project
|
|
4
4
|
1. Takes a database dump and uploads it to our s3 bucket
|
|
5
|
-
2. Deletes old backups using a backoff heuristic:
|
|
5
|
+
2. Deletes old backups using a backoff heuristic: 48 hours, 30 days, 26 weeks, 24 months, then yearly
|
|
6
6
|
3. Raises an error if we don't have a backup from the previous hour
|
|
7
7
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
|
10
10
|
## Usage
|
|
11
11
|
|
|
12
|
-
Run with `Bard::Backup.call
|
|
12
|
+
Run with `Bard::Backup.call path: "s3_bucket/optional_subfolder", access_key_id: "...", secret_access_key: "...", region: "..."`
|
|
13
13
|
|
|
14
14
|
Or just run via the `bard-rake` gem: `rake db:backup`, which wires up the above for you.
|
|
15
15
|
|
data/config/cucumber.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
default: --publish-quiet
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require "backhoe"
|
|
2
|
+
|
|
3
|
+
module Bard
|
|
4
|
+
class Backup
|
|
5
|
+
class CachedLocalBackhoe < Struct.new(:s3_dir, :now)
|
|
6
|
+
def self.call *args
|
|
7
|
+
new(*args).call
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
s3_dir.put path
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def path
|
|
17
|
+
@@path ||= begin
|
|
18
|
+
filename = "#{now.iso8601}.sql.gz"
|
|
19
|
+
path = "/tmp/#{filename}"
|
|
20
|
+
Backhoe.dump path
|
|
21
|
+
at_exit { FileUtils.rm_f path }
|
|
22
|
+
path
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/bard/backup/deleter.rb
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require "bard/backup/s3_dir"
|
|
2
|
+
require "bard/backup/deleter"
|
|
3
|
+
require "bard/backup/local_backhoe"
|
|
4
|
+
require "bard/backup/cached_local_backhoe"
|
|
5
|
+
|
|
6
|
+
module Bard
|
|
7
|
+
class Backup
|
|
8
|
+
class S3Destination < Destination
|
|
9
|
+
def call
|
|
10
|
+
strategy.call(s3_dir, now)
|
|
11
|
+
Deleter.new(s3_dir, now).call
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def s3_dir
|
|
15
|
+
@s3_dir ||= S3Dir.new(**config.slice(:endpoint, :path, :access_key_id, :secret_access_key, :region))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def info
|
|
19
|
+
config.slice(:name, :type, :path, :region)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def config
|
|
25
|
+
@config ||= begin
|
|
26
|
+
config = {}
|
|
27
|
+
|
|
28
|
+
if defined?(Rails)
|
|
29
|
+
credentials = Rails.application.credentials.bard_backup || []
|
|
30
|
+
credentials = [credentials] if credentials.is_a?(Hash)
|
|
31
|
+
config = credentials.find { |c| c[:name] == super[:name] } || {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
config = { type: :s3, region: "us-west-2" }.merge(config).merge(super)
|
|
35
|
+
config[:endpoint] ||= "https://s3.#{config[:region]}.amazonaws.com"
|
|
36
|
+
config
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def strategy
|
|
41
|
+
return @strategy if @strategy
|
|
42
|
+
@strategy = config.fetch(:strategy, LocalBackhoe)
|
|
43
|
+
@strategy = Bard::Backup.const_get(@strategy) if @strategy.is_a?(String)
|
|
44
|
+
@strategy
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "net/http"
|
|
4
|
+
|
|
5
|
+
module Bard
|
|
6
|
+
class Backup
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
class UploadDestination < Destination
|
|
10
|
+
def call
|
|
11
|
+
timestamp = now
|
|
12
|
+
filename = "#{timestamp.iso8601}.sql.gz"
|
|
13
|
+
temp_path = "/tmp/#{filename}"
|
|
14
|
+
errors = []
|
|
15
|
+
|
|
16
|
+
begin
|
|
17
|
+
Backhoe.dump(temp_path)
|
|
18
|
+
size = File.size(temp_path)
|
|
19
|
+
|
|
20
|
+
threads = urls.map do |url|
|
|
21
|
+
Thread.new do
|
|
22
|
+
upload_to_url(url, temp_path)
|
|
23
|
+
rescue => e
|
|
24
|
+
errors << e
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
threads.each(&:join)
|
|
29
|
+
ensure
|
|
30
|
+
FileUtils.rm_f(temp_path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
raise Error, "Upload failed: #{errors.map(&:message).join(", ")}" unless errors.empty?
|
|
34
|
+
|
|
35
|
+
Bard::Backup.new(timestamp:, size:, destinations: [])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def info
|
|
39
|
+
{ type: :upload, name: config[:name] }.compact
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def urls
|
|
45
|
+
@urls ||= begin
|
|
46
|
+
Array(config[:urls]).compact
|
|
47
|
+
raise Error, "No URLs provided" if urls.empty?
|
|
48
|
+
urls
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def upload_to_url(url, file_path)
|
|
53
|
+
uri = URI.parse(url)
|
|
54
|
+
|
|
55
|
+
File.open(file_path, "rb") do |file|
|
|
56
|
+
request = Net::HTTP::Put.new(uri)
|
|
57
|
+
request.body = file.read
|
|
58
|
+
request.content_type = "application/octet-stream"
|
|
59
|
+
|
|
60
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
61
|
+
http.request(request)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
65
|
+
raise Error, "Upload failed with status #{response.code}: #{response.body}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Bard
|
|
2
|
+
class Backup
|
|
3
|
+
class Destination < Struct.new(:config)
|
|
4
|
+
def self.build(config)
|
|
5
|
+
klass = Bard::Backup.const_get("#{config[:type].to_s.capitalize}Destination")
|
|
6
|
+
klass.new(config)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def now
|
|
16
|
+
@now ||= config.fetch(:now, Time.now.utc)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
require "bard/backup/destination/s3_destination"
|
|
23
|
+
require "bard/backup/destination/upload_destination"
|
|
@@ -0,0 +1,47 @@
|
|
|
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_dir.files.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_dir, 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_dir, filename)
|
|
42
|
+
key = [s3_dir.folder_prefix, filename].compact.join("/")
|
|
43
|
+
s3_dir.send(:client).head_object(bucket: s3_dir.bucket_name, key: key).content_length
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/bard/backup/s3_dir.rb
CHANGED
|
@@ -2,8 +2,13 @@ require "aws-sdk-s3"
|
|
|
2
2
|
require "rexml"
|
|
3
3
|
|
|
4
4
|
module Bard
|
|
5
|
-
|
|
6
|
-
class S3Dir < Data.define(:endpoint, :path, :
|
|
5
|
+
class Backup
|
|
6
|
+
class S3Dir < Data.define(:endpoint, :path, :access_key_id, :secret_access_key, :region)
|
|
7
|
+
def initialize **kwargs
|
|
8
|
+
kwargs[:endpoint] ||= "https://s3.#{kwargs[:region]}.amazonaws.com"
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
|
|
7
12
|
def files
|
|
8
13
|
response = client.list_objects_v2({
|
|
9
14
|
bucket: bucket_name,
|
|
@@ -72,11 +77,10 @@ module Bard
|
|
|
72
77
|
Aws::S3::Client.new({
|
|
73
78
|
endpoint: endpoint,
|
|
74
79
|
region: region,
|
|
75
|
-
access_key_id:
|
|
76
|
-
secret_access_key:
|
|
80
|
+
access_key_id: access_key_id,
|
|
81
|
+
secret_access_key: secret_access_key,
|
|
77
82
|
})
|
|
78
83
|
end
|
|
79
84
|
end
|
|
80
85
|
end
|
|
81
86
|
end
|
|
82
|
-
|
data/lib/bard/backup/version.rb
CHANGED
data/lib/bard/backup.rb
CHANGED
|
@@ -1,17 +1,37 @@
|
|
|
1
|
-
require "bard/backup/
|
|
2
|
-
require "bard/backup/
|
|
3
|
-
require "bard/backup/
|
|
1
|
+
require "bard/backup/destination"
|
|
2
|
+
require "bard/backup/latest_finder"
|
|
3
|
+
require "bard/backup/railtie" if defined?(Rails)
|
|
4
4
|
|
|
5
5
|
module Bard
|
|
6
|
-
|
|
7
|
-
def self.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
class Backup
|
|
7
|
+
def self.create!(destination_hashes = nil, **config)
|
|
8
|
+
if destination_hashes.nil? && !config.empty?
|
|
9
|
+
destination_hashes = [config]
|
|
10
|
+
end
|
|
11
|
+
destination_hashes ||= Bard::Config.current.backup.destinations
|
|
12
|
+
Array(destination_hashes).each do |hash|
|
|
13
|
+
Destination.build(hash).call
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.latest
|
|
18
|
+
LatestFinder.new.call
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :timestamp, :size, :destinations
|
|
22
|
+
|
|
23
|
+
def initialize(timestamp:, size: nil, destinations: [])
|
|
24
|
+
@timestamp = timestamp
|
|
25
|
+
@size = size
|
|
26
|
+
@destinations = destinations
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def as_json(*)
|
|
30
|
+
{
|
|
31
|
+
timestamp: timestamp&.iso8601,
|
|
32
|
+
size: size,
|
|
33
|
+
destinations: destinations
|
|
34
|
+
}.compact
|
|
14
35
|
end
|
|
15
36
|
end
|
|
16
37
|
end
|
|
17
|
-
|
metadata
CHANGED
|
@@ -1,14 +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.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Micah Geisel
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
10
|
+
date: 2025-12-11 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: backhoe
|
|
@@ -53,7 +52,21 @@ dependencies:
|
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
53
|
version: '0'
|
|
55
54
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name:
|
|
55
|
+
name: rails
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: bard
|
|
57
70
|
requirement: !ruby/object:Gem::Requirement
|
|
58
71
|
requirements:
|
|
59
72
|
- - ">="
|
|
@@ -66,7 +79,6 @@ dependencies:
|
|
|
66
79
|
- - ">="
|
|
67
80
|
- !ruby/object:Gem::Version
|
|
68
81
|
version: '0'
|
|
69
|
-
description:
|
|
70
82
|
email:
|
|
71
83
|
- micah@botandrose.com
|
|
72
84
|
executables: []
|
|
@@ -79,11 +91,19 @@ files:
|
|
|
79
91
|
- LICENSE.txt
|
|
80
92
|
- README.md
|
|
81
93
|
- Rakefile
|
|
94
|
+
- config/cucumber.yml
|
|
82
95
|
- lib/bard-backup.rb
|
|
83
96
|
- lib/bard/backup.rb
|
|
97
|
+
- lib/bard/backup/cached_local_backhoe.rb
|
|
84
98
|
- lib/bard/backup/deleter.rb
|
|
99
|
+
- lib/bard/backup/destination.rb
|
|
100
|
+
- lib/bard/backup/destination/s3_destination.rb
|
|
101
|
+
- lib/bard/backup/destination/upload_destination.rb
|
|
102
|
+
- lib/bard/backup/latest_finder.rb
|
|
85
103
|
- lib/bard/backup/local_backhoe.rb
|
|
104
|
+
- lib/bard/backup/railtie.rb
|
|
86
105
|
- lib/bard/backup/s3_dir.rb
|
|
106
|
+
- lib/bard/backup/tasks.rake
|
|
87
107
|
- lib/bard/backup/version.rb
|
|
88
108
|
- sig/bard/backup.rbs
|
|
89
109
|
homepage: https://github.com/botandrose/bard-backup
|
|
@@ -91,7 +111,6 @@ licenses:
|
|
|
91
111
|
- MIT
|
|
92
112
|
metadata:
|
|
93
113
|
homepage_uri: https://github.com/botandrose/bard-backup
|
|
94
|
-
post_install_message:
|
|
95
114
|
rdoc_options: []
|
|
96
115
|
require_paths:
|
|
97
116
|
- lib
|
|
@@ -106,8 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
106
125
|
- !ruby/object:Gem::Version
|
|
107
126
|
version: '0'
|
|
108
127
|
requirements: []
|
|
109
|
-
rubygems_version: 3.
|
|
110
|
-
signing_key:
|
|
128
|
+
rubygems_version: 3.6.2
|
|
111
129
|
specification_version: 4
|
|
112
130
|
summary: Provides automated db backups for bard projects
|
|
113
131
|
test_files: []
|