#!/usr/local/bin/ruby # # backup_svn_repositories.rb # # Created by Benoit Gagnon on 2008-01-03. # Copyright 2008. All rights reserved. # require 'yaml' # Basic parameters REPOS_PARENT = "/home/svn" DUMP_DIR = "/var/svn_dumps" SYNC_FILE = "#{DUMP_DIR}/syncfile.yml" # Paths to executables SVNADMIN = '/usr/local/bin/svnadmin' SVNLOOK = '/usr/local/bin/svnlook' RSYNC = '/usr/local/bin/rsync' MD5 = '/sbin/md5' # Rsync options USE_RSYNC = true RSYNC_FLAGS = '--ignore-times -avcz' REMOTE_HOST = 'www.yourhostname.com' REMOTE_USER = 'username' REMOTE_KEY = '/path/to/your_ssh_key' REMOTE_DIR = "/var/svn_dumps" # Log formatter def log( m ) STDOUT.puts "#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} | #{m}" end log "-- Session started --" # Get a list of all repositories repos_parent_dir = Dir.new( REPOS_PARENT ) repos_list = repos_parent_dir.reject { |p| [ '.', '..' ].include?(p) } # Get a fresh hash with the HEAD of each repo fresh_hash = Hash.new log "Scanning for repositories..." repos_list.each do |r| log " found repo: #{r}" uuid = `#{SVNLOOK} uuid #{REPOS_PARENT}/#{r}`.chomp head = `#{SVNLOOK} youngest #{REPOS_PARENT}/#{r}`.chomp.to_i fresh_hash[uuid] = { :name => r, :rev => head } end # Load previous hash prev_hash = Hash.new if File.exist?( SYNC_FILE ) prev_hash = YAML.load_file( SYNC_FILE ) log "Loaded sync file: #{SYNC_FILE}" else log "Sync file not found: #{SYNC_FILE}" end # Compute operations ops = Array.new log "Finding differences..." fresh_hash.each do |uuid, h| if prev_hash[uuid].nil? log " new: #{h[:name]}" ops << { :uuid => uuid, :name => h[:name], :range => "0:#{h[:rev]}", :mode => :full } elsif prev_hash[uuid][:rev] < h[:rev] log " changed: #{h[:name]}" ops << { :uuid => uuid, :name => h[:name], :range => "#{prev_hash[uuid][:rev]+1}:#{h[:rev]}", :mode => :incremental } else log " up to date: #{h[:name]}" end end # Perform operations ops.each do |op| repo_path = "#{REPOS_PARENT}/#{op[:name]}" dump_path = "#{DUMP_DIR}/#{op[:uuid]}.svndump" # Use '>' if we're writing to a new file, '>>' if we're appending inc, stream = '', '>' if op[:mode] == :incremental inc = '--incremental' stream = '>>' # Integrity check if `#{MD5} -q #{dump_path}`.chomp != prev_hash[op[:uuid]][:md5] log "ERROR: Integrity check failed for #{op[:name]}" exit end end cmd = "#{SVNADMIN} dump -q #{inc} #{repo_path} -r #{op[:range]} #{stream} #{dump_path}" log "start: #{cmd}" if not system(cmd) log "ERROR: Command failed." exit end log "done." # Update hash prev_hash[op[:uuid]] = fresh_hash[op[:uuid]] prev_hash[op[:uuid]][:md5] = `#{MD5} -q #{dump_path}`.chomp # Save updated hash after each op File.open( SYNC_FILE, 'w' ) { |f| YAML.dump( prev_hash, f ) } log "Saved sync file to #{SYNC_FILE}" end if USE_RSYNC if ops.empty? log "No operations performed; will not rsync." else # Rsync the dumps to some exotic location cmd = "#{RSYNC} --rsh=\"ssh -i #{REMOTE_KEY}\" #{RSYNC_FLAGS} #{DUMP_DIR} #{REMOTE_USER}@#{REMOTE_HOST}:#{REMOTE_DIR}" log "Starting rsync to #{REMOTE_USER}@#{REMOTE_HOST}:#{REMOTE_DIR}" if not system(cmd) log "ERROR: Command failed." exit end log "rsync complete." end end # At this point, prev_hash should be equivalent to fresh_hash log "-- Session complete --"