yobi 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edd02fb1bbb985e22d5e32e8558b37f469809e9b5cdc079c6b4885e5ada5a775
4
- data.tar.gz: 4d6a90b324596e6fc375a1338fdba1c70d84a6bf9504b15513614c87458bd7c0
3
+ metadata.gz: 47dd32402940fcd7c2699d932ca4a4e0db0f6e1ca4e8342ac5d41abc1cbe6114
4
+ data.tar.gz: 888feb226907437769093ffbad5245f159e5879acb9a6a12de77e908ef14ed51
5
5
  SHA512:
6
- metadata.gz: 9c11e446c74d42a68b7a34c61b4952451f0929d56114a6439ec6462f854019ea40d7844dd730371026a679f744a464600c997ce76b884de538a38ee879348efc
7
- data.tar.gz: e3245e59a13c6b9f8712824406f0844106e1495a543ff68cf2ac11210835ef480109755acf2019ca5b10708d46436bc1e58f7e9e122f450ce3dd4f40395695fb
6
+ metadata.gz: d9d683046fe0d910622731b5e527f2fda6a93ddf753bc849d9066ae2432667fe4feb2a3c404c48f7e0a54c7cacb5442bf7b1ab36cefd17d24e21388932342893
7
+ data.tar.gz: 46c31f00651a04b6fd67ec3ccba73ff1bca4b6f2f8deccd3f18233f274d0c8c1394f19d4472dd96013ac340adb225d20f4fc87e8a41d49eeeef9f34dd256b8d4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [0.3.0] - 2026-08-03
2
+
3
+ ### Changed
4
+
5
+ - **Breaking:** `#init`/`#copy`'s `from_repository_file:` kwarg removed - `from_repo:` is now polymorphic, additionally accepting a `[:file, "..."]` tuple to read the source repository's URL from a file, alongside the existing plain URL String/`Repository` instance shapes.
6
+ - Invalid shapes for `password:`, `backend_credentials:`, `restic:`, `source:`/a `stdin_from_command` command, `new_password:`, `from_repo:`, and `from_password:` now raise `ArgumentError` with a message like `"invalid password shape"`, instead of a raw `NoMatchingPatternError`/`NoMatchingPatternKeyError`.
7
+ - `Repository#initialize`'s `password:`/`backend_credentials:` shapes are now validated eagerly at construction (and `from_password:`/`from_repo:` eagerly in `#init`/`#copy`), instead of lazily on the first `#env`/`#inspect`/argv-building call.
8
+
1
9
  ## [0.2.0] - 2026-08-03
2
10
 
3
11
  ### Added
data/README.md CHANGED
@@ -521,7 +521,7 @@ A shortcut for setting up a `#copy` destination: constructs a new `Repository` a
521
521
  mirror_repo.copy(from_repo: primary_repo, tags: "nightly")
522
522
  ```
523
523
 
524
- Replicates snapshots from another repository into this one. `from_repo:` accepts a plain URL String or a `Repository` instance. Given the latter, its own `#url`/`#password` are used automatically unless `from_password:` is given explicitly. Already-copied snapshots are skipped automatically on a repeat run. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#copy-instance_method) for the full list of options. Returns `true`.
524
+ Replicates snapshots from another repository into this one. `from_repo:` accepts a plain URL String, a `[:file, "..."]` tuple reading the URL from a file, or a `Repository` instance. Given the latter, its own `#url`/`#password` are used automatically unless `from_password:` is given explicitly. Already-copied snapshots are skipped automatically on a repeat run. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#copy-instance_method) for the full list of options. Returns `true`.
525
525
 
526
526
  ## Mounting
527
527
 
@@ -83,6 +83,8 @@ module Yobi
83
83
  a.flag(:stdin_from_command)
84
84
  a.flag(:stdin_filename, filename)
85
85
  a.end_of_options.append(tokenize(command))
86
+ else
87
+ raise ArgumentError, "invalid source shape"
86
88
  end
87
89
  end
88
90
 
@@ -99,6 +101,8 @@ module Yobi
99
101
  Shellwords.split(value)
100
102
  in Array => values
101
103
  values.map(&:to_s)
104
+ else
105
+ raise ArgumentError, "invalid stdin_from_command command shape"
102
106
  end
103
107
  end
104
108
  end
@@ -5,33 +5,31 @@ module Yobi
5
5
  # `restic copy`: replicates snapshots from another repository into
6
6
  # this one. Already-copied snapshots are skipped automatically.
7
7
  #
8
- # @param from_repo [String, Yobi::Repository] the source repository, or its URL
9
- # (given a `Repository`, its own `#url`/`#password` are used automatically)
8
+ # @param from_repo [String, Yobi::Repository, Array] the source repository's
9
+ # URL; a `Repository` instance (its own `#url`/`#password` are used
10
+ # automatically); or a `[:file, "..."]` tuple reading the URL from a file
10
11
  # @param from_password [String, Array, Symbol, #call, nil] the source
11
12
  # repository's password, same shape as {#initialize}'s `password:`
12
13
  # @param snapshot_ids [Array<String>, String] snapshots to copy; all of them if empty
13
14
  # @param from_key_hint [String, nil]
14
- # @param from_repository_file [String, nil] read the source repository's URL from a file
15
15
  # @param hosts [Array<String>, String] filter by hostname(s)
16
16
  # @param paths [Array<String>, String] filter by originally backed-up path(s)
17
17
  # @param tags [Array<String>, String] filter by tag(s)
18
18
  # @return [true]
19
- def copy(from_repo:, from_password: nil, snapshot_ids: [], from_key_hint: nil,
20
- from_repository_file: nil, hosts: [], paths: [], tags: [])
21
- if from_repo.is_a?(Repository)
22
- from_password = from_repo.password if from_password.nil?
23
- from_repo = from_repo.url
24
- end
19
+ def copy(from_repo:, from_password: nil, snapshot_ids: [], from_key_hint: nil, hosts: [], paths: [], tags: [])
20
+ from_repo_flag, from_repo_value = resolved_from_repo_flag(from_repo)
21
+ from_password = from_repo_password(from_repo) if from_password.nil?
22
+ validate_from_password_shape!(from_password)
25
23
 
26
24
  argv = build_argv("copy", snapshot_ids) do |a|
27
- a.flag(:from_repo, from_repo)
25
+ a.flag(from_repo_flag, from_repo_value)
28
26
  a.flag(:from_insecure_no_password) if from_password == :insecure_no_password
29
27
  a.flag(:from_key_hint, from_key_hint) unless from_key_hint.nil?
30
- a.flag(:from_repository_file, from_repository_file) unless from_repository_file.nil?
31
28
  a.repeat_flag(:host, hosts)
32
29
  a.repeat_flag(:path, paths)
33
30
  a.repeat_flag(:tag, tags)
34
31
  end
32
+
35
33
  run_restic(argv, extra_env: password_env(from_password, "RESTIC_FROM_PASSWORD"))
36
34
  true
37
35
  end
@@ -6,31 +6,32 @@ module Yobi
6
6
  class Repository
7
7
  # `restic init`: creates the repository at {#url}.
8
8
  #
9
- # @param copy_chunker_params [Boolean] copy chunker parameters from `from_repo:`/`from_repository_file:`
9
+ # @param copy_chunker_params [Boolean] copy chunker parameters from `from_repo:`
10
10
  # @param from_key_hint [String, nil]
11
11
  # @param from_password [String, Array, Symbol, #call, nil] the source
12
12
  # repository's password (when copying chunker params from one), same
13
13
  # shape as {#initialize}'s `password:`
14
- # @param from_repo [String, Yobi::Repository, nil] the source repository, or its URL
15
- # (given a `Repository`, its own `#url`/`#password` are used automatically)
16
- # @param from_repository_file [String, nil]
14
+ # @param from_repo [String, Yobi::Repository, Array, nil] the source repository's
15
+ # URL; a `Repository` instance (its own `#url`/`#password` are used
16
+ # automatically); or a `[:file, "..."]` tuple reading the URL from a file
17
17
  # @param repository_version [String, nil]
18
18
  # @return [Yobi::Initialized]
19
- def init(copy_chunker_params: false, from_key_hint: nil, from_password: nil, from_repo: nil,
20
- from_repository_file: nil, repository_version: nil)
21
- if from_repo.is_a?(Repository)
22
- from_password = from_repo.password if from_password.nil?
23
- from_repo = from_repo.url
19
+ def init(copy_chunker_params: false, from_key_hint: nil, from_password: nil, from_repo: nil, repository_version: nil)
20
+ from_repo_flag, from_repo_value = resolved_from_repo_flag(from_repo) unless from_repo.nil?
21
+
22
+ if from_repo_value
23
+ from_password = from_repo_password(from_repo) if from_password.nil?
24
+ validate_from_password_shape!(from_password)
24
25
  end
25
26
 
26
27
  argv = build_argv("init") do |a|
27
28
  a.flag(:copy_chunker_params) if copy_chunker_params
28
29
  a.flag(:from_insecure_no_password) if from_password == :insecure_no_password
29
30
  a.flag(:from_key_hint, from_key_hint) unless from_key_hint.nil?
