Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions lib/ruborg/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ def info
desc "validate TYPE", "Validate configuration file or repository (TYPE: config or repo)"
option :verify_data, type: :boolean, default: false, desc: "Verify repository data (slower, only for 'repo' type)"
option :all, type: :boolean, default: false, desc: "Validate all repositories (only for 'repo' type)"
option :repair, type: :boolean, default: false,
desc: "Repair corruption via borg check --repair (only for 'repo', requires --yes)"
option :yes, type: :boolean, default: false, desc: "Confirm destructive repair operation (required with --repair)"
def validate(type)
case type
when "config"
Expand Down Expand Up @@ -335,6 +338,19 @@ def validate_repository(repo_config, global_settings)
@logger.info("Integrity check passed for #{repo_name}")
end

# Run repair if requested
if options[:repair]
raise ConfigError, "Repair mode requires --yes to acknowledge potential data loss" unless options[:yes]

puts " Running repair (borg check --repair)..."
puts " ⚠ WARNING: repair may delete corrupted archives"
@logger.warn("Starting repair on #{repo_name} — data loss possible if archives are corrupted")
output = repo.repair
puts " ✓ Repair completed"
output.split("\n").each { |line| puts " #{line}" } unless output.empty?
@logger.info("Repair completed for #{repo_name}:\n#{output}")
end

puts ""
rescue BorgError => e
puts " ✗ Validation failed: #{e.message}"
Expand Down
14 changes: 14 additions & 0 deletions lib/ruborg/repository.rb
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,20 @@ def check
execute_borg_command(cmd)
end

def repair
raise BorgError, "Repository does not exist at #{@path}" unless exists?

cmd = inject_lock_wait([@borg_path, "check", "--repair", @path])
env = build_borg_env

stdout, stderr, status = Open3.capture3(env, *cmd, stdin_data: "YES\n")
output = [stdout, stderr].map(&:strip).reject(&:empty?).join("\n")

raise BorgError, "Borg repair failed:\n#{output}" unless status.success?

output
end

# Get Borg version
def self.borg_version(borg_path = "borg")
output, status = execute_version_command(borg_path)
Expand Down
62 changes: 62 additions & 0 deletions spec/ruborg/cli_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1333,6 +1333,68 @@
end
end

describe "validate repo --repair", :borg do
let(:repair_config) do
{
"repositories" => [
{
"name" => "test-repo",
"path" => repo_path,
"sources" => [{ "name" => "main", "paths" => ["/tmp/test"] }]
}
]
}
end

before do
allow_any_instance_of(Ruborg::RuborgLogger).to receive(:info)
allow_any_instance_of(Ruborg::RuborgLogger).to receive(:warn)
allow_any_instance_of(Ruborg::RuborgLogger).to receive(:error)
allow(Ruborg::Repository).to receive(:borg_version).and_return("1.2.8")
end

it "raises ConfigError when --repair is given without --yes" do
config_file = create_test_config(repair_config)
FileUtils.mkdir_p(repo_path)
repo = Ruborg::Repository.new(repo_path)
repo.create

expect do
described_class.start(["validate", "repo", "--config", config_file,
"--repository", "test-repo", "--repair"])
end.to raise_error(Ruborg::ConfigError, /--yes/)
end

it "calls repo.repair and logs output when --repair --yes are given" do
config_file = create_test_config(repair_config)
FileUtils.mkdir_p(repo_path)
repo_instance = Ruborg::Repository.new(repo_path)
repo_instance.create

allow_any_instance_of(Ruborg::Repository).to receive(:repair).and_return("Repaired 3 segments")

expect do
described_class.start(["validate", "repo", "--config", config_file,
"--repository", "test-repo", "--repair", "--yes"])
end.to output(/Repair completed/).to_stdout
end

it "surfaces BorgError from repair as a failure message" do
config_file = create_test_config(repair_config)
FileUtils.mkdir_p(repo_path)
repo_instance = Ruborg::Repository.new(repo_path)
repo_instance.create

allow_any_instance_of(Ruborg::Repository).to receive(:repair)
.and_raise(Ruborg::BorgError, "Borg repair failed: segment error")

expect do
described_class.start(["validate", "repo", "--config", config_file,
"--repository", "test-repo", "--repair", "--yes"])
end.to output(/Validation failed/).to_stdout
end
end

describe "lock command" do
before do
allow_any_instance_of(Ruborg::RuborgLogger).to receive(:info)
Expand Down
49 changes: 49 additions & 0 deletions spec/ruborg/repository_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,55 @@
end
end

describe "#repair", :borg do
it "raises error if repository does not exist" do
repo = described_class.new(repo_path)

expect do
repo.repair
end.to raise_error(Ruborg::BorgError, /does not exist/)
end

it "runs borg check --repair piping YES to stdin and returns output" do
repo = described_class.new(repo_path, passphrase: passphrase)
repo.create

fake_status = instance_double(Process::Status, success?: true)
expect(Open3).to receive(:capture3).with(
hash_including("BORG_PASSPHRASE" => passphrase),
"borg", "check", "--repair", repo_path,
stdin_data: "YES\n"
).and_return(["Repair output", "", fake_status])

output = repo.repair
expect(output).to eq("Repair output")
end

it "raises BorgError when borg check --repair fails" do
repo = described_class.new(repo_path, passphrase: passphrase)
repo.create

fake_status = instance_double(Process::Status, success?: false)
allow(Open3).to receive(:capture3).and_return(["", "segment error", fake_status])

expect do
repo.repair
end.to raise_error(Ruborg::BorgError, /repair failed/)
end

it "combines stdout and stderr in the returned output" do
repo = described_class.new(repo_path, passphrase: passphrase)
repo.create

fake_status = instance_double(Process::Status, success?: true)
allow(Open3).to receive(:capture3).and_return(["stdout line", "stderr line", fake_status])

output = repo.repair
expect(output).to include("stdout line")
expect(output).to include("stderr line")
end
end

describe "#check_compatibility", :borg do
it "raises error if repository does not exist" do
repo = described_class.new(repo_path)
Expand Down