Skip to content

Instantly share code, notes, and snippets.

@mmrwoods
Last active January 26, 2016 08:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mmrwoods/9989502 to your computer and use it in GitHub Desktop.
Save mmrwoods/9989502 to your computer and use it in GitHub Desktop.
#!/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