Class: Ruborg::Repository

Inherits:
Object
  • Object
show all
Defined in:
lib/ruborg/repository.rb

Overview

Borg repository management

Constant Summary collapse

MINIMUM_BORG_VERSION =
"1.4.0"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path, passphrase: nil, borg_options: {}, borg_path: nil, lock_wait: nil, logger: nil) ⇒ Repository

Returns a new instance of Repository.



9
10
11
12
13
14
15
16
17
# File 'lib/ruborg/repository.rb', line 9

def initialize(path, passphrase: nil, borg_options: {}, borg_path: nil, lock_wait: nil, logger: nil)
  @original_path = path
  @path = validate_repo_path(path)
  @passphrase = passphrase
  @borg_options = borg_options
  @borg_path = validate_borg_path(borg_path || "borg")
  @lock_wait = lock_wait&.to_i
  @logger = logger
end

Instance Attribute Details

#borg_pathObject (readonly)

Returns the value of attribute borg_path.



7
8
9
# File 'lib/ruborg/repository.rb', line 7

def borg_path
  @borg_path
end

#pathObject (readonly)

Returns the value of attribute path.



7
8
9
# File 'lib/ruborg/repository.rb', line 7

def path
  @path
end

Class Method Details

.borg_path(borg_command = "borg") ⇒ Object

Get Borg path (full path to executable)



519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'lib/ruborg/repository.rb', line 519

def self.borg_path(borg_command = "borg")
  # If it's an absolute or relative path, expand it
  return File.expand_path(borg_command) if borg_command.include?("/")

  # Otherwise, search in PATH
  ENV["PATH"].split(File::PATH_SEPARATOR).each do |directory|
    path = File.join(directory, borg_command)
    return path if File.executable?(path)
  end

  # Not found in PATH, return the command as-is
  borg_command
end

.borg_version(borg_path = "borg") ⇒ Object

Get Borg version

Raises:



507
508
509
510
511
512
513
514
515
516
# File 'lib/ruborg/repository.rb', line 507

def self.borg_version(borg_path = "borg")
  output, status = execute_version_command(borg_path)
  raise BorgError, "Borg is not installed or not in PATH" unless status.success?

  # Parse version from output like "borg 1.2.8"
  match = output.match(/borg (\d+\.\d+\.\d+)/)
  raise BorgError, "Could not parse Borg version from: #{output}" unless match

  match[1]
end

.execute_version_command(borg_path = "borg") ⇒ Object

Execute borg version command (extracted for testing)



534
535
536
537
538
539
540
# File 'lib/ruborg/repository.rb', line 534

def self.execute_version_command(borg_path = "borg")
  require "open3"

  # Use Open3.capture2e for safe command execution
  output, status = Open3.capture2e(borg_path, "--version")
  [output.strip, status]
end

Instance Method Details

#break_lockObject

Raises:



30
31
32
33
34
35
36
37
# File 'lib/ruborg/repository.rb', line 30

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

  check_borg_version!
  cmd = [@borg_path, "break-lock", @path]
  execute_borg_command(cmd)
  @logger&.info("Lock broken for repository at #{@path}")
end

#checkObject

Raises:



499
500
501
502
503
504
# File 'lib/ruborg/repository.rb', line 499

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

  cmd = [@borg_path, "check", @path]
  execute_borg_command(cmd)
end

#check_compatibilityObject

Check compatibility between Borg version and repository

Raises:



543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
# File 'lib/ruborg/repository.rb', line 543

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

  borg_version = self.class.borg_version(@borg_path)
  borg_major, borg_minor = borg_version.split(".").map(&:to_i)

  # Get repository version from config
  config_file = File.join(@path, "config")
  config_content = File.read(config_file)

  # Extract version from config (format: version = 1)
  repo_version = config_content.match(/version\s*=\s*(\d+)/)&.captures&.first&.to_i

  {
    borg_version: borg_version,
    repository_version: repo_version,
    compatible: check_version_compatibility(borg_major, borg_minor, repo_version)
  }
end

#createObject

Raises:



54
55
56
57
58
59
60
61
# File 'lib/ruborg/repository.rb', line 54

