lib/dreamback/backup.rb in dreamback-0.0.4 vs lib/dreamback/backup.rb in dreamback-0.0.5

- old
+ new

@@ -37,31 +37,36 @@ backup_folders << [ entry.name, entry.name.split(".")[1].to_i ] if entry.name.include?("dreamback") backup_folders.sort! {|a,b| b <=> a} end end - # Get the newest folder for linking + # Get yesterday's folder to link against backup_to_link = backup_folders.first[0] + # If this would link us to the same folder, don't do that. Try yesterday's instead. + if backup_to_link.eql? "dreamback." + Date.today.strftime("%Y%m%d") + backup_to_link = "dreamback." + (Date.today - 1).strftime("%Y%m%d") + end # Delete any folders older than our limit - # Subtract one to account for the folder we're about to create - # Normally we remove a folder so that our count is one less than the "days to keep" - # However, if today's folder already exists then there's no need - offset = backup_folders.include?("dreamback.#{Time.now.strftime("%Y%m%d")}") ? 0 : 1 - if backup_folders.length >= Dreamback.settings[:days_to_keep] - offset - folders_to_delete = backup_folders.slice(Dreamback.settings[:days_to_keep] - offset, backup_folders.length) - folders_to_delete.map! {|f| f[0]} - rsync_delete(folders_to_delete) + if Dreamback.settings[:days_to_keep] + folders_to_delete = rotate_daily(backup_folders) + elsif Dreamback.settings[:keep_time_machine] + folders_to_delete = rotate_time_machine(backup_folders)[:delete] + else + folders_to_delete = nil end + rsync_delete(folders_to_delete) end + backup_to_link end # This uses a hack where we sync an empty directory to remove files # We do this because sftp has no recursive delete method # @params [Array[String]] list of backup directories def self.rsync_delete(directories) + return if directories.nil? || directories.empty? empty_dir_path = File.expand_path("../.dreamback_empty_dir", __FILE__) empty_dir = Dir.mkdir(empty_dir_path) unless File.exists?(empty_dir_path) begin directories.each do |dir| `rsync --delete -a #{empty_dir_path}/ #{Dreamback.settings[:backup_server_user]}@#{Dreamback.settings[:backup_server]}:#{BACKUP_FOLDER}/#{dir}` @@ -76,10 +81,12 @@ # Sync to the backup server using link-dest to save space # @param [String] name of the most recent backup folder prior to starting this run to link against def self.rsync_backup(link_dir) tmp_dir_path = "~/.dreamback_tmp" + tmp_dir = File.expand_path(tmp_dir_path) + Dir.mkdir(tmp_dir) unless File.exists?(tmp_dir) user_exclusions_path = File.expand_path("~/.dreamback_exclusions") default_exclusions_path = File.expand_path("../exclusions.txt", __FILE__) exclusions_path = File.exists?(user_exclusions_path) ? user_exclusions_path : default_exclusions_path begin backup_server_user = Dreamback.settings[:backup_server_user] @@ -88,19 +95,86 @@ Dreamback.settings[:dreamhost_users].each do |dreamhost| # User that we're going to back up user = dreamhost[:user] server = dreamhost[:server] # rsync won't do remote<->remote syncing, so we stage everything here first - tmp_dir = File.expand_path(tmp_dir_path) - Dir.mkdir(tmp_dir) unless File.exists?(tmp_dir) `rsync -e ssh -av --keep-dirlinks --exclude-from #{exclusions_path} --copy-links #{user}@#{server}:~/ #{tmp_dir}/#{user}@#{server}` - # Now we can sync local to remote. Only use link-dest if a previous folder to link to exists. - link_dest = link_dir.nil? ? "" : "--link-dest=~#{BACKUP_FOLDER.gsub(".", "")}/#{link_dir}" - `rsync -e ssh -av --delete --copy-links --keep-dirlinks #{link_dest} #{tmp_dir}/ #{backup_server_user}@#{backup_server}:#{BACKUP_FOLDER}/dreamback.#{today}` end + # Now we can sync local to remote. Only use link-dest if a previous folder to link to exists. + link_dest = link_dir.nil? ? "" : "--link-dest=~#{BACKUP_FOLDER.gsub(".", "")}/#{link_dir}" + `rsync -e ssh -av --delete --copy-links --keep-dirlinks #{link_dest} #{tmp_dir}/ #{backup_server_user}@#{backup_server}:#{BACKUP_FOLDER}/dreamback.#{today}` ensure # Remove the staging directory `rm -rf #{File.expand_path(tmp_dir_path)}` end + end + + # Rotate folders based on the number of days to keep + # @param [Array] folders to rotate in Dreamback format (dreamback.20120521) + # @return [Array] list of folders to delete + def self.rotate_daily(backup_folders) + # Subtract one to account for the folder we're about to create + # Normally we remove a folder so that our count is one less than the "days to keep" + # However, if today's folder already exists then there's no need + offset = backup_folders.include?("dreamback.#{Time.now.strftime("%Y%m%d")}") ? 0 : 1 + if backup_folders.length >= Dreamback.settings[:days_to_keep] - offset + folders_to_delete = backup_folders.slice(Dreamback.settings[:days_to_keep] - offset, backup_folders.length) + folders_to_delete.map! {|f| f[0]} + end + folders_to_delete ||= nil + end + + # Use a time machine-like algorithm to rotate backups. This will keep daily backups for seven days, weeklies for three months, and monthlies forever + # @param [Date] the day to count as "today", where the rotation will start + # @param [Array] folders to rotate in Dreamback format (dreamback.20120521) + # @return [Hash] :keep => list of folders to retain, :delete => folders marked for deletion + def self.rotate_time_machine(folders, today = nil) + today ||= Date.today + ymd = "%Y%m%d" + + folder_data = {} + folders.each do |f| + folder_data[f] = { + :date => Date.strptime(f[1].to_s, ymd) + } + end + + keep = [] + one_week_ago = today - 7 + three_months_ago = today << 3 + + week_hash = {} + month_hash = {} + folder_data.each do |k, v| + if v[:date] >= one_week_ago + keep << k + elsif v[:date] >= three_months_ago + week_hash[v[:date].cweek] = [] if week_hash[v[:date].cweek].nil? + week_hash[v[:date].cweek] << [k, v] + else + ym = v[:date].strftime("%Y%m").to_i + month_hash[ym] = [] if month_hash[ym].nil? + month_hash[ym] << [k, v] + end + end + + week_hash.each do |week, dates| + dates.sort! {|a, b| a[1][:date] <=> b[1][:date]} + end + + week_hash.each do |week, dates| + keep << dates.shift[0] unless dates.empty? + end + + month_hash.each do |month, dates| + dates.sort! {|a, b| a[1][:date] <=> b[1][:date]} + end + + month_hash.each do |month, dates| + keep << dates.shift[0] unless dates.empty? + end + + keep.sort!.compact! + { :keep => keep, :delete => folders - keep} end end end \ No newline at end of file