Last active
January 26, 2016 08:12
-
-
Save mmrwoods/9989502 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Dumb-ass backup - better than no backup! | |
# | |
# Run nightly and enjoy daily, weekly and monthly filesystem backups | |
# | |
# - default destination directory is /var/local/backup | |
# - latest, daily, weekly and monthly sub-directories are created | |
# - latest directory should only ever contain the latest backup | |
# - latest backup copied to daily, weekly and monthly directories as required | |
# - weekly backups created on Sunday | |
# - monthly backups created on first day of month | |
# - retains 7 daily, 4 weekly and 3 monthly backups | |
# | |
# TODO: | |
# - exit non-zero and output to stderr when external commands fail | |
# - allow retention policy to be configured | |
# - allow day of week for weekly to be configured | |
# - allow day of month for monthly to be configured | |
# - look into automagic inclusion of common directories into default sources | |
# - code cleanup, it's been hacked together to get something working | |
# - add option to provide path for config file (and ensure one exists) | |
require 'yaml' | |
require 'fileutils' | |
# Use full paths to system commands to avoid probles with aliases etc. | |
whoami = `which whoami`.chomp | |
tar = `which tar`.chomp | |
# Must be root to ensure backup archives contain expected files | |
if `#{whoami}`.chomp != "root" | |
puts "Error: You must be root to run this script" | |
exit 1 | |
end | |
config = File.exist?("/usr/local/etc/dumb_ass_backup.yml") ? YAML::load_file("/usr/local/etc/dumb_ass_backup.yml") : {} | |
sources = config['sources'] || { | |
"etc" => "/etc/" | |
} | |
destination = config['destination'] || "/var/local/backup" | |
puts "\nBackup started: #{Time.now}" | |
puts "\nDestination: #{destination}" | |
# Make sure sub-directories exist | |
%w{ latest daily weekly monthly }.each do |name| | |
FileUtils.mkdir_p File.join(destination, name) | |
end | |
# Delete any old backups from latest directory | |
Dir.glob(File.join(destination, "latest", "*")).each do |file| | |
FileUtils.rm_rf file | |
end | |
today = Time.now.strftime('%Y-%m-%d') | |
latest_path = File.join(destination, "latest", today) | |
puts "\nBacking up files" | |
sources.each do |name, glob_pattern| | |
backup_path = File.join(latest_path, name) | |
FileUtils.mkdir_p(backup_path) | |
Dir.glob(glob_pattern).each do |source_path| | |
print "* #{File.basename(source_path)} -> " | |
if File.directory?(source_path) | |
# tar and gzip directories, following symlinks | |
target_path = File.join(backup_path, "#{File.basename(source_path)}.tar.gz") | |
system("#{tar} -chz \"#{source_path}\" > \"#{target_path}\" 2> /dev/null") | |
raise "Failed to create archive #{target_path}" unless File.exist?(target_path) | |
# To preserve file permissions, only allow root to extract archives | |
FileUtils.chmod(0600, target_path) | |
else | |
# copy files, preserving permissions | |
target_path = File.join(backup_path, File.basename(source_path)) | |
FileUtils.cp_r(source_path, target_path, :preserve => true) | |
end | |
relative_target_path = target_path.sub(destination,'').sub(/^\//,'') | |
print "#{relative_target_path}\n" | |
end | |
end | |
puts "\nCreating copies" | |
do_weekly = Time.now.wday == 0 | |
do_monthly = Time.now.mday == 1 | |
%w{ daily weekly monthly }.each do |name| | |
print "* Creating #{name} copy..." | |
if (name == "weekly" && !do_weekly) || (name == "monthly" && !do_monthly) | |
print "n/a\n" | |
next | |
end | |
target_path = File.join(destination, name) | |
FileUtils.cp_r(latest_path, target_path, :preserve => true) | |
if File.exist?(File.join(target_path, today)) | |
print "OK\n" | |
else | |
print "FAIL\n" | |
exit 1 | |
end | |
end | |
puts "\nApplying retention policy" | |
keep_daily = 7 | |
keep_weekly = 4 | |
keep_monthly = 3 | |
%w{ daily weekly monthly }.each do |name| | |
keep_count = eval("keep_#{name}") | |
print "* Keeping #{keep_count} #{name} backups..." | |
target_path = File.join(destination, name) | |
list_backups = Proc.new do | |
Dir.glob(File.join(target_path, "*")).select{|file| | |
file.match(/\d{4}-\d{2}-\d{2}$/) | |
}.sort | |
end | |
backups_before = list_backups.call | |
backups_to_keep = backups_before.reverse.slice(0,keep_count) | |
backups_to_delete = backups_before - backups_to_keep | |
if backups_to_delete.empty? | |
print "OK\n" | |
next | |
end | |
backups_to_delete.each do |backup| | |
FileUtils.rm_rf(backup) | |
end | |
backups_after = list_backups.call | |
if backups_after.size == keep_count | |
print "OK\n" | |
else | |
print "FAIL\n" | |
exit 1 | |
end | |
end | |
puts "\nVerifying destination" | |
%w{ latest daily weekly monthly }.each do |name| | |
print "* #{name} -> " | |
target_path = File.join(destination, name) | |
print Dir.entries(target_path).select{ |file| | |
file.match(/\d{4}-\d{2}-\d{2}$/) | |
}.sort.reverse.join(", ") | |
print "\n" | |
end | |
puts "\nBackup completed: #{Time.now}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment