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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +14 -0
- data/lib/noop_backup/commands/backup.rb +8 -8
- data/lib/noop_backup/configuration.rb +38 -4
- data/lib/noop_backup/notifiers/sentinel.rb +103 -0
- data/lib/noop_backup/notifiers/slack.rb +8 -3
- data/lib/noop_backup/notifiers/stdout.rb +2 -2
- data/lib/noop_backup/stores/s3.rb +6 -4
- data/lib/noop_backup/version.rb +1 -1
- data/lib/noop_backup.rb +3 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c6bdd09dd1ea2dc4826aa9e8e64f9d84f6c400baa5b000911de51d90c678167
|
|
4
|
+
data.tar.gz: 25722912661bd899bd56414d65cc68f9a33fc135225b74c0a4cc8c10f0cc091b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 || [
|
|
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(
|
|
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.
|
|
15
|
-
request.body = {text:
|
|
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
|
|
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
data/lib/noop_backup/version.rb
CHANGED
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(
|
|
56
|
+
def notify(result)
|
|
56
57
|
config.notifiers.each do |notifier|
|
|
57
|
-
notifier.notify(
|
|
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.
|
|
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
|