noop-backup 0.2.0 → 0.3.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: bba6625ab079fd41615d6ff304e7517743a67d2cb394a6644e058f3d93a8dd0e
4
- data.tar.gz: 194c11d8d1e0cdb069f05a20b06342a1d421ae4178d3d918090a9f6fc182722a
3
+ metadata.gz: 6c6bdd09dd1ea2dc4826aa9e8e64f9d84f6c400baa5b000911de51d90c678167
4
+ data.tar.gz: 25722912661bd899bd56414d65cc68f9a33fc135225b74c0a4cc8c10f0cc091b
5
5
  SHA512:
6
- metadata.gz: 201fdd9c78813f8f20d1e042d842dc76f2bfff07f94e71ce0ef926e6e606f549b7889075cae371602f33e6d095a1c854ce762daf21de7cbd7bee1a7bd32a730a
7
- data.tar.gz: 313b0980120d3330cd6a37959813cd3fbd44f037369eab05fdff7c3a18dd8d06cd1207aaeeaac1c83eea9adb76ee1ed46303f5a1425392e7a7de3f34cfc65a82
6
+ metadata.gz: 645e48fe012407117c84a0293d4eea26f9c5f5103ee4b3ed266bb7a5fa2ebcb5c48e967921d1490f6d701361fa6bd92aaddd46e16e077738863918050ddb6d47
7
+ data.tar.gz: 2bc43c9e34ed129bc3c87f54d9b29ad7e38d5aea564824f30c490bb818a4e1719fb17f07a53bfb021d244755f511208d837caeec5f269155228b78a3b9f0e0e3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-07-13
4
+
5
+ - Make s3 upload stream part size and thread count configurable
6
+ - Sentinel integration
7
+ - Option to ignore tables
8
+
3
9
  ## [0.2.0] - 2026-07-04
4
10
 
5
11
  - Support multiple destination stores in a single backup run
data/README.md CHANGED
@@ -75,6 +75,20 @@ NoopBackup.configure do |config|
75
75
  end
