noop-backup 0.1.3 → 0.2.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: 478d0c1b90aca1c432382ecc87e6ee858f6bc5c47fbb563c22952add1c804c50
4
- data.tar.gz: 3965b585c105b4a84410d5e76ecaeaaab1f0f9ffcce1ff820cac044c351e0d78
3
+ metadata.gz: bba6625ab079fd41615d6ff304e7517743a67d2cb394a6644e058f3d93a8dd0e
4
+ data.tar.gz: 194c11d8d1e0cdb069f05a20b06342a1d421ae4178d3d918090a9f6fc182722a
5
5
  SHA512:
6
- metadata.gz: 2ed6ed19b704c0ecb39be159bbedf3579a3efe546940cb03d6a1b5c7bc2a536858cb12c6e1dd5606f349e9af6e62decf1771a492be0d8f0644ca2f622029fc97
7
- data.tar.gz: 5e32a0bb63c92a0900fa34a2bfeeaa2f529f549cf8483ba7f189a55e36b31bbf23128a226d6c5a1b93ea12d544cc55843e6c56a8be04f75c310280b0f9528fe0
6
+ metadata.gz: 201fdd9c78813f8f20d1e042d842dc76f2bfff07f94e71ce0ef926e6e606f549b7889075cae371602f33e6d095a1c854ce762daf21de7cbd7bee1a7bd32a730a
7
+ data.tar.gz: 313b0980120d3330cd6a37959813cd3fbd44f037369eab05fdff7c3a18dd8d06cd1207aaeeaac1c83eea9adb76ee1ed46303f5a1425392e7a7de3f34cfc65a82
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2026-07-04
4
+
5
+ - Support multiple destination stores in a single backup run
6
+ - Delete partial uploads and fail the job safely when a dump fails
7
+ - Validate configuration before running, with clearer errors
8
+ - A lot of undocumented API and configuration changes
9
+
3
10
  ## [0.1.3] - 2026-07-02
4
11
 
5
12
  - Pass `access_key_id` and `secret_access_key` in config block
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Ruby gem for backing up PostgreSQL databases with minimum effort.
4
4
 
5
+ This gem is very much __unstable__. Expect APIs to change a lot before v1.0.0 is released.
6
+
5
7
  ## Installation
6
8
 
7
9
  If using bundler, add the gem to your `Gemfile`:
@@ -24,11 +26,24 @@ The gem requires `pg_dump` to be installed on the machine that is running it.
24
26
 
25
27
  #### Rails 8 with Solid Queue
26
28
 
27
- (soon) NoopBackup will run automatically using the Solid Queue scheduler.
29
+ Add the NoopBackup job to the `config/recurring.yml` file:
30
+
31
+ ```yaml
32
+ production:
33
+ # other jobs
34
+ noop_backup:
35
+ class: NoopBackup::BackupJob
36
+ schedule: at 3am every day
37
+ ```
38
+
39
+ The job will run on the `:default` queue on your desired schedule.
40
+ If you wish to customize the job, create a new one and make sure to invoke
41
+ `NoopBackup::Commands::Backup.execute` in the `perform` method.
42
+
28
43
 
29
44
  #### Sidekiq, Good Job, whenever
30
45
 
31
- (soon)
46
+ Create a new recurring job and have it invoke `NoopBackup::Commands::Backup.execute`
32
47
 
33
48
  ### Manually
34
49
 
@@ -36,33 +51,23 @@ The gem requires `pg_dump` to be installed on the machine that is running it.
36
51
  bundle exec nbu backup
37
52
  ```
38
53
 
39
- This command will dump the database and stream it to S3 without writing anything to disk. Use any
40
- scheduler or even cron to run it periodically.
54
+ This command will dump the database and stream it to your configured destinations without writing
55
+ anything to disk. Use any scheduler or even cron to run it periodically.
41
56
 
42
57
  ## Configuration
43
58
 
44
- The gem needs AWS credentials and a bucket name:
45
-
46
- ```
47
- AWS_REGION=region
48
- AWS_ACCESS_KEY_ID=your-key
49
- AWS_SECRET_ACCESS_KEY=your-secret-key
50
- NBU_BUCKET=s3-bucket
51
- NBU_MIN_SIZE=2048
52
- ```
53
-
54
- If your app already has the AWS SDK set up, only the bucket needs to be configured.
55
-
56
- Alternatively, the gem can be configured with an initializer:
59
+ All configuration options can be edited in the initializer:
57
60
 
58
61
  ```rb
