Class: Yobi::Restic

Inherits:
Object
  • Object
show all
Defined in:
lib/yobi/restic.rb,
lib/yobi/repository/mount.rb,
sig/yobi.rbs

Overview

A specific Restic binary, plus its process-level settings that apply regardless of which repository is being operated on. Repository-specific identity (url, credentials) lives on Yobi::Repository instead.

Constant Summary collapse

MINIMUM_VERSION =

The oldest Restic version #run/#run_dump/#run_mount verify the installed binary meets before executing a real command. See Yobi::UnsupportedResticVersion.

Returns:

  • (Gem::Version)
Gem::Version.new("0.18.0")
ALLOWED_ENV_VARS =

Env vars #inspect shows in the clear; anything else is redacted.

Returns:

  • (Set[String])
%w[ # :nodoc:
RESTIC_REPOSITORY RESTIC_CACHE_DIR RESTIC_COMPRESSION RESTIC_PACK_SIZE
RESTIC_READ_CONCURRENCY RESTIC_HOST RESTIC_PROGRESS_FPS RESTIC_CACERT
RESTIC_TLS_CLIENT_CERT RESTIC_KEY_HINT TMPDIR TMP AWS_DEFAULT_REGION
AWS_SHARED_CREDENTIALS_FILE AZURE_ENDPOINT_SUFFIX AZURE_FORCE_CLI_CREDENTIAL
RCLONE_BWLIMIT].to_set.freeze
READY_LINE =

:nodoc:

"Now serving the repository at"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(restic_path = nil, env: {}, cache_dir: nil, compression: nil, pack_size: nil, read_concurrency: nil, host: nil, progress_fps: nil, cacert: nil, tls_client_cert: nil, key_hint: nil, limit_download: nil, limit_upload: nil, retry_lock: nil, no_lock: false, no_cache: false, cleanup_cache: false, no_extra_verify: false, stuck_request_timeout: nil, options: [], http_user_agent: nil, quiet: false) ⇒ Restic

Builds a Restic executor. restic_path defaults to $RESTIC_PATH, then "restic". env: is extra env vars merged over the named settings below (env-var-backed accessors like cache_dir:, compression:, etc.).



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/yobi/restic.rb', line 72

def initialize(restic_path = nil, env: {}, cache_dir: nil, compression: nil, pack_size: nil,
  read_concurrency: nil, host: nil, progress_fps: nil, cacert: nil, tls_client_cert: nil,
  key_hint: nil, limit_download: nil, limit_upload: nil, retry_lock: nil, no_lock: false,
  no_cache: false, cleanup_cache: false, no_extra_verify: false, stuck_request_timeout: nil,
  options: [], http_user_agent: nil, quiet: false)
  @restic_path = restic_path || ENV.fetch("RESTIC_PATH", "restic")
  @env = env
  @cache_dir = cache_dir
  @compression = compression
  @pack_size = pack_size
  @read_concurrency = read_concurrency
  @host = host
  @progress_fps = progress_fps
  @cacert = cacert
  @tls_client_cert = tls_client_cert
  @key_hint = key_hint
  @limit_download = limit_download
  @limit_upload = limit_upload
  @retry_lock = retry_lock
  @no_lock = no_lock
  @no_cache = no_cache
  @cleanup_cache = cleanup_cache
  @no_extra_verify = no_extra_verify
  @stuck_request_timeout = stuck_request_timeout
  @options = options
  @http_user_agent = http_user_agent
  @quiet = quiet
end

Instance Attribute Details

#cacertString?

$RESTIC_CACERT

Returns:

  • (String, nil)


40
41
42
# File 'lib/yobi/restic.rb', line 40

def cacert
  @cacert
end

#cache_dirString?

$RESTIC_CACHE_DIR

Returns:

  • (String, nil)


28
29
30
# File 'lib/yobi/restic.rb', line 28

def cache_dir
  @cache_dir
end

#cleanup_cacheBoolean

--cleanup-cache

Returns:

  • (Boolean)


56
57
58
# File 'lib/yobi/restic.rb', line 56

def cleanup_cache
  @cleanup_cache
end

#compressionString?

$RESTIC_COMPRESSION

Returns:

  • (String, nil)


30
31
32
# File 'lib/yobi/restic.rb', line 30

def compression
  @compression
end

#hostString?

