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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a5141d8e968362d84504fa3cd23a76f3611afd498546de5fb4fdee9d12f2767
4
- data.tar.gz: 658af1b347dc3ab666d62b89e49fee8e13d0c4a68f99331c0942f1572fe37ae2
3
+ metadata.gz: 27e2f3a66518e1d3449f448b15a587fb1839d7fb8d978e969ef583821dc01f9e
4
+ data.tar.gz: 3deae6b4c09d9492aa194f5511dc91d478de9311a559bf8620fdc0a6ca339ea1
5
5
  SHA512:
6
- metadata.gz: 2bed11c4b0915f4302e7039199cfdb58e4cc1c31173511c821b0ff218f40cfab28d9781980e1d81e086fc0d6fa46e3391265226baf9e9469c87ebd2206a24389
7
- data.tar.gz: 03a1bfdf1fbcaaa077e3ef52c69e8181622bd12ccd22c5bd53d4dbdde90d263237c3da39c76ff7a4f5c0b4c689b96dec0984011659e281b9fbcbd6c88b65bf21
6
+ metadata.gz: d941d50b9c96292732262452f71c127525e446b9b4aa10b146b8e1bf8488026d02f1816240bb1e5721d47947215d6dff566437a7b6bcf0c6675390b4d8453580
7
+ data.tar.gz: a9c8f3c8d09f8e38b738e78966bf8a585ea15851867e8ae837ced8ede3c8769d50248789c265bd604194a7e7e52557192e974aa2317c00aadfc59c398b52bedc
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-3.3.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: 72 hours, 60 days, 52 weeks, 48 months, then yearly
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 s3_path, access_key: "...", secret_key: "...", region: "..."`
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
 
@@ -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
@@ -3,7 +3,7 @@ require "active_support/core_ext/date_time/calculations"
3
3
  require "active_support/core_ext/integer/time"
4
4
 
5
5
  module Bard
6
- module Backup
6
+ class Backup
7
7
  class Deleter < Struct.new(:s3_dir, :now)
8
8
  def call
9
9
  s3_dir.delete files_to_delete
@@ -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
@@ -1,7 +1,7 @@
1
1
  require "backhoe"
2
2
 
3
3
  module Bard
4
- module Backup
4
+ class Backup
5
5
  class LocalBackhoe
6
6
  def self.call s3_dir, now
7
7
  filename = "#{now.iso8601}.sql.gz"
@@ -0,0 +1,12 @@
1
+ require "bard/backup"
2
+ require "rails"
3
+
4
+ module Bard
5
+ class Backup
6
+ class Railtie < Rails::Railtie
7
+ rake_tasks do
8
+ load "bard/backup/tasks.rake"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -2,8 +2,13 @@ require "aws-sdk-s3"
2
2
  require "rexml"
3
3
 
4
4
  module Bard
5
- module Backup
6
- class S3Dir < Data.define(:endpoint, :path, :access_key, :secret_key, :region)
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: access_key,
76
- secret_access_key: secret_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
-
@@ -0,0 +1,7 @@
1
+ namespace :bard do
2
+ desc "Backup the database to configured destinations"
3
+ task :backup => :environment do
4
+ require "bard/backup"
5
+ Bard::Backup.create!
6
+ end
7
+ end
@@ -1,6 +1,6 @@
1
1
  module Bard
2
- module Backup
3
- VERSION = "0.7.0"
2
+ class Backup
3
+ VERSION = "0.9.0"
4
4
  end
5
5
  end
6
6
 
data/lib/bard/backup.rb CHANGED
@@ -1,17 +1,37 @@
1
- require "bard/backup/s3_dir"
2
- require "bard/backup/local_backhoe"
3
- require "bard/backup/deleter"
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
- module Backup
7
- def self.call s3_path, region: "us-west-2", now: Time.now.utc, strategy: LocalBackhoe, **kwargs
8
- endpoint = kwargs[:endpoint] || "https://s3.#{region}.amazonaws.com"
9
- access_key = kwargs[:access_key_id] || kwargs[:access_key]
10
- secret_key = kwargs[:secret_access_key] || kwargs[:secret_key]
11
- s3_dir = S3Dir.new(endpoint:, path: s3_path, access_key:, secret_key:, region:)
12
- strategy.call(s3_dir, now)
13
- Deleter.new(s3_dir, now).call
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.7.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-07-01 00:00:00.000000000 Z
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: activesupport
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.5.11
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: []