59
62
  # config/initializers/noop-backup.rb
60
63
 
61
64
  NoopBackup.configure do |config|
62
- config.bucket = 'bucket-name'
63
- config.region = 'eu-central-1'
64
- config.prefix = Rails.env
65
- config.min_size = 2048
65
+ config.register(:s3) do |store|
66
+ store.bucket = Settings.aws.bucket
67
+ store.region = Settings.aws.region
68
+ store.access_key_id = Settings.aws.access_key_id
69
+ store.secret_access_key = Settings.aws.secret_access_key
70
+ end
66
71
 
67
72
  config.notifier :slack do |slack|
68
73
  slack.webhook_url = 'https://hooks.slack.com/services/whatever'
@@ -83,11 +88,13 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/gmitre
83
88
  ## Wishlist
84
89
 
85
90
  - [ ] S3-compatible backends
86
- - [ ] file backends
91
+ - [ ] file backend
87
92
  - [ ] email notifier
88
93
  - [ ] SMS notifier
89
94
  - [ ] restore command
90
95
  - [ ] test command
96
+ - [ ] encryption
97
+ - [ ] delete stale
91
98
 
92
99
  ## License
93
100
 
@@ -7,9 +7,10 @@ module NoopBackup
7
7
  desc "backup", "Create and store a new backup"
8
8
  def backup
9
9
  NoopBackup.prepare!
10
- NoopBackup::Commands::Backup.execute
10
+ result = NoopBackup::Commands::Backup.execute
11
+ exit 1 unless result.success?
11
12
  rescue => e
12
- warn e.message
13
+ warn e
13
14
  exit 1
14
15
  end
15
16
  end
@@ -1,101 +1,135 @@
1
- require "aws-sdk-s3"
2
1
  require "open3"
3
2
 
4
3
  module NoopBackup::Commands
4
+ CommandResult = Struct.new(:error, :store_results, keyword_init: true) do
5
+ def success?
6
+ status == :success
7
+ end
8
+
9
+ def status
10
+ return :error if error
11
+ return :error if store_results.empty? || store_results.none?(&:success)
12
+ return :success if store_results.all?(&:success)
13
+
14
+ :partial_success
15
+ end
16
+
17
+ 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
25
+ end
26
+ end
27
+
5
28
  class Backup
6
- def self.execute
7
- new.execute
29
+ def self.execute(report: NoopBackup.config.report?)
30
+ result = new.execute
31
+
32
+ result.report if report
33
+
34
+ result
8
35
  end
9
36
 
10
37
  def initialize
11
- now = Time.now.utc
12
- @key = [
13
- config.prefix,
14
- config.pg_env["PGDATABASE"],
15
- now.strftime("%Y"),
16
- now.strftime("%m"),
17
- "#{now.strftime("%d-%H%M%S")}.dump"
18
- ].compact.join("/")
19
- @bytes = 0
20
- @started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
38
+ @key = generate_key
39
+ @store_results = []
40
+ @sinks = []
21
41
  end
22
42
 
23
43
  def execute
24
- commands = [
25
- [config.pg_env, "pg_dump", "--format=custom", "--no-owner"]
26
- ]
44
+ perform_sanity_check!
45
+
46
+ commands = [config.dump_command]
27
47
 
28
- commands << ["pv", "-btra"] if system("which", "pv", out: File::NULL, err: File::NULL)
48
+ # Pipe pg_dump through pv if installed for a basic progress report
49
+ commands << ["pv", "-btra"] if config.report? && system("which", "pv", out: File::NULL, err: File::NULL)
29
50
 
30
51
  Open3.pipeline_r(*commands) do |last_stdout, wait_threads|
31
- upload(last_stdout)
52
+ @sinks = config.stores.map do |store|
53
+ reader, writer = IO.pipe(binmode: true)
54
+
55
+ thread = Thread.new do
56
+ store.backup!(@key, reader)
57
+ rescue => e
58
+ # *always* return a Result, even if unexpected. Add store name for debugging.
59
+ NoopBackup::Stores::Result.new(success: false, error: e, store: store.class.name, key: @key)
60
+ ensure
61
+ reader.close
62
+ end
63
+
64
+ NoopBackup::Tee::Sink.new(store:, writer:, thread:)
65
+ end
32
66
 