def create
  raise BorgError, "Repository already exists at #{@path}" if exists?

  @logger&.info("Creating Borg repository at #{@path} with repokey encryption")
  cmd = [@borg_path, "init", "--encryption=repokey", @path]
  execute_borg_command(cmd)
  @logger&.info("Repository created successfully at #{@path}")
end

#exists?Boolean

Returns:

  • (Boolean)


19
20
21
# File 'lib/ruborg/repository.rb', line 19

def exists?
  File.directory?(@path) && File.exist?(File.join(@path, "config"))
end

#force_break_lockObject

Raises:



39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/ruborg/repository.rb', line 39

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

  require "fileutils"
  removed = %w[lock.exclusive lock.roster].select do |name|
    target = File.join(@path, name)
    next false unless File.exist?(target)

    FileUtils.rm_rf(target)
    true
  end
  @logger&.info("Force-removed lock files at #{@path}: #{removed.join(", ")}")
  removed
end

#get_archive_info(archive_name) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/ruborg/repository.rb', line 85

def get_archive_info(archive_name)
  raise BorgError, "Repository does not exist at #{@path}" unless exists?
  raise BorgError, "Archive name cannot be empty" if archive_name.nil? || archive_name.strip.empty?

  require "json"
  require "open3"

  cmd = [@borg_path, "info", "#{@path}::#{archive_name}", "--json"]
  env = build_borg_env

  stdout, stderr, status = Open3.capture3(env, *cmd)
  raise BorgError, "Failed to get archive info: #{stderr}" unless status.success?

  JSON.parse(stdout)
rescue JSON::ParserError => e
  raise BorgError, "Failed to parse archive info: #{e.message}"
end

#get_file_metadata(archive_name, file_path: nil) ⇒ Object

Raises:



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/ruborg/repository.rb', line 103

def (archive_name, file_path: nil)
  raise BorgError, "Repository does not exist at #{@path}" unless exists?
  raise BorgError, "Archive name cannot be empty" if archive_name.nil? || archive_name.strip.empty?

  require "json"
  require "open3"

  # Get archive info to check if it's a per-file archive
  archive_info = get_archive_info(archive_name)
  comment = archive_info.dig("archives", 0, "comment")

  # If it's a per-file archive (has comment with original path), get metadata for that file
  # Otherwise, require file_path parameter
  if comment && !comment.empty?
    # Per-file archive - get metadata for the single file
    (archive_name, nil)
  else
    # Standard archive - require file_path
    raise BorgError, "file_path parameter required for standard archives" if file_path.nil? || file_path.empty?

    (archive_name, file_path)
  end
end

#infoObject

Raises:



63
64
65
66
67
68
# File 'lib/ruborg/repository.rb', line 63

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

  cmd = [@borg_path, "info", @path]
  execute_borg_command(cmd)
end

#listObject

Raises:



70
71
72
73
74
75
# File 'lib/ruborg/repository.rb', line 70

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

  cmd = [@borg_path, "list", @path]
  execute_borg_command(cmd)
end

#list_archive(archive_name) ⇒ Object

Raises:



77
78
79
80
81
82
83
# File 'lib/ruborg/repository.rb', line 77

def list_archive(archive_name)
  raise BorgError, "Repository does not exist at #{@path}" unless exists?
  raise BorgError, "Archive name cannot be empty" if archive_name.nil? || archive_name.strip.empty?

  cmd = [@borg_path, "list", "#{@path}::#{archive_name}"]
  execute_borg_command(cmd)
end

#locked?Boolean

Returns:

  • (Boolean)


25
26
27
28
# File 'lib/ruborg/repository.rb', line 25

def locked?
  File.exist?(File.join(@path, "lock.exclusive")) ||
    File.exist?(File.join(@path, "lock.roster"))
end

#prune(retention_policy = {}, retention_mode: "standard") ⇒ Object

Raises:



166
167
168
169
170
171
172
173
174
175
# File 'lib/ruborg/repository.rb', line 166

def prune(retention_policy = {}, retention_mode: "standard")
  raise BorgError, "Repository does not exist at #{@path}" unless exists?
  raise BorgError, "No retention policy specified" if retention_policy.nil? || retention_policy.empty?

  if retention_mode == "per_file"
    prune_per_file_archives(retention_policy)
  else
    prune_standard_archives(retention_policy)
  end
end