Class: KamalBackup::Restic
- Inherits:
-
Object
- Object
- KamalBackup::Restic
- Defined in:
- lib/kamal_backup/restic.rb
Instance Attribute Summary collapse
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#redactor ⇒ Object
readonly
Returns the value of attribute redactor.
Instance Method Summary collapse
- #backup_command_output(command, filename:, tags:) ⇒ Object
- #backup_file_content(path, filename:, tags:) ⇒ Object
- #backup_path(path, tags:) ⇒ Object
- #backup_paths(paths, tags:) ⇒ Object
- #check! ⇒ Object
- #common_tags ⇒ Object
- #database_file(snapshot, adapter) ⇒ Object
- #dump_file_to_command(snapshot, filename, command) ⇒ Object
- #dump_file_to_path(snapshot, filename, target_path) ⇒ Object
- #ensure_repository! ⇒ Object
- #forget_after_success! ⇒ Object
-
#initialize(config, redactor:) ⇒ Restic
constructor
A new instance of Restic.
- #latest_snapshot(tags:) ⇒ Object
- #ls_json(snapshot) ⇒ Object
- #restore_snapshot(snapshot, target) ⇒ Object
- #run(args) ⇒ Object
- #snapshots(tags: common_tags) ⇒ Object
- #snapshots_json(tags: common_tags) ⇒ Object
Constructor Details
#initialize(config, redactor:) ⇒ Restic
Returns a new instance of Restic.
11 12 13 14 |
# File 'lib/kamal_backup/restic.rb', line 11 def initialize(config, redactor:) @config = config @redactor = redactor end |
Instance Attribute Details
#config ⇒ Object (readonly)
Returns the value of attribute config.
9 10 11 |
# File 'lib/kamal_backup/restic.rb', line 9 def config @config end |
#redactor ⇒ Object (readonly)
Returns the value of attribute redactor.
9 10 11 |
# File 'lib/kamal_backup/restic.rb', line 9 def redactor @redactor end |
Instance Method Details
#backup_command_output(command, filename:, tags:) ⇒ Object
25 26 27 28 29 |
# File 'lib/kamal_backup/restic.rb', line 25 def backup_command_output(command, filename:, tags:) restic_command = CommandSpec.new(argv: ["restic", "backup", "--stdin", "--stdin-filename", filename] + tag_args( + )) log("backing up stream as #{filename}") pipe_commands(command, restic_command, producer_label: "dump", consumer_label: "restic backup") end |
#backup_file_content(path, filename:, tags:) ⇒ Object
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/kamal_backup/restic.rb', line 31 def backup_file_content(path, filename:, tags:) command = CommandSpec.new(argv: ["restic", "backup", "--stdin", "--stdin-filename", filename] + tag_args( + )) log("backing up file content as #{filename}") File.open(path, "rb") do |file| Open3.popen3(command.env, *command.argv) do |stdin, stdout, stderr, wait_thread| stdout_reader = Thread.new { stdout.read } stderr_reader = Thread.new { stderr.read } IO.copy_stream(file, stdin) stdin.close out = stdout_reader.value err = stderr_reader.value status = wait_thread.value raise_command_error(command, status, out, err) unless status.success? CommandResult.new(stdout: out, stderr: err, status: status.exitstatus) end end rescue Errno::ENOENT => e raise CommandError.new("command not found: #{command.argv.first}", command: command, status: 127, stderr: e.) end |
#backup_path(path, tags:) ⇒ Object
62 63 64 |
# File 'lib/kamal_backup/restic.rb', line 62 def backup_path(path, tags:) backup_paths([path], tags: ) end |
#backup_paths(paths, tags:) ⇒ Object
53 54 55 56 57 58 59 60 |
# File 'lib/kamal_backup/restic.rb', line 53 def backup_paths(paths, tags:) paths = Array(paths).compact.map(&:to_s).reject(&:empty?) return if paths.empty? = paths.map { |path| "path:#{config.backup_path_label(path)}" } log("backing up #{paths.size} file path(s): #{paths.join(", ")}") run(["backup"] + paths + tag_args( + + )) end |
#check! ⇒ Object
72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/kamal_backup/restic.rb', line 72 def check! args = %w[check] args.concat(["--read-data-subset", config.check_read_data_subset]) if config.check_read_data_subset started_at = Time.now.utc result = run(args) write_last_check(status: "ok", started_at: started_at, finished_at: Time.now.utc, output: result.stdout) result rescue CommandError => e write_last_check(status: "failed", started_at: started_at || Time.now.utc, finished_at: Time.now.utc, error: e.) raise end |
#common_tags ⇒ Object
163 164 165 |
# File 'lib/kamal_backup/restic.rb', line 163 def ["kamal-backup", "app:#{config.app_name}"] end |
#database_file(snapshot, adapter) ⇒ Object
114 115 116 117 118 119 120 121 122 123 |
# File 'lib/kamal_backup/restic.rb', line 114 def database_file(snapshot, adapter) legacy_prefix = "databases/#{config.app_name}/#{adapter}/" flat_prefix = "databases-#{config.app_name.gsub(/[^A-Za-z0-9_.-]+/, "-")}-#{adapter}-" ls_json(snapshot).find do |entry| next false unless entry["type"] == "file" normalized = entry["path"].to_s.sub(%r{\A/+}, "") normalized.start_with?(legacy_prefix) || File.basename(normalized).start_with?(flat_prefix) end&.fetch("path") end |
#dump_file_to_command(snapshot, filename, command) ⇒ Object
125 126 127 128 |
# File 'lib/kamal_backup/restic.rb', line 125 def dump_file_to_command(snapshot, filename, command) restic_command = CommandSpec.new(argv: ["restic", "dump", snapshot, filename]) pipe_commands(restic_command, command, producer_label: "restic dump", consumer_label: command.argv.first) end |
#dump_file_to_path(snapshot, filename, target_path) ⇒ Object
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/kamal_backup/restic.rb', line 130 def dump_file_to_path(snapshot, filename, target_path) command = CommandSpec.new(argv: ["restic", "dump", snapshot, filename]) target_path = File.(target_path) FileUtils.mkdir_p(File.dirname(target_path)) temp_path = "#{target_path}.kamal-backup-#{$$}.tmp" Open3.popen3(command.env, *command.argv) do |stdin, stdout, stderr, wait_thread| stdin.close stderr_reader = Thread.new { stderr.read } File.open(temp_path, "wb") { |file| IO.copy_stream(stdout, file) } err = stderr_reader.value status = wait_thread.value raise_command_error(command, status, "", err) unless status.success? end File.rename(temp_path, target_path) target_path rescue Errno::ENOENT => e FileUtils.rm_f(temp_path) if temp_path raise CommandError.new("command not found: #{command.argv.first}", command: command, status: 127, stderr: e.) rescue StandardError FileUtils.rm_f(temp_path) if temp_path raise end |
#ensure_repository! ⇒ Object
16 17 18 19 20 21 22 23 |
# File 'lib/kamal_backup/restic.rb', line 16 def ensure_repository! run(%w[snapshots --json]) rescue CommandError => e raise e unless config.restic_init_if_missing? log("restic repository not ready, running restic init") run(%w[init]) end |
#forget_after_success! ⇒ Object
66 67 68 69 70 |
# File 'lib/kamal_backup/restic.rb', line 66 def forget_after_success! args = ["forget", "--prune"] + config.retention_args + tag_args() log("running restic forget/prune with retention policy") run(args) end |
#latest_snapshot(tags:) ⇒ Object
98 99 100 101 102 103 |
# File 'lib/kamal_backup/restic.rb', line 98 def latest_snapshot(tags:) snapshots = snapshots_json(tags: + ) snapshots.max_by { |snapshot| Time.parse(snapshot.fetch("time")) } rescue JSON::ParserError nil end |
#ls_json(snapshot) ⇒ Object
105 106 107 108 109 110 111 112 |
# File 'lib/kamal_backup/restic.rb', line 105 def ls_json(snapshot) output = run(["ls", "--json", snapshot]).stdout output.lines.filter_map do |line| JSON.parse(line) rescue JSON::ParserError nil end end |
#restore_snapshot(snapshot, target) ⇒ Object
154 155 156 157 |
# File 'lib/kamal_backup/restic.rb', line 154 def restore_snapshot(snapshot, target) log("restoring file snapshot #{snapshot} to #{target}") run(["restore", snapshot, "--target", target]) end |
#run(args) ⇒ Object
159 160 161 |
# File 'lib/kamal_backup/restic.rb', line 159 def run(args) Command.capture(CommandSpec.new(argv: ["restic"] + args), redactor: redactor) end |
#snapshots(tags: common_tags) ⇒ Object
84 85 86 |
# File 'lib/kamal_backup/restic.rb', line 84 def snapshots(tags: ) run(["snapshots"] + tag_args()) end |
#snapshots_json(tags: common_tags) ⇒ Object
88 89 90 91 92 93 94 95 96 |
# File 'lib/kamal_backup/restic.rb', line 88 def snapshots_json(tags: ) output = run(["snapshots", "--json"] + tag_args()).stdout snapshots = JSON.parse(output) = .compact snapshots.select do |snapshot| = Array(snapshot["tags"]) .all? { |tag| .include?(tag) } end end |