33
- raise "pipeline failed" unless wait_threads.all? { |t| t.value.success? }
34
- end
67
+ sinks_fanout = NoopBackup::Tee.new(@sinks)
35
68
 
36
- duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started
69
+ begin
70
+ IO.copy_stream(last_stdout, sinks_fanout)
71
+ rescue => e
72
+ raise NoopBackup::DumpFailedError, "streaming failed: #{e.message}"
73
+ ensure
74
+ @sinks.each(&:close)
75
+ end
37
76
 
38
- if config.min_size && @bytes.to_i < config.min_size.to_i
39
- raise "backup too small: #{human_size(@bytes)} < min_size (#{human_size(config.min_size)})"
77
+ @store_results = @sinks.map(&:collect)
78
+
79
+ raise NoopBackup::DumpFailedError, "pipeline failed" unless wait_threads.all? { |t| t.value.success? }
40
80
  end
41
81
 
42
- config.notify(success_message(duration))
43
- rescue => e
44
- remove_partial_upload
82
+ CommandResult.new(store_results: @store_results)
83
+ rescue NoopBackup::DumpFailedError => error
84
+ # The dump stream itself failed, so uploads that finished are truncated copies of a bad stream — delete them.
85
+ # Collect the results first: if copy_stream raised, the collection line above never ran. Sinks are already
86
+ # closed on every DumpFailedError path, so collect can't block.
87
+ @store_results = @sinks.map(&:collect)
45
88
 
46
- config.notify("❌ Backup failed: #{e.message}")
89
+ cleanup_uploaded_stores!
47
90
 
48
- raise
91
+ CommandResult.new(error:, store_results: @store_results)
92
+ rescue => error
93
+ CommandResult.new(error:, store_results: @store_results)
49
94
  end
50
95
 
51
96
  private
52
97
 
53
- def config
54
- @config ||= NoopBackup.configuration
55
- end
98
+ # 1. Check if any stores are registered.
99
+ # 2. Make sure **all** stores have a valid configuration
100
+ def perform_sanity_check!
101
+ raise NoopBackup::ConfigurationError, "No backup stores registered" if config.stores.empty?
102
+ raise NoopBackup::ConfigurationError, "Could not resolve PGDATABASE" if config.pg_env["PGDATABASE"].to_s.empty?
56
103
 
57
- def s3_client
58
- @_s3_client ||= Aws::S3::Client.new(**config.s3_config)
104
+ config.stores.each(&:validate!)
59
105
  end
60
106
 
61
- # Prefer Aws::S3::TransferManager for streaming uploads if available.
62
- # Aws::S3::Resource.upload_stream is deprecated in newer versions
63
- def upload(stdout)
64
- if defined?(Aws::S3::TransferManager)
65
- manager = Aws::S3::TransferManager.new(client: s3_client)
66
-
67
- manager.upload_stream(bucket: config.bucket, key: @key, part_size: 8 * 1024 * 1024, thread_count: 2) do |s3_stream|
68
- @bytes = IO.copy_stream(stdout, s3_stream)
69
- end
70
- else
71
- object = Aws::S3::Resource.new(region: config.region).bucket(config.bucket).object(@key)
72
-
73
- object.upload_stream(part_size: 8 * 1024 * 1024, thread_count: 2) do |s3_stream|
74
- @bytes = IO.copy_stream(stdout, s3_stream)
75
- end
107
+ def cleanup_uploaded_stores!
108
+ @sinks.each do |sink|
109
+ sink.store.cleanup!(@key) if sink.collect&.success
110
+ rescue => e
111
+ NoopBackup.notify "⚠️ Cleanup failed for #{sink.store.class}: #{e.message}"
76
112
  end
77
113
  end
78
114
 
79
- def human_size(bytes)
80
- units = %w[B KB MB GB TB]
81
- size = bytes.to_f
82
- i = 0
83
- while size >= 1024 && i < units.size - 1
84
- size /= 1024
85
- i += 1
86
- end
87
- format("%.1f %s", size, units[i])
115
+ def config
116
+ @config ||= NoopBackup.config
88
117
  end
89
118
 