76
76
  ```
77
77
 
78
+ ### Ignoring tables
79
+
80
+ To skip the rows of tables you don't need backed up, such as audit trails or job history:
81
+
82
+ ```rb
83
+ NoopBackup.configure do |config|
84
+ config.ignore_tables = %w[versions logs]
85
+ end
86
+ ```
87
+
88
+ Or set `NBU_IGNORE_TABLES=versions,logs`.
89
+
90
+ A restore still creates these tables empty.
91
+
78
92
  ## Development
79
93
 
80
94
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -14,14 +14,14 @@ module NoopBackup::Commands
14
14
  :partial_success
15
15
  end
16
16
 
17
+ def messages
18
+ return ["❌ Fatal error: #{error.message}"] if error
19
+
20
+ store_results.map(&:message)
21
+ end
22
+
17
23
  def report
18
- if error
19
- NoopBackup.notify "❌ Fatal error: #{error.message}"
20
- else
21
- store_results.each do |result|
22
- NoopBackup.notify result.message
23
- end
24
- end
24
+ NoopBackup.notify(self)
25
25
  end
26
26
  end
27
27
 
@@ -108,7 +108,7 @@ module NoopBackup::Commands
108
108
  @sinks.each do |sink|
109
109
  sink.store.cleanup!(@key) if sink.collect&.success
110
110
  rescue => e
111
- NoopBackup.notify "⚠️ Cleanup failed for #{sink.store.class}: #{e.message}"
111
+ warn "⚠️ Cleanup failed for #{sink.store.class}: #{e.message}"
112
112
  end
113
113
  end
114
114
 
@@ -1,15 +1,18 @@
1
1
  module NoopBackup
2
2
  class Configuration
3
+ DEFAULT_SENTINEL_HOST = "https://boringbackup.com"
4
+
3
5
  attr_accessor :prefix,
4
6
  :min_size,
5
7
  :pg_host,
6
8
  :pg_port,
7
9
  :pg_user,
8
10
  :pg_password,
9
- :pg_database
11
+ :pg_database,
12
+ :sentinel_key
10
13
 
11
- attr_reader :stores, :notifiers
12
- attr_writer :report, :dump_command
14
+ attr_reader :stores
15
+ attr_writer :report, :dump_command, :ignore_tables, :sentinel_host
13
16
 
14
17
  def initialize
15
18
  @prefix = ENV.fetch("NBU_PREFIX", "database")
@@ -17,14 +20,45 @@ module NoopBackup
17
20
  @min_size = ENV.fetch("NBU_MIN_SIZE", 2048).to_i
18
21
  @notifiers = [NoopBackup::Notifiers::Stdout.new]
19
22
  @report = true
23
+ @ignore_tables = ENV.fetch("NBU_IGNORE_TABLES", "").split(",")
24
+ @sentinel_key = ENV["NBU_SENTINEL_KEY"]
25
+ @sentinel_host = ENV.fetch("NBU_SENTINEL_HOST", DEFAULT_SENTINEL_HOST)
20
26
  end
21
27
 
22
28
  def report?
23
29
  @report
24
30
  end
25
31
 
32
+ def sentinel_host
33
+ @sentinel_host || DEFAULT_SENTINEL_HOST
34
+ end
35
+
36
+ def sentinel?
37
+ !sentinel_key.to_s.empty?
38
+ end
39
+
40
+ def sentinel
41
+ return unless sentinel?
42
+
43
+ @sentinel ||= NoopBackup::Notifiers::Sentinel.new(key: sentinel_key, host: sentinel_host)
44
+ end
45
+
46
+ def notifiers
47
+ [*@notifiers, sentinel].compact
48
+ end
49
+
50
+ def ignore_tables
51
+ Array(@ignore_tables).map { |table| table.to_s.strip }.reject(&:empty?).uniq
52
+ end
53
+
26
54
  def dump_command
27
- @dump_command || [pg_env, "pg_dump", "--format=custom", "--no-owner"]
55
+ @dump_command || [
56
+ pg_env,
57
+ "pg_dump",
58
+ "--format=custom",
59
+ "--no-owner",
60
+ *ignore_tables.map { |table| "--exclude-table-data=#{table}" }
61
+ ]
28
62
  end
29
63
 
30
64
  def register(store_type)
@@ -0,0 +1,103 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "json"
4
+
5
+ module NoopBackup::Notifiers
6
+ class Sentinel
7
+ PAYLOAD_VERSION = 1
8
+
9
+ OPEN_TIMEOUT = 5
10
+ READ_TIMEOUT = 10
11
+ MAX_ATTEMPTS = 3
12
+
13
+ LOCAL_HOSTS = %w[localhost 127.0.0.1 0.0.0.0]
14
+
15
+ attr_accessor :key, :host
16
+
17
+ def initialize(key:, host:)
18
+ @key = key
19
+ @host = host
20
+ end
21
+
22
+ def ping_url
23
+ URI.join(normalized_host, "/ping/#{key}")
24
+ end
25
+
26
+ def notify(result)
27
+ body = payload(result).to_json
28
+ attempt = 0
29
+
30
+ begin
31
+ attempt += 1
32
+ response = post(body)
33
+
34
+ return true if response.is_a?(Net::HTTPSuccess)
35
+ raise NoopBackup::Error, "#{response.code} #{response.body}" if retryable?(response)
36
+
37
+ warn "Sentinel ping failed: #{response.code} #{response.body}"
38
+
39
+ false
40
+ rescue => e
41
+ if attempt < MAX_ATTEMPTS
42
+ sleep(2**(attempt - 1))
43
+ retry
44
+ end
45
+
46
+ warn "Sentinel ping error after #{attempt} attempts: #{e.message}"
47
+
48
+ false
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def normalized_host
55
+ uri = URI.parse(host)
56
+
57
+ return host if uri.is_a?(URI::HTTP)
58
+
59
+ "#{LOCAL_HOSTS.include?(uri.scheme) ? "http" : "https"}://#{host}"
60
+ end
61
+
62
+ def post(body)
63
+ uri = ping_url
64
+
65
+ http = Net::HTTP.new(uri.host, uri.port)
66
+ http.use_ssl = uri.scheme == "https"
67
+ http.open_timeout = OPEN_TIMEOUT
68
+ http.read_timeout = READ_TIMEOUT
69
+
70
+ request = Net::HTTP::Post.new(uri.request_uri, "Content-Type" => "application/json")
71
+ request.body = body
72
+
73
+ http.request(request)
74
+ end
75
+
76
+ def retryable?(response)
77
+ response.is_a?(Net::HTTPServerError) || response.is_a?(Net::HTTPTooManyRequests)
78
+ end
79
+
80
+ def payload(result)
81
+ store_results = result.store_results
82
+
83
+ {
84
+ version: PAYLOAD_VERSION,
85
+ status: result.status,
86
+ error: result.error&.message,
87
+ database: NoopBackup.config.pg_env["PGDATABASE"],
88
+ bytes: store_results.filter_map(&:bytes).max,
89
+ duration: store_results.filter_map(&:duration).max&.round(3),
90
+ stores: store_results.map do |store_result|
91
+ {
92
+ store: store_result.store,
93
+ success: store_result.success,
94
+ key: store_result.key,
95
+ bytes: store_result.bytes,
96
+ duration: store_result.duration&.round(3),
97
+ error: store_result.error&.message
98
+ }
99
+ end
100
+ }
101
+ end
102
+ end
103
+ end
@@ -4,15 +4,20 @@ require "json"
4
4
 
5
5
  module NoopBackup::Notifiers
6
6
  class Slack
7
+ OPEN_TIMEOUT = 5
8
+ READ_TIMEOUT = 10
9
+
7
10
  attr_accessor :webhook_url
8
11
 
9
- def notify(text)
12
+ def notify(result)
10
13
  uri = URI(webhook_url)
11
14
  http = Net::HTTP.new(uri.host, uri.port)
12
15
  http.use_ssl = true
16
+ http.open_timeout = OPEN_TIMEOUT
17
+ http.read_timeout = READ_TIMEOUT
13
18
 
14
- request = Net::HTTP::Post.new(uri.path, "Content-Type" => "application/json")
15
- request.body = {text: text}.to_json
19
+ request = Net::HTTP::Post.new(uri.request_uri, "Content-Type" => "application/json")
20
+ request.body = {text: result.messages.join("\n")}.to_json
16
21
 
17
22
  response = http.request(request)
18
23
 
@@ -1,7 +1,7 @@
1
1
  module NoopBackup::Notifiers
2
2
  class Stdout
3
- def notify(text)
4
- puts text
3
+ def notify(result)
4
+ result.messages.each { |message| puts message }
5
5
  end
6
6
  end
7
7
  end
@@ -2,13 +2,15 @@ require "aws-sdk-s3"
2
2
 
3
3
  module NoopBackup::Stores
4
4
  class S3 < Store
5
- attr_accessor :bucket, :region, :access_key_id, :secret_access_key
5
+ attr_accessor :bucket, :region, :access_key_id, :secret_access_key, :part_size, :thread_count
6
6
 
7
7
  def initialize
8
8
  @bucket = ENV["AWS_S3_BUCKET"]
9
9
  @region = ENV["AWS_REGION"]
10
10
  @access_key_id = ENV["AWS_ACCESS_KEY_ID"]
11
11
  @secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"]
12
+ @part_size = ENV.fetch("NBU_S3_PART_SIZE", 8 * 1024 * 1024).to_i
13
+ @thread_count = ENV.fetch("NBU_S3_THREAD_COUNT", 2).to_i
12
14
  end
13
15
 
14
16
  # Prefer Aws::S3::TransferManager for streaming uploads if available.
@@ -24,13 +26,13 @@ module NoopBackup::Stores
24
26
  if defined?(Aws::S3::TransferManager)
25
27
  manager = Aws::S3::TransferManager.new(client: s3_client)
26
28
 
27
- manager.upload_stream(bucket:, key: key, part_size: 8 * 1024 * 1024, thread_count: 2) do |s3_stream|
29
+ manager.upload_stream(bucket:, key: key, part_size:, thread_count:) do |s3_stream|
28
30
  bytes = IO.copy_stream(stream, s3_stream)
29
31
  end
30
32
  else
31
33
  object = Aws::S3::Resource.new(client: s3_client).bucket(bucket).object(key)
32
34
 
33
- object.upload_stream(part_size: 8 * 1024 * 1024, thread_count: 2) do |s3_stream|
35
+ object.upload_stream(part_size:, thread_count:) do |s3_stream|
34
36
  bytes = IO.copy_stream(stream, s3_stream)
35
37
  end
36
38
  end
@@ -60,7 +62,7 @@ module NoopBackup::Stores
60
62
  end
61
63
 
62
64
  def cleanup!(key)
63
- s3_client.delete_object(bucket:, key: key)
65
+ s3_client.delete_object(bucket:, key:)
64
66
  rescue => e
65
67
  warn "Failed to clean up partial upload #{key}: #{e.message}"
66
68
  end
@@ -1,3 +1,3 @@
1
1
  module NoopBackup
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/noop_backup.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "noop_backup/stores"
6
6
  require_relative "noop_backup/commands/backup"
7
7
  require_relative "noop_backup/notifiers/slack"
8
8
  require_relative "noop_backup/notifiers/stdout"
9
+ require_relative "noop_backup/notifiers/sentinel"
9
10
 
10
11
  module NoopBackup
11
12
  class Error < StandardError; end
@@ -52,9 +53,9 @@ module NoopBackup
52
53
  require env_file if File.exist?(env_file)
53
54
  end
54
55
 
55
- def notify(message)
56
+ def notify(result)
56
57
  config.notifiers.each do |notifier|
57
- notifier.notify(message)
58
+ notifier.notify(result)
58
59
  end
59
60
  end
60
61
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: noop-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Georgi Mitrev
@@ -57,6 +57,7 @@ files:
57
57
  - lib/noop_backup/commands/backup.rb
58
58
  - lib/noop_backup/configuration.rb
59
59
  - lib/noop_backup/jobs/backup_job.rb
60
+ - lib/noop_backup/notifiers/sentinel.rb
60
61
  - lib/noop_backup/notifiers/slack.rb
61
62
  - lib/noop_backup/notifiers/stdout.rb
62
63
  - lib/noop_backup/plugins/rails.rb