bard-backup 0.8.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: 5727d5acc23ec75d951a70f062b7bf4da0bbc3ab010c3d96de502530f063497e
4
- data.tar.gz: d3d07429a57b4e2ea090216ae954e7e682f5f18c9ddeb44f93446af3b871dd53
3
+ metadata.gz: 27e2f3a66518e1d3449f448b15a587fb1839d7fb8d978e969ef583821dc01f9e
4
+ data.tar.gz: 3deae6b4c09d9492aa194f5511dc91d478de9311a559bf8620fdc0a6ca339ea1
5
5
  SHA512:
6
- metadata.gz: e6ccbdd2cef3c31c43165bdb4a206b95d5648aaff3722a71e2e7bb644170367b5b74639155e564ee379d0e70066fa9e5ccbf4fdddaf6185331bc2ce7334f6a51
7
- data.tar.gz: 9b0a36b4784e890b8c2c77a0e60f8c24ebf5ebd8515739bd9ad678c7237ba7d317bdd51a17847473f7519537664521a7b2165e426990290bfb93c29af18623b1
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
@@ -9,7 +9,7 @@ Bard::Backup does 3 things in a bard project
9
9
 
10
10
  ## Usage
11
11
 
12
- Run with `Bard::Backup.call path: "s3_bucket/optional_subfolder", 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,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
@@ -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
@@ -3,7 +3,7 @@ require "rexml"
3
3
 
4
4
  module Bard
5
5
  class Backup
6
- class S3Dir < Data.define(:endpoint, :path, :access_key, :secret_key, :region)
6
+ class S3Dir < Data.define(:endpoint, :path, :access_key_id, :secret_access_key, :region)
7
7
  def initialize **kwargs
8
8
  kwargs[:endpoint] ||= "https://s3.#{kwargs[:region]}.amazonaws.com"
9
9
  super
@@ -77,8 +77,8 @@ module Bard
77
77
  Aws::S3::Client.new({
78
78
  endpoint: endpoint,
79
79
  region: region,
80
- access_key_id: access_key,
81
- secret_access_key: secret_key,
80
+ access_key_id: access_key_id,
81
+ secret_access_key: secret_access_key,
82
82
  })
83
83
  end
84
84
  end
@@ -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
2
  class Backup
3
- VERSION = "0.8.0"
3
+ VERSION = "0.9.0"
4
4
  end
5
5
  end
6
6
 
data/lib/bard/backup.rb CHANGED
@@ -1,62 +1,37 @@
1
- require "bard/backup/s3_dir"
2
- require "bard/backup/local_backhoe"
3
- require "bard/backup/cached_local_backhoe"
4
- require "bard/backup/deleter"
1
+ require "bard/backup/destination"
2
+ require "bard/backup/latest_finder"
3
+ require "bard/backup/railtie" if defined?(Rails)
5
4
 
6
5
  module Bard
7
6
  class Backup
8
- def self.call configs
9
- configs = [configs] if configs.is_a?(Hash)
10
- configs.each do |config|
11
- new(config).call
7
+ def self.create!(destination_hashes = nil, **config)
8
+ if destination_hashes.nil? && !config.empty?
9
+ destination_hashes = [config]
12
10
  end
13
- end
14
-
15
- def initialize config
16
- @config = config
17
- end
18
- attr_reader :config
19
-
20
- def call
21
- strategy.call(s3_dir, now)
22
- Deleter.new(s3_dir, now).call
23
- end
24
-
25
- def s3_dir
26
- @s3_dir ||= S3Dir.new(endpoint:, path:, access_key:, secret_key:, region:)
27
- end
28
-
29
- def strategy
30
- return @strategy if @strategy
31
- @strategy = config.fetch(:strategy, LocalBackhoe)
32
- if @strategy.is_a?(String)
33
- @strategy = Bard::Backup.const_get(@strategy)
11
+ destination_hashes ||= Bard::Config.current.backup.destinations
12
+ Array(destination_hashes).each do |hash|
13
+ Destination.build(hash).call
34
14
  end
35
- @strategy
36
- end
37
-
38
- def path
39
- config.fetch(:path)
40
15
  end
41
16
 
42
- def access_key
43
- config[:access_key_id] || config[:access_key]
17
+ def self.latest
18
+ LatestFinder.new.call
44
19
  end
45
20
 
46
- def secret_key
47
- config[:secret_access_key] || config[:secret_key]
48
- end
49
-
50
- def region
51
- config.fetch(:region, "us-west-2")
52
- end
21
+ attr_reader :timestamp, :size, :destinations
53
22
 
54
- def now
55
- @now ||= config.fetch(:now, Time.now.utc)
23
+ def initialize(timestamp:, size: nil, destinations: [])
24
+ @timestamp = timestamp
25
+ @size = size
26
+ @destinations = destinations
56
27
  end
57
28
 
58
- def endpoint
59
- config.fetch(:endpoint, "https://s3.#{region}.amazonaws.com")
29
+ def as_json(*)
30
+ {
31
+ timestamp: timestamp&.iso8601,
32
+ size: size,
33
+ destinations: destinations
34
+ }.compact
60
35
  end
61
36
  end
62
37
  end
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.8.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-03 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,12 +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
84
97
  - lib/bard/backup/cached_local_backhoe.rb
85
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
86
103
  - lib/bard/backup/local_backhoe.rb
104
+ - lib/bard/backup/railtie.rb
87
105
  - lib/bard/backup/s3_dir.rb
106
+ - lib/bard/backup/tasks.rake
88
107
  - lib/bard/backup/version.rb
89
108
  - sig/bard/backup.rbs
90
109
  homepage: https://github.com/botandrose/bard-backup
@@ -92,7 +111,6 @@ licenses:
92
111
  - MIT
93
112
  metadata:
94
113
  homepage_uri: https://github.com/botandrose/bard-backup
95
- post_install_message:
96
114
  rdoc_options: []
97
115
  require_paths:
98
116
  - lib
@@ -107,8 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
125
  - !ruby/object:Gem::Version
108
126
  version: '0'
109
127
  requirements: []
110
- rubygems_version: 3.5.11
111
- signing_key:
128
+ rubygems_version: 3.6.2
112
129
  specification_version: 4
113
130
  summary: Provides automated db backups for bard projects
114
131
  test_files: []