90
- def success_message(duration)
91
- "✅ #{config.pg_env["PGDATABASE"]} backed up successfully — " \
92
- "#{human_size(@bytes)} in #{duration.round(1)}s → #{config.bucket}/#{@key}"
93
- end
119
+ # File name of the current backup. Example:
120
+ #
121
+ # prefix db_name y m d h m s ms
122
+ # /database/db_name_production/2026/07/03-14-47-23-724.dump
123
+ def generate_key
124
+ now = Time.now.utc
94
125
 
95
- def remove_partial_upload
96
- s3_client.delete_object(bucket: config.bucket, key: @key)
97
- rescue => e
98
- warn "Failed to clean up partial upload #{@key}: #{e.message}"
126
+ [
127
+ config.prefix,
128
+ config.pg_env["PGDATABASE"],
129
+ now.strftime("%Y"),
130
+ now.strftime("%m"),
131
+ "#{now.strftime("%d-%H-%M-%S-%L")}.dump"
132
+ ].compact.join("/")
99
133
  end
100
134
  end
101
135
  end
@@ -1,22 +1,44 @@
1
1
  module NoopBackup
2
2
  class Configuration
3
- # NOTE: These are all going to change a lot before version 1.0.0
4
- attr_accessor :bucket,
5
- :region,
6
- :access_key_id,
7
- :secret_access_key,
8
- :prefix,
3
+ attr_accessor :prefix,
4
+ :min_size,
9
5
  :pg_host,
10
6
  :pg_port,
11
7
  :pg_user,
12
8
  :pg_password,
13
- :pg_database,
14
- :min_size
9
+ :pg_database
10
+
11
+ attr_reader :stores, :notifiers
12
+ attr_writer :report, :dump_command
15
13
 
16
14
  def initialize
17
- @prefix = "database"
15
+ @prefix = ENV.fetch("NBU_PREFIX", "database")
16
+ @stores = []
17
+ @min_size = ENV.fetch("NBU_MIN_SIZE", 2048).to_i
18
18
  @notifiers = [NoopBackup::Notifiers::Stdout.new]
19
- @min_size = ENV.fetch("NBU_MIN_SIZE", 1024).to_i
19
+ @report = true
20
+ end
21
+
22
+ def report?
23
+ @report
24
+ end
25
+
26
+ def dump_command
27
+ @dump_command || [pg_env, "pg_dump", "--format=custom", "--no-owner"]
28
+ end
29
+
30
+ def register(store_type)
31
+ raise NoopBackup::ConfigurationError, "`config.register` requires a block" unless block_given?
32
+
33
+ store =
34
+ case store_type.to_sym
35
+ when :s3 then NoopBackup::Stores::S3.new
36
+ else raise NoopBackup::ConfigurationError, "unknown store type: #{store_type}"
37
+ end
38
+
39
+ @stores << store
40
+
41
+ yield store
20
42
  end
21
43
 
22
44
  def notifier(type)
@@ -31,27 +53,13 @@ module NoopBackup
31
53
  end
32
54
  end
33
55
 
34
- def notify(message)
35
- @notifiers.each do |notifier|
36
- notifier.notify(message)
37
- end
38
- end
39
-
40
56
  def pg_env
41
57
  {
42
- "PGHOST" => pg_host || db_config[:host]&.to_s || "localhost",
43
- "PGPORT" => pg_port || db_config[:port]&.to_s,
44
- "PGUSER" => pg_user || db_config[:username]&.to_s,
45
- "PGPASSWORD" => pg_password || db_config[:password]&.to_s,
46
- "PGDATABASE" => pg_database || db_config[:database].to_s
47
- }.compact
48
- end
49
-
50
- def s3_config
51
- {
52
- region: region,
53
- access_key_id: access_key_id,
54
- secret_access_key: secret_access_key
58
+ "PGHOST" => (pg_host || db_config[:host])&.to_s,
59
+ "PGPORT" => (pg_port || db_config[:port])&.to_s,
60
+ "PGUSER" => (pg_user || db_config[:username])&.to_s,
61
+ "PGPASSWORD" => (pg_password || db_config[:password])&.to_s,
62
+ "PGDATABASE" => (pg_database || db_config[:database])&.to_s
55
63
  }.compact
56
64
  end
57
65
 
@@ -59,9 +67,5 @@ module NoopBackup
59
67
  @db_config ||= defined?(::ActiveRecord) ?
60
68
  ::ActiveRecord::Base.connection_db_config.configuration_hash : {}