$RESTIC_HOST

Returns:

  • (String, nil)


36
37
38
# File 'lib/yobi/restic.rb', line 36

def host
  @host
end

#http_user_agentString?

--http-user-agent

Returns:

  • (String, nil)


64
65
66
# File 'lib/yobi/restic.rb', line 64

def http_user_agent
  @http_user_agent
end

#key_hintString?

$RESTIC_KEY_HINT

Returns:

  • (String, nil)


44
45
46
# File 'lib/yobi/restic.rb', line 44

def key_hint
  @key_hint
end

#limit_downloadString?

--limit-download

Returns:

  • (String, nil)


46
47
48
# File 'lib/yobi/restic.rb', line 46

def limit_download
  @limit_download
end

#limit_uploadString?

--limit-upload

Returns:

  • (String, nil)


48
49
50
# File 'lib/yobi/restic.rb', line 48

def limit_upload
  @limit_upload
end

#no_cacheBoolean

--no-cache

Returns:

  • (Boolean)


54
55
56
# File 'lib/yobi/restic.rb', line 54

def no_cache
  @no_cache
end

#no_extra_verifyBoolean

--no-extra-verify

Returns:

  • (Boolean)


58
59
60
# File 'lib/yobi/restic.rb', line 58

def no_extra_verify
  @no_extra_verify
end

#no_lockBoolean

--no-lock

Returns:

  • (Boolean)


52
53
54
# File 'lib/yobi/restic.rb', line 52

def no_lock
  @no_lock
end

#optionsArray[String]

--option, one per element.

Returns:

  • (Array[String])


62
63
64
# File 'lib/yobi/restic.rb', line 62

def options
  @options
end

#pack_sizeInteger?

$RESTIC_PACK_SIZE

Returns:

  • (Integer, nil)


32
33
34
# File 'lib/yobi/restic.rb', line 32

def pack_size
  @pack_size
end

#progress_fpsInteger?

$RESTIC_PROGRESS_FPS

Returns:

  • (Integer, nil)


38
39
40
# File 'lib/yobi/restic.rb', line 38

def progress_fps
  @progress_fps
end

#quietBoolean

--quiet

Returns:

  • (Boolean)


66
67
68
# File 'lib/yobi/restic.rb', line 66

def quiet
  @quiet
end

#read_concurrencyInteger?

$RESTIC_READ_CONCURRENCY

Returns:

  • (Integer, nil)


34
35
36
# File 'lib/yobi/restic.rb', line 34

def read_concurrency
  @read_concurrency
end

#restic_pathString

Path to the Restic binary.

Returns:

  • (String)


26
27
28
# File 'lib/yobi/restic.rb', line 26

def restic_path
  @restic_path
end

#retry_lockString?

--retry-lock

Returns:

  • (String, nil)


50
51
52
# File 'lib/yobi/restic.rb', line 50

def retry_lock
  @retry_lock
end

#stuck_request_timeoutString?

--stuck-request-timeout

Returns:

  • (String, nil)


60
61
62
# File 'lib/yobi/restic.rb', line 60

def stuck_request_timeout
  @stuck_request_timeout
end

#tls_client_certString?

$RESTIC_TLS_CLIENT_CERT

Returns:

  • (String, nil)


42
43
44
# File 'lib/yobi/restic.rb', line 42

def tls_client_cert
  @tls_client_cert
end

Class Method Details

.dispatch(execution) ⇒ execution

Maps Restic's exit codes to Yobi's typed errors. Returns execution unchanged on 0/3.

Parameters:

  • execution (execution)

Returns:

  • (execution)


185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/yobi/restic.rb', line 185

def self.dispatch(execution) # :nodoc:
  case execution[:exit_code]
  when 0, 3
    execution
  when 10
    raise Yobi::RepositoryNotFound, execution
  when 11
    raise Yobi::RepositoryLocked, execution
  when 12
    raise Yobi::AuthenticationFailed, execution
  else
    raise Yobi::ResticCommandFailed, execution
  end
end

Instance Method Details

#append_global_flags(a) ⇒ void

This method returns an undefined value.

Appends the CLI-only global flags (the ones above with no env var equivalent) to a builder. Called from both #build_argv and Repository#build_argv.

Parameters:



121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/yobi/restic.rb', line 121

def append_global_flags(a) # :nodoc:
  a.flag(:limit_download, limit_download) unless limit_download.nil?
  a.flag(:limit_upload, limit_upload) unless limit_upload.nil?
  a.flag(:retry_lock, retry_lock) unless retry_lock.nil?
  a.flag(:no_lock) if no_lock
  a.flag(:no_cache) if no_cache
  a.flag(:cleanup_cache) if cleanup_cache
  a.flag(:no_extra_verify) if no_extra_verify
  a.flag(:stuck_request_timeout, stuck_request_timeout) unless stuck_request_timeout.nil?
  a.repeat_flag(:option, options)
  a.flag(:http_user_agent, http_user_agent) unless http_user_agent.nil?
  a.flag(:quiet) if quiet
end

#cache(cleanup: false, max_age: nil, no_size: false) ⇒ true

restic cache: lists and optionally cleans local cache directories. Not repository-scoped. cleanup: triggers --cleanup, max_age: sets --max-age, no_size: triggers --no-size. Returns true.

Parameters:

  • cleanup: (Boolean) (defaults to: false)
  • max_age: (String, nil) (defaults to: nil)
  • no_size: (Boolean) (defaults to: false)

Returns:

  • (true)


151
152
153
154
155
156
157
158
159
# File 'lib/yobi/restic.rb', line 151

def cache(cleanup: false, max_age: nil, no_size: false)
  argv = build_argv("cache") do |a|
    a.flag(:cleanup) if cleanup
    a.flag(:max_age, max_age) unless max_age.nil?
    a.flag(:no_size) if no_size
  end
  run(argv)
  true
end

#ensure_minimum_version!void

This method returns an undefined value.

Verifies the installed Restic binary meets MINIMUM_VERSION, once per instance (memoized). #run/#run_dump/#run_mount call this automatically before executing a real command; public so a caller can also call it explicitly to fail fast. Raises Yobi::UnsupportedResticVersion when too old.



239
240
241
242
243
244
245
246
247
# File 'lib/yobi/restic.rb', line 239

def ensure_minimum_version!
  return if defined?(@version_checked)
  @version_checked = true

  installed = Gem::Version.new(version["version"])
  return if installed >= MINIMUM_VERSION

  raise Yobi::UnsupportedResticVersion.new(installed_version: installed.to_s, minimum_version: MINIMUM_VERSION.to_s)
end

#envHash[String, String]

The env-var-backed settings above, computed fresh from their current accessor values on every call. env: given at construction wins over any of these on key collision.

Returns:

  • (Hash[String, String])


104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/yobi/restic.rb', line 104

def env
  {
    "RESTIC_CACHE_DIR" => cache_dir,
    "RESTIC_COMPRESSION" => compression,
    "RESTIC_PACK_SIZE" => pack_size&.to_s,
    "RESTIC_READ_CONCURRENCY" => read_concurrency&.to_s,
    "RESTIC_HOST" => host,
    "RESTIC_PROGRESS_FPS" => progress_fps&.to_s,
    "RESTIC_CACERT" => cacert,
    "RESTIC_TLS_CLIENT_CERT" => tls_client_cert,
    "RESTIC_KEY_HINT" => key_hint
  }.compact.merge(@env)
end

#inspectObject

Redacts sensitive env values so a stray +pp+/+puts+/log call never prints a credential in plaintext.



137
138
139
# File 'lib/yobi/restic.rb', line 137

def inspect
  "#<#{self.class} restic_path=#{restic_path.inspect} env=#{redacted_env.inspect}>"
end

#run(argv, extra_env: {}, skip_version_check: false, output: nil, &block) ⇒ void

This method returns an undefined value.

Runs argv against this Restic binary, merging extra_env: with this instance's own global env. output: takes a caller-configured Yobi::ResticOutput (e.g. with its own transform:); passing one forces streaming even without a block. When a block is given, each parsed message is yielded live as the command runs.

skip_version_check: is used internally by #version to avoid recursing into #ensure_minimum_version!

Returns {exit_code:, output:, argv:} on exit code 0 or 3. Raises Yobi::RepositoryNotFound, Yobi::RepositoryLocked, Yobi::AuthenticationFailed, or Yobi::ResticCommandFailed otherwise.