30
- a.flag(:from_repo, from_repo) unless from_repo.nil?
31
- a.flag(:from_repository_file, from_repository_file) unless from_repository_file.nil?
31
+ a.flag(from_repo_flag, from_repo_value) if from_repo_flag
32
32
  a.flag(:repository_version, repository_version) unless repository_version.nil?
33
33
  end
34
+
34
35
  execution = run_restic(argv, extra_env: password_env(from_password, "RESTIC_FROM_PASSWORD"))
35
36
  Initialized.new(JSON.parse(execution[:output].to_s))
36
37
  end
@@ -24,7 +24,7 @@ module Yobi
24
24
  argv = build_argv("key", "add") do |a|
25
25
  a.flag(:host, host) unless host.nil?
26
26
  a.flag(:user, user) unless user.nil?
27
- a.flag(flag_name, flag_value) unless flag_name.nil?
27
+ a.flag(flag_name, flag_value)
28
28
  end
29
29
  run_restic(argv)
30
30
  end
@@ -55,7 +55,7 @@ module Yobi
55
55
  argv = build_argv("key", "passwd") do |a|
56
56
  a.flag(:host, host) unless host.nil?
57
57
  a.flag(:user, user) unless user.nil?
58
- a.flag(flag_name, flag_value) unless flag_name.nil?
58
+ a.flag(flag_name, flag_value)
59
59
  end
60
60
  run_restic(argv)
61
61
  end
@@ -88,6 +88,8 @@ module Yobi
88
88
  with_temp_password_file(literal) { |path| yield :new_password_file, path }
89
89
  in callable if callable.respond_to?(:call)
90
90
  with_temp_password_file(callable.call.to_s) { |path| yield :new_password_file, path }
91
+ else
92
+ raise ArgumentError, "invalid new_password shape"
91
93
  end
92
94
  end
93
95
 
@@ -90,7 +90,7 @@ module Yobi
90
90
  self.class.dispatch(exit_code: status.exitstatus, output: output, argv: argv)
91
91
  end
92
92
  rescue Errno::ENOENT
93
- output&.file&.close
93
+ output.file.close
94
94
  raise Yobi::ResticNotFound.new(restic_path: restic_path, argv: argv)
95
95
  end
96
96
 
@@ -23,9 +23,10 @@ module Yobi
23
23
  # responding to `#call` returning such a Hash
24
24
  # @param restic [Yobi::Restic, String, nil] a `Restic` instance to share,
25
25
  # a bare Restic binary path, or `nil` to create a default one
26
- # @raise [ArgumentError] if `password:` is `nil`
26
+ # @raise [ArgumentError] if `password:` is `nil`, or if `password:`/`backend_credentials:`/`restic:` is an invalid shape
27
27
  def initialize(url:, password:, backend_credentials: {}, restic: nil)
28
- raise ArgumentError, "password: is required (use :insecure_no_password if the repository truly has none)" if password.nil?
28
+ validate_password_shape!(password)
29
+ validate_backend_credentials_shape!(backend_credentials)
29
30
 
30
31
  @url, @extracted_rest_credentials = extract_rest_credentials(url)
31
32
  @password = password
@@ -119,6 +120,51 @@ module Yobi
119
120
  end
120
121
  end
121
122
 
123
+ def from_repo_password(from_repo)
124
+ from_repo.password if from_repo.is_a?(Repository)
125
+ end
126
+
127
+ def resolved_from_repo_flag(from_repo)
128
+ case from_repo
129
+ in Repository
130
+ [:from_repo, from_repo.url]
131
+ in String
132
+ [:from_repo, from_repo]
133
+ in [:file, String => path]
134
+ [:from_repository_file, path]
135
+ else
136
+ raise ArgumentError, "invalid from_repo shape"
137
+ end
138
+ end
139
+
140
+ def validate_password_shape!(value, name: "password")
141
+ case value
142
+ in nil
143
+ raise ArgumentError, "#{name}: is required (use :insecure_no_password if there truly is none)"
144
+ in :insecure_no_password | String | [:command, String] | [:file, String]
145
+ true
146
+ in callable if callable.respond_to?(:call)
147
+ true
148
+ else
149
+ raise ArgumentError, "invalid #{name} shape"
150
+ end
151
+ end
152
+
153
+ def validate_from_password_shape!(value)
154
+ validate_password_shape!(value, name: "from_password")
155
+ end
156
+
157
+ def validate_backend_credentials_shape!(value)
158
+ case value
159
+ in Hash
160
+ true
161
+ in callable if callable.respond_to?(:call)
162
+ true
163
+ else
164
+ raise ArgumentError, "invalid backend_credentials shape"
165
+ end
166
+ end
167
+
122
168
  def extract_rest_credentials(url)