61
69
  end
62
-
63
- # TODO: raise if config invalid (e.g. bucket missing)
64
- def validate!
65
- end
66
70
  end
67
71
  end
@@ -1,9 +1,11 @@
1
1
  module NoopBackup
2
2
  class BackupJob < ActiveJob::Base
3
- queue_as :backups
3
+ queue_as :default
4
4
 
5
5
  def perform
6
- NoopBackup::Commands::Backup.execute
6
+ result = NoopBackup::Commands::Backup.execute
7
+
8
+ raise NoopBackup::BackupFailedError.new(result) unless result.success?
7
9
  end
8
10
  end
9
11
  end
@@ -0,0 +1,11 @@
1
+ module NoopBackup::Stores
2
+ Result = Struct.new(:success, :error, :store, :key, :bytes, :duration, keyword_init: true) do
3
+ def message
4
+ if success
5
+ "✅ [#{store}] Backup successful — #{NoopBackup.utils.human_size(bytes)} in #{duration.round(1)}s → /#{key}"
6
+ else
7
+ "❌ [#{store}] Backup failed: #{error.message}"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,82 @@
1
+ require "aws-sdk-s3"
2
+
3
+ module NoopBackup::Stores
4
+ class S3 < Store
5
+ attr_accessor :bucket, :region, :access_key_id, :secret_access_key
6
+
7
+ def initialize
8
+ @bucket = ENV["AWS_S3_BUCKET"]
9
+ @region = ENV["AWS_REGION"]
10
+ @access_key_id = ENV["AWS_ACCESS_KEY_ID"]
11
+ @secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"]
12
+ end
13
+
14
+ # Prefer Aws::S3::TransferManager for streaming uploads if available.
15
+ # Aws::S3::Resource.upload_stream is deprecated in newer versions
16
+ def backup!(key, stream)
17
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
18
+ bytes = 0
19
+
20
+ validate!
21
+
22
+ upload_attempted = true
23
+
24
+ if defined?(Aws::S3::TransferManager)
25
+ manager = Aws::S3::TransferManager.new(client: s3_client)
26
+
27
+ manager.upload_stream(bucket:, key: key, part_size: 8 * 1024 * 1024, thread_count: 2) do |s3_stream|
28
+ bytes = IO.copy_stream(stream, s3_stream)
29
+ end
30
+ else
31
+ object = Aws::S3::Resource.new(client: s3_client).bucket(bucket).object(key)
32
+
33
+ object.upload_stream(part_size: 8 * 1024 * 1024, thread_count: 2) do |s3_stream|
34
+ bytes = IO.copy_stream(stream, s3_stream)
35
+ end
36
+ end
37
+
38
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
39
+
40
+ if config.min_size && bytes < config.min_size.to_i
41
+ raise NoopBackup::DumpTooSmallError, "backup too small: #{NoopBackup.utils.human_size(bytes)} < min_size (#{NoopBackup.utils.human_size(config.min_size)})"
42
+ end
43
+
44
+ Result.new(success: true, store: :s3, bytes:, key:, duration:)
45
+ rescue => e
46
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
47
+
48
+ cleanup!(key) if upload_attempted
49
+
50
+ Result.new(success: false, error: e, store: :s3, bytes:, key:, duration:)
51
+ end
52
+
53
+ def validate!
54
+ raise NoopBackup::ConfigurationError, "bucket is not configured" if bucket.to_s.empty?
55
+
56
+ if access_key_id.to_s.empty? != secret_access_key.to_s.empty?
57
+ raise NoopBackup::ConfigurationError,
58
+ "access_key_id and secret_access_key must both be set, or both left blank to use the default AWS credential chain"
59
+ end
60
+ end
61
+
62
+ def cleanup!(key)
63
+ s3_client.delete_object(bucket:, key: key)
64
+ rescue => e
65
+ warn "Failed to clean up partial upload #{key}: #{e.message}"
66
+ end
67
+
68
+ private
69
+
70
+ def s3_client
71
+ @_s3_client ||= Aws::S3::Client.new(**s3_config)
72
+ end
73
+
74
+ def s3_config
75
+ {
76
+ region: region,
77
+ access_key_id: access_key_id,
78
+ secret_access_key: secret_access_key
79
+ }.compact
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,9 @@
1
+ module NoopBackup::Stores
2
+ class Store
3
+ private
4
+
5
+ def config
6
+ @config ||= NoopBackup.config
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ require_relative "stores/store"
2
+ require_relative "stores/result"
3
+ require_relative "stores/s3" # TODO: load conditionally
@@ -0,0 +1,33 @@
1
+ module NoopBackup
2
+ class Tee
3
+ Sink = Struct.new(:store, :writer, :thread, keyword_init: true) do
4
+ def write(chunk)
5
+ return if @error
6
+
7
+ writer.write(chunk)
8
+ rescue Errno::EPIPE, IOError => e
9
+ @error = e
10
+
11
+ close unless writer.closed?
12
+ end
13
+
14
+ def close
15
+ writer.close
16
+ end
17
+
18
+ def collect
19
+ thread.value
20
+ rescue
21
+ nil
22
+ end
23
+ end
24
+
25
+ def initialize(sinks) = @sinks = sinks
26
+
27
+ def write(chunk)
28
+ @sinks.each { |sink| sink.write(chunk) }
29
+
30
+ chunk.bytesize
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,14 @@
1
+ module NoopBackup::Utils
2
+ extend self
3
+
4
+ def human_size(bytes)
5
+ units = %w[B KB MB GB TB]
6
+ size = bytes.to_f
7
+ i = 0
8
+ while size >= 1024 && i < units.size - 1
9
+ size /= 1024
10
+ i += 1
11
+ end
12
+ format("%.1f %s", size, units[i])
13
+ end
14
+ end
@@ -1,3 +1,3 @@
1
1
  module NoopBackup
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/noop_backup.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  require_relative "noop_backup/version"
2
2
  require_relative "noop_backup/configuration"
