mirror of
https://codeberg.org/puppe/mpbackup.git
synced 2025-12-19 21:42:17 +01:00
187 lines
4.9 KiB
Ruby
187 lines
4.9 KiB
Ruby
#!/usr/bin/env ruby
|
||
require 'io/console'
|
||
require 'optparse'
|
||
require 'pathname'
|
||
require 'yaml'
|
||
|
||
DEFAULT_CONFIG_FILE = Pathname.new('/etc/mpbackup/config.yaml')
|
||
NAME = 'mpbackup'
|
||
VERSION = '0.4.0'
|
||
|
||
$original_cache_home = ENV['XDG_CACHE_HOME']
|
||
|
||
def set_restic_vars(config)
|
||
if config['repository-file']
|
||
puts "Reading repository from file #{config['repository-file']} …"
|
||
repo = File.open(config['repository-file'], &:gets).chomp
|
||
elsif config['repository']
|
||
repo = config['repository']
|
||
else
|
||
error(1, 'No repository has been specified.')
|
||
end
|
||
if config['password-file']
|
||
puts "Reading password from file #{config['password-file']} …"
|
||
password = File.open(config['password-file'], &:gets).chomp
|
||
else
|
||
password = STDIN.getpass('Please put in your restic password: ')
|
||
end
|
||
if config['cache-home']
|
||
puts "Setting XDG_CACHE_HOME to #{config['cache-home']} …"
|
||
ENV['XDG_CACHE_HOME'] = config['cache-home']
|
||
end
|
||
|
||
ENV['RESTIC_REPOSITORY'] = repo
|
||
ENV['RESTIC_PASSWORD'] = password
|
||
end
|
||
|
||
def unset_restic_vars
|
||
ENV.delete('RESTIC_REPOSITORY')
|
||
ENV.delete('RESTIC_PASSWORD')
|
||
if $original_cache_home
|
||
ENV['XDG_CACHE_HOME'] = $original_cache_home
|
||
else
|
||
ENV.delete('XDG_CACHE_HOME')
|
||
end
|
||
end
|
||
|
||
def error(exit_status, message)
|
||
STDERR.puts("Error: #{message}")
|
||
exit exit_status
|
||
end
|
||
|
||
def check
|
||
puts 'Checking restic repo …'
|
||
check_command = ['restic', 'check']
|
||
puts("Command: #{check_command.join(' ')}")
|
||
system(*check_command)
|
||
if $?.exitstatus > 0
|
||
error(1, "Checking restic repository failed.")
|
||
end
|
||
end
|
||
|
||
def backup(config)
|
||
puts "Backing up with restic …"
|
||
# https://restic.readthedocs.io/en/latest/040_backup.html#including-and-excluding-files
|
||
exclude = config.dig('backup', 'exclude')&.flat_map{|e| ['--exclude', e]} || []
|
||
paths = config.dig('backup', 'paths') || []
|
||
backup_command = ['restic', 'backup', *exclude, *paths]
|
||
puts("Command: #{backup_command.join(' ')}")
|
||
system(*backup_command)
|
||
if $?.exitstatus > 0
|
||
error(1, 'Failed to do backup.')
|
||
end
|
||
|
||
check
|
||
|
||
if config.dig('forget', 'enable')
|
||
puts 'Forgetting unnecessary snapshots …'
|
||
flags = config['forget'].select{|k,v| k != 'enable'}
|
||
flags = flags.flat_map{|k,v| ['--' + k, v.to_s]}
|
||
forget_command = ['restic', 'forget'] + flags
|
||
puts("Command: #{forget_command.join(' ')}")
|
||
system(*forget_command)
|
||
# Data will only be deleted when `restic prune` is executed or when
|
||
# `restic forget` is called with `--prune`.
|
||
if $?.exitstatus > 0
|
||
error(1, "Forgetting snapshots failed.")
|
||
end
|
||
end
|
||
end
|
||
|
||
def prune(config)
|
||
puts 'Pruning restic repo …'
|
||
prune_command = ['restic', 'prune']
|
||
puts("Command: #{prune_command.join(' ')}")
|
||
system(*prune_command)
|
||
if $?.exitstatus > 0
|
||
error(1, 'Failed to prune.')
|
||
end
|
||
|
||
check
|
||
end
|
||
|
||
def run_restic(config)
|
||
puts 'Executing restic with the following arguments …'
|
||
puts "ARGV: #{ARGV}"
|
||
exec('restic', *ARGV)
|
||
end
|
||
|
||
def act(config, options)
|
||
puts "Applying configuration ‘#{config['name']}’ …"
|
||
set_restic_vars(config)
|
||
if options[:prune]
|
||
prune config
|
||
elsif options[:run_restic]
|
||
run_restic config
|
||
else
|
||
backup config
|
||
end
|
||
unset_restic_vars
|
||
end
|
||
|
||
unset_restic_vars
|
||
|
||
options = {}
|
||
options[:config_names] = []
|
||
options[:prune] = false
|
||
options[:run_restic] = false
|
||
|
||
OptionParser.new do |parser|
|
||
parser.on("-c NAME", "--configuration-name NAME", "Name of the configuration to use") do |v|
|
||
options[:config_names] << v
|
||
end
|
||
|
||
parser.on("-f CONFIG_FILE", "--config-file CONFIG_FILE", "Path to the configuration file.") do |v|
|
||
options[:config_file] = v
|
||
end
|
||
|
||
parser.on("-h", "--help", "Print this help.") do
|
||
puts parser
|
||
exit
|
||
end
|
||
|
||
parser.on('-p', '--prune', 'Prunes the repository. Does NOT do anything else.') do |v|
|
||
options[:prune] = v
|
||
end
|
||
|
||
parser.on('-r', '--run-restic', 'Set environment variables '\
|
||
'RESTIC_REPOSITORY and RESTIC_PASSWORD, execute restic (while passing '\
|
||
'all following command line arguments to restic).') do |v|
|
||
options[:run_restic] = v
|
||
parser.terminate
|
||
end
|
||
|
||
parser.on('--version', "Print version number.") do
|
||
puts "#{NAME}, #{VERSION}"
|
||
exit
|
||
end
|
||
end.parse!
|
||
|
||
if options[:config_file]
|
||
config_file = Pathname.new(options[:config_file])
|
||
else
|
||
config_file = DEFAULT_CONFIG_FILE
|
||
end
|
||
|
||
if !Pathname.new(config_file).exist? then
|
||
error(1, "Config file #{config_file} has not been found!")
|
||
end
|
||
|
||
puts "Using config file #{config_file} …"
|
||
configs = YAML.load_stream(File.open(config_file))
|
||
config_names = options[:config_names]
|
||
if config_names.empty?
|
||
puts "No configuration name has been given. Will use the first "\
|
||
"configuration from the file (‘#{configs.dig(0, 'name')}’)."
|
||
act(configs[0], options)
|
||
else
|
||
config_hash = configs.map {|c| [c['name'], c]}.to_h
|
||
config_names.each do |name|
|
||
if !config_hash.key? name
|
||
error(1, "Cannot find configuration ‘#{name}’.")
|
||
end
|
||
end
|
||
config_names.each do |config_name|
|
||
act(config_hash[config_name], options)
|
||
end
|
||
end
|