class BackupRestore

some assumptions: this tool runs on Ubuntu archive is valid archive generated with backup gem files are encrypted and archived with tar archives may be split or not

Public Class Methods

change_directory(target) click to toggle source
# File lib/backup_restore.rb, line 21
def self.change_directory(target)
  Dir.chdir(target)
  return if File.identical?(target, Dir.getwd)
  raise "failed to change working directory to #{target} (it is #{Dir.getwd})"
end
compare(compared_path, unpack_root) click to toggle source
# File lib/backup_restore.rb, line 178
def self.compare(compared_path, unpack_root)
  text = compare_paths(compared_path, unpack_root)
  return text
end
compare_paths(path_to_backuped, backup_location) click to toggle source
# File lib/backup_restore.rb, line 233
def self.compare_paths(path_to_backuped, backup_location)
  original = path_to_backuped
  restored = backup_location + path_to_backuped
  raise "missing folder for comparison: #{original}" unless Dir.exist?(original)
  raise "missing folder for comparison: #{restored}" unless Dir.exist?(restored)
  command = "diff --brief -r --no-dereference '#{original}' '#{restored}'"
  puts
  puts command
  puts
  returned = execute_command(command, [1])
  if returned == ""
    return everything_is_fine_message
  else
    return returned
  end
end
debug(message, priority = :medium) click to toggle source
# File lib/backup_restore.rb, line 15
def self.debug(message, priority = :medium)
  return if priority == :low
  return if priority == :medium
  puts message
end
discard_unimportant(text, unimportant_paths_array, possible_prefix = []) click to toggle source
# File lib/backup_restore.rb, line 189
def self.discard_unimportant(text, unimportant_paths_array, possible_prefix = [])
  possible_prefix << ""
  output = ""
  text.split("\n").each do |line|
    line.strip!
    unimportant = false
    unimportant_paths_array.each do |filter|
      possible_prefix.each do |prefix|
        r_filter = (Regexp.escape filter).gsub('/', '\/')
        r_prefix = (Regexp.escape prefix).gsub('/', '\/')
        if line =~ /\AOnly in (.+): (.+)\z/
          filepath_without_file, file = /\AOnly in (.+): (.+)\z/.match(line).captures
          filepath_without_file += '/' if filepath_without_file[-1] != '/'
          filepath = filepath_without_file + file
          unimportant = true if filepath =~ /\A#{r_prefix}#{r_filter}.*\z/
        elsif line =~ /\AFiles (.+) and (.+) differ\z/
          filepath_a, filepath_b = /\AFiles (.+) and (.+) differ\z/.match(line).captures
          unimportant = true if filepath_a =~ /\A#{r_prefix}#{r_filter}.*\z/
          unimportant = true if filepath_b =~ /\A#{r_prefix}#{r_filter}.*\z/
        elsif line =~ /\AFile (.+) is a fifo while file (.+) is a fifo\z/
          unimportant = true
        elsif line =~ /\AFile (.+) is a character special file while file (.+) is a character special file\z/
          unimportant = true
        elsif line == everything_is_fine_message.strip
          next
        elsif line == ""
          next
        else
          raise UnexpectedData, "unexpected line <#{line}>"
        end
      end
    end
    next if unimportant
    output += line + "\n"
  end
  puts
  return nil if output == ""
  return output.to_s
end
everything_is_fine_message() click to toggle source
# File lib/backup_restore.rb, line 250
def self.everything_is_fine_message
  return "everything is fine!" + "\n"
end
everything_is_fine_or_unimportant_message() click to toggle source
# File lib/backup_restore.rb, line 229
def self.everything_is_fine_or_unimportant_message
  "no important differences"
end
execute_command(command, unstandard_error_free_exit_codes = []) click to toggle source

alternatives - see stackoverflow.com/questions/3159945/running-command-line-commands-within-ruby-script

# File lib/backup_restore.rb, line 47
def self.execute_command(command, unstandard_error_free_exit_codes = [])
  output = `#{command}`
  if $?.success? || unstandard_error_free_exit_codes.include?($?.exitstatus)
    debug('all done', :low)
    return output
  end
  raise "<#{command}> command had problem (<#{$?}> with output <#{output}>). Working directory path was <#{Dir.getwd}>"
end
extract_archive(archive_storage_root, archive_name, unpack_root) click to toggle source
# File lib/backup_restore.rb, line 98
def self.extract_archive(archive_storage_root, archive_name, unpack_root)
  debug("unpacking <#{archive_name}>", :high)

  storage = get_storage_folder(archive_storage_root, archive_name)
  change_directory(storage)
  debug("archive is stored at <#{storage}>")

  file = get_the_only_expected_file('*.tar')
  debug("extracting #{file}")
  extract_tar_file(file)
  folder_with_unpacked_archive = storage + archive_name
  debug("unpacked archive with second layer of archive is stored at <#{folder_with_unpacked_archive}>")

  change_directory(folder_with_unpacked_archive + '/archives/')
  file = get_the_only_expected_file('*.tar.gz')
  debug("extracting #{file}")
  extract_tar_file(file, unpack_root)

  change_directory(storage)
  FileUtils.rm_rf(folder_with_unpacked_archive)
end
extract_tar_file(file, target_folder = nil) click to toggle source
# File lib/backup_restore.rb, line 56
def self.extract_tar_file(file, target_folder = nil)
  command = "tar --extract --file=#{file}"
  unless target_folder.nil?
    command += " --preserve-permissions -C '#{target_folder}'"
    # -C
    # tar will change its current directory to dir before performing any operations
    # https://www.gnu.org/software/tar/manual/html_node/Option-Summary.html
  end
  # base test:
  # echo "tar: Removing leading \`/' from member names" | grep -v "tar: Removing leading \`/' from member names"
  # shell should get:
  # grep -v "tar: Removing leading \`/' from member names"
  # command += ' | grep -v "tar: Removing leading \`/\' from member names"'
  # the code above is not proper way to solve this, it will mess up errorcode (hide errors, grep will return error on lack of match)
  execute_command(command)
end
get_storage_folder(archive_storage_root, archive_name) click to toggle source
# File lib/backup_restore.rb, line 27
def self.get_storage_folder(archive_storage_root, archive_name)
  debug("joining <#{archive_storage_root}> and <#{archive_name}>", :low)
  target = archive_storage_root + '/' + archive_name
  debug("looking for date folder in <#{target}>", :low)
  change_directory(target)
  directory = Dir.glob('*').select { |f| File.directory? f }
  if directory.length != 1
    puts "unexpected multiple backups at once in #{target}, or backup not found"
    puts "not supposed to happen in my workflow"
    puts "listing #{directory.length} directories, expected exactly 1:"
    directory.each do |file|
      puts file
    end
    raise "unhandled workflow"
  end
  target += '/' + directory[0] + '/'
  return target
end
get_the_only_expected_file(filter = '*') click to toggle source
# File lib/backup_restore.rb, line 73
def self.get_the_only_expected_file(filter = '*')
  files = Dir.glob(filter)
  puts files
  if files.length != 1
    if files.empty?
      puts 'no files found'
    else
      puts "files:"
    end
    files.each do |file|
      puts file
    end
    raise "expected exactly one file, not handled!"
  end
  return files[0]
end
is_unsplitting_necessary(archive_storage_root, archive_name) click to toggle source
# File lib/backup_restore.rb, line 120
def self.is_unsplitting_necessary(archive_storage_root, archive_name)
  storage = get_storage_folder(archive_storage_root, archive_name)
  return File.exist?(storage + "#{archive_name}.tar.enc-aaa")
end
process_given_archive(archive_storage_root, archive_name, unpack_root, password) click to toggle source
# File lib/backup_restore.rb, line 149
def self.process_given_archive(archive_storage_root, archive_name, unpack_root, password)
  debug("processsing #{archive_name} in #{archive_storage_root} - extracting to #{unpack_root}", :high)
  validate_folder_parameters(archive_storage_root, unpack_root)
  # archive may be in form of
  # (1) file(s) $NAME.tar.enc-aaa, $NAME.tar.enc-aab, $NAME.tar.enc-aac ...
  # (2) single  $NAME.tar.enc
  if is_unsplitting_necessary(archive_storage_root, archive_name)
    unsplit_archive(archive_storage_root, archive_name)
  end

  # now $NAME.tar.enc exists with the entire archive
  uncrypt_archive(archive_storage_root, archive_name, password)
  # now $NAME.tar exists with the entire archive unencrypted

  # now $NAME.tar.enc can be now deleted if it was created
  # it MUST NOT be deleted if archive was not split - in that case
  # it is the original archive file!
  # it is deleted at this step to reduce peak memory consumption on disk
  # for unpacking large archives
  if is_unsplitting_necessary(archive_storage_root, archive_name)
    storage = get_storage_folder(archive_storage_root, archive_name)
    FileUtils.rm_rf(storage + archive_name + ".tar.enc")
  end

  extract_archive(archive_storage_root, archive_name, unpack_root)
  storage = get_storage_folder(archive_storage_root, archive_name)
  FileUtils.rm_rf(storage + archive_name + ".tar")
end
uncrypt_archive(archive_storage_root, archive_name, password) click to toggle source
# File lib/backup_restore.rb, line 90
def self.uncrypt_archive(archive_storage_root, archive_name, password)
  storage = get_storage_folder(archive_storage_root, archive_name)
  output_archive = archive_name + '.tar'
  change_directory(storage)
  command = "openssl aes-256-cbc -d -in '#{archive_name}.tar.enc' -k '#{password}' -out '#{output_archive}'"
  execute_command(command)
end
unsplit_archive(archive_storage_root, archive_name) click to toggle source
# File lib/backup_restore.rb, line 125
def self.unsplit_archive(archive_storage_root, archive_name)
  storage = get_storage_folder(archive_storage_root, archive_name)
  change_directory(storage)
  if archive_name.include?(" ")
    raise "archive name was assumed to not have spaces but it is #{archive_name}"
  end
  execute_command("cat #{archive_name}.tar.enc-* > '#{archive_name}.tar.enc'")
end
validate_folder_parameters(archive_storage_root, unpack_root) click to toggle source
# File lib/backup_restore.rb, line 134
def self.validate_folder_parameters(archive_storage_root, unpack_root)
  unless File.exist?(archive_storage_root)
    raise PreconditionFailed.new("archive_storage_root (<#{archive_storage_root}>) does not exists")
  end
  unless File.exist?(unpack_root)
    raise PreconditionFailed.new("unpack_root (<#{unpack_root}>) does not exists")
  end
  unless Dir.exist?(archive_storage_root)
    raise PreconditionFailed.new("archive_storage_root (<#{archive_storage_root}>) is a file, not directory")
  end
  unless Dir.exist?(unpack_root)
    raise PreconditionFailed.new("unpack_root (<#{unpack_root}>) is a file, not directory")
  end
end