3
+ require_relative "noop_backup/tee"
4
+ require_relative "noop_backup/utils"
5
+ require_relative "noop_backup/stores"
3
6
  require_relative "noop_backup/commands/backup"
4
7
  require_relative "noop_backup/notifiers/slack"
5
8
  require_relative "noop_backup/notifiers/stdout"
@@ -7,6 +10,26 @@ require_relative "noop_backup/notifiers/stdout"
7
10
  module NoopBackup
8
11
  class Error < StandardError; end
9
12
 
13
+ class DumpTooSmallError < Error; end
14
+ class DumpFailedError < Error; end
15
+ class ConfigurationError < Error; end
16
+
17
+ class BackupFailedError < Error
18
+ attr_reader :result
19
+
20
+ def initialize(result)
21
+ @result = result
22
+
23
+ super("Backup failed (#{result.status}): #{result.error&.message || failed_stores(result)}")
24
+ end
25
+
26
+ private
27
+
28
+ def failed_stores(result)
29
+ result.store_results.reject(&:success).map(&:store).join(", ")
30
+ end
31
+ end
32
+
10
33
  class << self
11
34
  def configuration
12
35
  @configuration ||= Configuration.new
@@ -14,6 +37,10 @@ module NoopBackup
14
37
 
15
38
  alias_method :config, :configuration
16
39
 
40
+ def reset!
41
+ @configuration = nil
42
+ end
43
+
17
44
  def configure
18
45
  yield(configuration)
19
46
  end
@@ -24,6 +51,16 @@ module NoopBackup
24
51
 
25
52
  require env_file if File.exist?(env_file)
26
53
  end
54
+
55
+ def notify(message)
56
+ config.notifiers.each do |notifier|
57
+ notifier.notify(message)
58
+ end
59
+ end
60
+
61
+ def utils
62
+ NoopBackup::Utils
63
+ end
27
64
  end
28
65
  end
29
66
 
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.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Georgi Mitrev
@@ -60,6 +60,12 @@ files:
60
60
  - lib/noop_backup/notifiers/slack.rb
61
61
  - lib/noop_backup/notifiers/stdout.rb
62
62
  - lib/noop_backup/plugins/rails.rb
63
+ - lib/noop_backup/stores.rb
64
+ - lib/noop_backup/stores/result.rb
65
+ - lib/noop_backup/stores/s3.rb
66
+ - lib/noop_backup/stores/store.rb
67
+ - lib/noop_backup/tee.rb
68
+ - lib/noop_backup/utils.rb
63
69
  - lib/noop_backup/version.rb
64
70
  homepage: https://github.com/gmitrev/noop-backup
65
71
  licenses: