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 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 UnsupportedResticVersion.

Returns:

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

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

Returns:

  • (Set[String])
%w[
  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 =
"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

Returns a new instance of Restic.

Parameters:

  • restic_path (String, nil) (defaults to: nil)

    defaults to $RESTIC_PATH, then "restic"

  • env (Hash{String => String}) (defaults to: {})

    extra env vars, merged over the named settings below

  • cache_dir (String, nil) (defaults to: nil)
  • compression (String, nil) (defaults to: nil)
  • pack_size (Integer, nil) (defaults to: nil)
  • read_concurrency (Integer, nil) (defaults to: nil)
  • host (String, nil) (defaults to: nil)
  • progress_fps (Integer, nil) (defaults to: nil)
  • cacert (String, nil) (defaults to: nil)
  • tls_client_cert (String, nil) (defaults to: nil)
  • key_hint (String, nil) (defaults to: nil)
  • limit_download (String, nil) (defaults to: nil)
  • limit_upload (String, nil) (defaults to: nil)
  • retry_lock (String, nil) (defaults to: nil)
  • no_lock (Boolean) (defaults to: false)
  • no_cache (Boolean) (defaults to: false)
  • cleanup_cache (Boolean) (defaults to: false)
  • no_extra_verify (Boolean) (defaults to: false)
  • stuck_request_timeout (String, nil) (defaults to: nil)
  • options (Array<String>) (defaults to: [])
  • http_user_agent (String, nil) (defaults to: nil)
  • quiet (Boolean) (defaults to: false)


91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/yobi/restic.rb', line 91

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?

Returns $RESTIC_CACERT.

Returns:

  • (String, nil)

    $RESTIC_CACERT



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

def cacert
  @cacert
end

#cache_dirString?

Returns $RESTIC_CACHE_DIR.

Returns:

  • (String, nil)

    $RESTIC_CACHE_DIR



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

def cache_dir
  @cache_dir
end

#cleanup_cacheBoolean

Returns --cleanup-cache.

Returns:

  • (Boolean)

    --cleanup-cache



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

def cleanup_cache
  @cleanup_cache
end

#compressionString?

Returns $RESTIC_COMPRESSION.

Returns:

  • (String, nil)

    $RESTIC_COMPRESSION



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

def compression
  @compression
end

#hostString?

Returns $RESTIC_HOST.

Returns:

  • (String, nil)

    $RESTIC_HOST



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

def host
  @host
end

#http_user_agentString?

Returns --http-user-agent.

Returns:

  • (String, nil)

    --http-user-agent



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

def http_user_agent
  @http_user_agent
end

#key_hintString?

Returns $RESTIC_KEY_HINT.

Returns:

  • (String, nil)

    $RESTIC_KEY_HINT



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

def key_hint
  @key_hint
end

#limit_downloadString?

Returns --limit-download.

Returns:

  • (String, nil)

    --limit-download



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

def limit_download
  @limit_download
end

#limit_uploadString?

Returns --limit-upload.

Returns:

  • (String, nil)

    --limit-upload



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

def limit_upload
  @limit_upload
end

#no_cacheBoolean

Returns --no-cache.

Returns:

  • (Boolean)

    --no-cache



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

def no_cache
  @no_cache
end

#no_extra_verifyBoolean

Returns --no-extra-verify.

Returns:

  • (Boolean)

    --no-extra-verify



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

def no_extra_verify
  @no_extra_verify
end

#no_lockBoolean

Returns --no-lock.

Returns:

  • (Boolean)

    --no-lock



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

def no_lock
  @no_lock
end

#optionsArray<String>

Returns --option, one per element.

Returns:

  • (Array<String>)

    --option, one per element



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

def options
  @options
end

#pack_sizeInteger?

Returns $RESTIC_PACK_SIZE.

Returns:

  • (Integer, nil)

    $RESTIC_PACK_SIZE



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

def pack_size
  @pack_size
end

#progress_fpsInteger?

Returns $RESTIC_PROGRESS_FPS.

Returns:

  • (Integer, nil)

    $RESTIC_PROGRESS_FPS



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

def progress_fps
  @progress_fps
end

#quietBoolean

Returns --quiet.

Returns:

  • (Boolean)

    --quiet



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

def quiet
  @quiet
end

#read_concurrencyInteger?

Returns $RESTIC_READ_CONCURRENCY.

Returns:

  • (Integer, nil)

    $RESTIC_READ_CONCURRENCY



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

def read_concurrency
  @read_concurrency
end

#restic_pathString (readonly)

Returns path to the Restic binary.

Returns:

  • (String)

    path to the Restic binary



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

def restic_path
  @restic_path
end

#retry_lockString?

Returns --retry-lock.

Returns:

  • (String, nil)

    --retry-lock



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

def retry_lock
  @retry_lock
end

#stuck_request_timeoutString?

Returns --stuck-request-timeout.

Returns:

  • (String, nil)

    --stuck-request-timeout



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

def stuck_request_timeout
  @stuck_request_timeout
end

#tls_client_certString?

Returns $RESTIC_TLS_CLIENT_CERT.

Returns:

  • (String, nil)

    $RESTIC_TLS_CLIENT_CERT



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

def tls_client_cert
  @tls_client_cert
end

Class Method Details

.dispatch(execution) ⇒ Hash

Returns execution, unchanged, on exit code 0/3.

Parameters:

  • execution (Hash)

    {exit_code:, output:, argv:}

Returns:

  • (Hash)

    execution, unchanged, on exit code 0/3

Raises:



214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/yobi/restic.rb', line 214

def self.dispatch(execution)
  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:



145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/yobi/restic.rb', line 145

def append_global_flags(a)
  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.

Parameters:

  • cleanup (Boolean) (defaults to: false)

    --cleanup

  • max_age (String, nil) (defaults to: nil)

    --max-age

  • no_size (Boolean) (defaults to: false)

    --no-size

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

Returns:

  • (true)


179
180
181
182
183
184
185
186
187
# File 'lib/yobi/restic.rb', line 179

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.



276
277
278
279
280
281
282
283
284
# File 'lib/yobi/restic.rb', line 276

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})


