2026-05-07 15:13:57 +00:00
|
|
|
#!/usr/bin/env ruby
|
|
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
|
|
# notify_backup — send a backup run result to the n8n backup monitor
|
|
|
|
|
#
|
|
|
|
|
# USAGE
|
|
|
|
|
# notify_backup.rb [options]
|
|
|
|
|
#
|
|
|
|
|
# REQUIRED OPTIONS
|
|
|
|
|
# --machine NAME Identifier for this host (e.g. desktop, vps-ovh-prod-01)
|
|
|
|
|
# --destination NAME Backup destination (e.g. synology, backblaze-b2, local)
|
|
|
|
|
# --status STATUS Result: success | failure | skipped
|
|
|
|
|
#
|
|
|
|
|
# OPTIONAL OPTIONS
|
|
|
|
|
# --bytes N Bytes transferred (raw integer; formatted to human-readable before sending)
|
|
|
|
|
# --snapshot ID Restic snapshot ID or equivalent
|
|
|
|
|
# --log-path PATH Path to the backup log file for this run
|
|
|
|
|
# --notes TEXT Free-form output (restic tail, skip reason, error message, etc.)
|
|
|
|
|
#
|
|
|
|
|
# REQUIRED ENV VARS
|
|
|
|
|
# N8N_SWANSONCLOUD_URL Base URL of the n8n instance (no trailing slash)
|
|
|
|
|
# e.g. https://n8n.swansoncloud.com
|
|
|
|
|
# N8N_SWANSONCLOUD_BACKUP_AUTH_TOKEN Bearer token for webhook authentication
|
|
|
|
|
#
|
|
|
|
|
# EXIT BEHAVIOR
|
|
|
|
|
# Always exits 0. A notification failure is logged to stderr but never causes
|
|
|
|
|
# the calling backup process to fail — the backup ran; the log is the source of truth.
|
|
|
|
|
#
|
|
|
|
|
# ADDING A NEW BACKUP SOURCE
|
|
|
|
|
# 1. Ensure the two ENV vars above are available on the new host
|
|
|
|
|
# 2. Call this script at the end of the backup process with --machine, --destination, --status
|
|
|
|
|
# 3. Add the {machine, destination} pair to the Set: Expected Sources node in n8n Workflow 2
|
|
|
|
|
#
|
|
|
|
|
# CLASSES
|
|
|
|
|
# ByteFormatter raw integer bytes → human-readable string (e.g. "4.50 GB")
|
|
|
|
|
# PayloadBuilder converts CLI option hash into a clean JSON-ready hash
|
|
|
|
|
# WebhookClient sends HTTP POST with auth header; knows nothing about backup data
|
|
|
|
|
# BackupNotifier orchestrates: build payload → send; the only public entry point
|
|
|
|
|
|
|
|
|
|
require 'net/http'
|
|
|
|
|
require 'json'
|
|
|
|
|
require 'optparse'
|
|
|
|
|
|
|
|
|
|
class ByteFormatter
|
2026-05-07 15:15:15 +00:00
|
|
|
UNITS = [['GB', 1_073_741_824], ['MB', 1_048_576], ['KB', 1_024]].freeze
|
|
|
|
|
|
|
|
|
|
def self.format(bytes)
|
|
|
|
|
return nil if bytes.nil?
|
|
|
|
|
|
|
|
|
|
bytes = bytes.to_i
|
|
|
|
|
unit, divisor = UNITS.find { |_, d| bytes >= d }
|
|
|
|
|
return "#{bytes} B" unless unit
|
|
|
|
|
|
|
|
|
|
'%.2f %s' % [bytes.to_f / divisor, unit]
|
|
|
|
|
end
|
2026-05-07 15:13:57 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
class PayloadBuilder
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
class WebhookClient
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
class BackupNotifier
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if __FILE__ == $PROGRAM_NAME
|
|
|
|
|
# CLI entry point — implemented in Task 5
|
|
|
|
|
end
|