# frozen_string_literal: true require "English" require "English" require "tempfile" require "open3" require "date" module Bup class Tar def initialize(config) @config = config end def expand_string(str) while str =~ /\${?([a-zA-Z0-9]+)}?/ all = $LAST_MATCH_INFO[0] nm = $LAST_MATCH_INFO[1] str = str.sub(all, ENV[nm] || "") end str end def build_exclude_file(patterns) tf = Tempfile.new("bup") patterns.each do |p| p = expand_string(p) tf.write(p) tf.write("\n") end tf.close tf end # Prepare the destination directory. def prepare_destination(profile_name) destination = expand_string(@config.profile(profile_name)["destination"] || ".") suffix = ".tar.xz" type = @config.runtime["type"] || "full" FileUtils.mkdir_p(destination) unless File.exist?(destination) filename_base = "#{profile_name}-#{type}" filename = File.join(destination, "#{filename_base}-#{DateTime.now.new_offset(0).strftime("%Y%m%d%H%M%S")}#{suffix}") history = @config.profile(profile_name)["history"] || 0 if type == "full" && history.positive? oldest_kept_file = nil # Keep some full backups and remove the others. # We capture the oldest full backup and get rid of preceeding incrementals. Dir["#{destination}/#{filename_base}*"].sort.reverse.each_with_index do |fullbackup, idx| if idx < history oldest_kept_file = fullbackup else File.delete(fullbackup) end end # Remove all incremental files that are older than the oldest kept full backup. remove_before = File.stat(oldest_kept_file).ctime Dir["#{destination}/#{profile_name}*"].each do |backupfile| File.delete(backupfile) if File.stat(backupfile).ctime < remove_before end end filename end def call(profile_name = nil) profile_name ||= @config.runtime["profile"] profile = @config.config["profiles"][profile_name] raise "Missing profile #{profile_name}." unless profile args = @config.profile(profile_name)["tarcmd"].dup || ["tar"] args << "cJvf" filename = prepare_destination(profile_name) args << filename if @config.runtime["type"] == "incremental" lastrun = @config.lastrun(profile_name) args += ["--newer", lastrun.strftime("%Y-%m-%d %H:%M:%S %z")] if lastrun end @config.update_lastrun(profile_name) tf = build_exclude_file(profile["exclude"] || []) args += ["--exclude-from", tf.path] args += (@config.profile(profile_name)["include"] || ["."]).map do |str| expand_string(str) end begin Open3.popen3(*args) do |stdin, stdout, stderr, wait_thr| stdin.close while wait_thr.status r, w, e = IO.select([stdout, stderr]) r.each do |stream| print stream.read end end wait_thr.join print stdout.read print stderr.read end ensure tf.unlink end end end end