123
169
  return [url, {}] unless url.start_with?("rest:")
124
170
 
@@ -139,6 +185,8 @@ module Yobi
139
185
  restic_or_restic_path
140
186
  in String | nil
141
187
  Yobi::Restic.new(restic_or_restic_path)
188
+ else
189
+ raise ArgumentError, "invalid restic shape"
142
190
  end
143
191
  end
144
192
  end
data/lib/yobi/restic.rb CHANGED
@@ -262,7 +262,7 @@ module Yobi
262
262
  self.class.dispatch(exit_code: status.exitstatus, output: output, argv: argv)
263
263
  result
264
264
  rescue Errno::ENOENT
265
- output&.file&.close
265
+ output.file.close
266
266
  raise Yobi::ResticNotFound.new(restic_path: restic_path, argv: argv)
267
267
  end
268
268
 
@@ -324,7 +324,7 @@ module Yobi
324
324
  _, status = Process.wait2(pid)
325
325
  {exit_code: status.exitstatus, output: output, argv: argv}
326
326
  rescue Errno::ENOENT
327
- output&.file&.close
327
+ output.file.close
328
328
  raise Yobi::ResticNotFound.new(restic_path: restic_path, argv: argv)
329
329
  end
330
330
 
@@ -352,7 +352,7 @@ module Yobi
352
352
  {exit_code: wait_thr.value.exitstatus, output: output, argv: argv}
353
353
  end
354
354
  rescue Errno::ENOENT
355
- output&.file&.close
355
+ output.file.close
356
356
  raise Yobi::ResticNotFound.new(restic_path: restic_path, argv: argv)
357
357
  end
358
358
  end
data/lib/yobi/version.rb CHANGED
@@ -4,5 +4,5 @@
4
4
  # plain Ruby objects instead of shelling out to flags and raw JSON by hand.
5
5
  module Yobi
6
6
  # @return [String]
7
- VERSION = "0.2.0"
7
+ VERSION = "0.3.0"
8
8
  end
data/sig/yobi.rbs CHANGED
@@ -221,6 +221,7 @@ module Yobi
221
221
  type new_password = String | [:file, String] | :insecure_no_password | (^() -> String)
222
222
  type backend_credentials = Hash[String, String] | (^() -> Hash[String, String])
223
223
  type restic_or_path = Restic | String | nil
224
+ type from_repo = String | Repository | [:file, String]
224
225
 
225
226
  class Restic
226
227
  MINIMUM_VERSION: Gem::Version
@@ -295,7 +296,7 @@ module Yobi
295
296
  def env: () -> Hash[String, String]
296
297
 
297
298
  def init: (?copy_chunker_params: bool, ?from_key_hint: String?, ?from_password: from_password,
298
- ?from_repo: String | Repository | nil, ?from_repository_file: String?, ?repository_version: String?) -> Initialized
299
+ ?from_repo: from_repo?, ?repository_version: String?) -> Initialized
299
300
 
300
301
  def init_mirror: (url: String, password: password, ?backend_credentials: backend_credentials,
301
302
  ?restic: restic_or_path, ?repository_version: String?) -> Repository
@@ -335,8 +336,8 @@ module Yobi
335
336
  def check: (?hosts: Array[String] | String, ?paths: Array[String] | String, ?read_data: bool,
336
337
  ?read_data_subset: String?, ?tags: Array[String] | String, ?with_cache: bool) -> CheckOutcome
337
338
 
338
- def copy: (from_repo: String | Repository, ?from_password: from_password, ?snapshot_ids: Array[String] | String,
339
- ?from_key_hint: String?, ?from_repository_file: String?, ?hosts: Array[String] | String,
339
+ def copy: (from_repo: from_repo, ?from_password: from_password, ?snapshot_ids: Array[String] | String,
340
+ ?from_key_hint: String?, ?hosts: Array[String] | String,
340
341
  ?paths: Array[String] | String, ?tags: Array[String] | String) -> true
341
342
 
342
343
  def diff: (from: String, to: String, ?metadata: bool) -> DiffOutcome
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yobi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zia Perdana
@@ -61,7 +61,7 @@ metadata:
61
61
  homepage_uri: https://codeberg.org/ukazap/yobi
62
62
  source_code_uri: https://codeberg.org/ukazap/yobi
63
63
  changelog_uri: https://codeberg.org/ukazap/yobi/src/branch/main/CHANGELOG.md
64
- documentation_uri: https://rubydoc.info/gems/yobi
64
+ documentation_uri: https://rubydoc.info/gems/yobi/0.3.0
65
65
  rdoc_options: []
66
66
  require_paths:
67
67
  - lib