125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/yobi/restic.rb', line 125

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

#inspectString

Returns:

  • (String)


160
161
162
# File 'lib/yobi/restic.rb', line 160

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

#run(argv, extra_env: {}, skip_version_check: false, output: nil) {|message| ... } ⇒ Hash

Runs argv against this Restic binary, merging extra_env with this instance's own global env.

Parameters:

  • argv (Array<String>)
  • extra_env (Hash{String => String}) (defaults to: {})
  • skip_version_check (Boolean) (defaults to: false)

    used internally by #version to avoid recursing into #ensure_minimum_version!

  • output (Yobi::ResticOutput, nil) (defaults to: nil)

    a caller-configured one (e.g. with its own transform:), used instead of a plain one this creates itself; forces streaming even without a block

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

Yield Parameters:

  • message (Object)

    each message, live, as the command runs - transformed via output's own transform: if it has one, the raw parsed Hash otherwise

Returns:

  • (Hash)

    {exit_code:, output:, argv:} on exit code 0/3

Raises:



201
202
203
204
205
206
207
208
209
# File 'lib/yobi/restic.rb', line 201

def run(argv, extra_env: {}, skip_version_check: false, output: nil, &block)
  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 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:

Parameters:

  • argv (Array<String>)
  • extra_env (Hash{String => String}) (defaults to: {})

Yields:

Yield Parameters:

  • io (IO)

Yield Returns:

  • (Object)

Returns:

  • (Yobi::IOHandle)

    if no block is given

  • (Object)

    the block's own return value, otherwise



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/yobi/restic.rb', line 244

def run_dump(argv, extra_env: {})
  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) ⇒ Yobi::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.

Parameters:

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

Returns:

Raises:



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/yobi/repository/mount.rb', line 74

def run_mount(argv, mountpoint:, extra_env: {}, ready_timeout: 10)
  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

#versionYobi::ResticVersion

restic version: the installed binary's own version info.

Returns:



167
168
169
170
# File 'lib/yobi/restic.rb', line 167

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