Parameters:

  • argv (Array[String])
  • extra_env: (Hash[String, String]) (defaults to: {})
  • skip_version_check: (Boolean) (defaults to: false)
  • output: (ResticOutput, nil) (defaults to: nil)


173
174
175
176
177
178
179
180
181
# File 'lib/yobi/restic.rb', line 173

def run(argv, extra_env: {}, skip_version_check: false, output: nil, &block) # :nodoc:
  ensure_minimum_version! unless skip_version_check
  execution = if output || block
    execute_with_streaming(argv, extra_env, output: output, &block)
  else
    execute(argv, extra_env)
  end
  self.class.dispatch(execution)
end

#run_dump(argv, extra_env:) ⇒ Object #run_dump(argv, extra_env:) ⇒ IOHandle

For commands whose success output is raw bytes with no JSON message framing (currently only Repository#dump). Spawns Restic with stdout wired to a pipe.

Without a block, returns a Yobi::IOHandle immediately; closing, reaping, and exit-code dispatch are the caller's own responsibility. With one, yields the pipe's read end; it's always closed and the process always reaped once the block returns or raises, before any exit-code dispatch runs.

Overloads:

  • #run_dump(argv, extra_env:) ⇒ Object

    Parameters:

    • argv (Array[String])
    • extra_env: (Hash[String, String])

    Returns:

    • (Object)
  • #run_dump(argv, extra_env:) ⇒ IOHandle

    Parameters:

    • argv (Array[String])
    • extra_env: (Hash[String, String])

    Returns:

Yields:

Yield Parameters:

  • io (IO)

Yield Returns:

  • (Object)


209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/yobi/restic.rb', line 209

def run_dump(argv, extra_env: {}) # :nodoc:
  ensure_minimum_version!
  output = Yobi::ResticOutput.new
  read_end, write_end = IO.pipe
  read_end.binmode

  pid = Process.spawn(env.merge(extra_env), restic_path, *argv, out: write_end, err: output.file)
  write_end.close

  return Yobi::IOHandle.new(read_end, pid: pid, output: output, argv: argv) unless block_given?

  begin
    result = yield read_end
  ensure
    read_end.close unless read_end.closed?
    _, status = Process.wait2(pid)
  end

  self.class.dispatch(exit_code: status.exitstatus, output: output, argv: argv)
  result
rescue Errno::ENOENT
  output.file.close
  raise Yobi::ResticNotFound.new(restic_path: restic_path, argv: argv)
end

#run_mount(argv, mountpoint:, extra_env: {}, ready_timeout: 10) ⇒ MountHandle

For Yobi::Repository#mount. Spawns Restic and waits for its readiness line (or ready_timeout: seconds, or Restic exiting on its own) before returning a Yobi::MountHandle. Raises Yobi::MountTimeout.

Parameters:

  • argv (Array[String])
  • mountpoint: (String)
  • extra_env: (Hash[String, String]) (defaults to: {})
  • ready_timeout: (Numeric) (defaults to: 10)

Returns:



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/yobi/repository/mount.rb', line 64

def run_mount(argv, mountpoint:, extra_env: {}, ready_timeout: 10) # :nodoc:
  ensure_minimum_version!
  output = Yobi::ResticOutput.new

  stdin, pipe, wait_thr = Open3.popen2e(env.merge(extra_env), restic_path, *argv)
  stdin.close

  case wait_for_ready(pipe, output.file, mountpoint, ready_timeout)
  when :ready
    Yobi::MountHandle.new(wait_thr: wait_thr, mountpoint: mountpoint, pipe: pipe, output: output, argv: argv)
  when :timeout
    Process.kill("INT", wait_thr.pid)
    wait_thr.value
    raise Yobi::MountTimeout.new(argv: argv, timeout: ready_timeout)
  when :exited
    status = wait_thr.value
    self.class.dispatch(exit_code: status.exitstatus, output: output, argv: argv)
  end
rescue Errno::ENOENT
  output.file.close
  raise Yobi::ResticNotFound.new(restic_path: restic_path, argv: argv)
end

#versionResticVersion

restic version: the installed binary's own version info. Returns a Yobi::ResticVersion.

Returns:



143
144
145
146
# File 'lib/yobi/restic.rb', line 143

def version
  execution = run(build_argv("version"), skip_version_check: true)
  Yobi::ResticVersion.new(parse_version_output(execution[:output].to_s))
end