ruborg 0.8.1 → 0.9.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: 74bbfee5ede7b87c5a99ddec057c90430ee66a5667e6a933455baf4478f569d2
4
- data.tar.gz: f7ddb9aa319573d39888fad0933de0ce99fcc08069eae459550605eba9ff1082
3
+ metadata.gz: a4d69dff5edaf281b1014e64653462df7d7af84be0f341db0c4fe389978f25b1
4
+ data.tar.gz: b206422e04dab022cd19a8d4ea6f9bd399988fc27b6e810fd673dfb7de0fb9ba
5
5
  SHA512:
6
- metadata.gz: 285d04844f53fc87e5af8b28d24fc9dfe410e1610e2c79845ea0b1a5ea8a7d60ed76c5df2f667068b0c2b55269870d9af2c30f64ed9f8efc63312d1f99acd60a
7
- data.tar.gz: e72be5e589955c1e3832a5f38236d485d61db23a8f60ece3a147a6ecf21eb1b66fd9e1eddbc77c4720c4dc69f675491eb4c7eb5dc22b93b2a1856757af4e7a96
6
+ metadata.gz: f881b5b0908afaa16729339263d1584d861cd3a3d6b613599cbdb120340a86a2dc69408dc3f630d347769b990ea6c36ced1b538d17ba1517bebfb347c5c9ef2b
7
+ data.tar.gz: ffd3ee5bd76b08ac9753d66ae95ac1aebaed2f4ba6db38ddc720e013970d2799a99202e4cf7295f969dd93ddf4f8b8b7df6d8730352a33f8ff86ce76eecbaff6
data/CHANGELOG.md CHANGED
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.0] - 2025-10-14
11
+
12
+ ### Changed
13
+ - **Command Consolidation**: Unified validation commands for consistency
14
+ - `ruborg validate` renamed to `ruborg validate config` (validates YAML configuration)
15
+ - `ruborg check` renamed to `ruborg validate repo` (validates repository compatibility and integrity)
16
+ - Both commands now use consistent `validate` terminology
17
+ - `--verify-data` option remains available for `validate repo` to run full integrity checks
18
+ - Eliminates confusion between `validate` (config) and `check` (repository)
19
+ - Updated README with new command syntax and examples
20
+ - Updated all tests to use new command format
21
+
22
+ ### Added
23
+ - **Skip Hash Check Option**: New `skip_hash_check` configuration option for faster per-file backups
24
+ - Skips expensive SHA256 content hash calculation when file already exists
25
+ - Trusts file path, size, and modification time matching for duplicate detection
26
+ - Significantly speeds up backups with many unchanged files
27
+ - Configurable globally or per-repository
28
+ - Default: `false` (paranoid mode - always verify content hash)
29
+ - Use case: Large directories where mtime changes are reliable (most filesystems)
30
+ - Example: `skip_hash_check: true` in YAML configuration
31
+ - **Migration Help**: `ruborg check` now displays a helpful deprecation notice
32
+ - Shows clear message explaining the command has been renamed
33
+ - Provides examples of the new `ruborg validate repo` syntax
34
+ - Exits with error to prevent confusion
35
+ - Logs deprecation warning for audit trail
36
+ - **Enhanced Version Command**: `ruborg version` now shows both Ruborg and Borg versions with path
37
+ - Displays Ruborg version (gem version)
38
+ - Displays installed Borg version and executable path
39
+ - Example output: `borg 1.2.8 (/usr/local/bin/borg)`
40
+ - Gracefully handles missing Borg installation
41
+ - Helps users verify both tool versions and location at a glance
42
+
10
43
  ## [0.8.1] - 2025-10-09
11
44
 
12
45
  ### Added
data/README.md CHANGED
@@ -25,7 +25,7 @@ A friendly Ruby frontend for [Borg Backup](https://www.borgbackup.org/). Ruborg
25
25
  - 📈 **Summary View** - Quick overview of all repositories and their configurations
26
26
  - 🔧 **Custom Borg Path** - Support for custom Borg executable paths per repository
27
27
  - 🏠 **Hostname Validation** - NEW! Restrict backups to specific hosts (global or per-repository)
28
- - ✅ **Well-tested** - Comprehensive test suite with RSpec (294 examples, 0 failures)
28
+ - ✅ **Well-tested** - Comprehensive test suite with RSpec (297 examples, 0 failures)
29
29
  - 🔒 **Security-focused** - Path validation, safe YAML loading, command injection protection
30
30
 
31
31
  ## Prerequisites
@@ -163,13 +163,14 @@ repositories:
163
163
 
164
164
  **Configuration Features:**
165
165
  - **Automatic Type Validation**: Configuration is validated on startup to catch type errors early
166
- - **Validation Command**: Run `ruborg validate` to check configuration files for errors
166
+ - **Validation Command**: Run `ruborg validate config` to check configuration files for errors
167
167
  - **Descriptions**: Add `description` field to document each repository's purpose
168
168
  - **Hostname Validation**: Optional `hostname` field to restrict backups to specific hosts (global or per-repository)
169
169
  - **Source Deletion Safety**: `allow_remove_source` flag to explicitly enable `--remove-source` option (default: disabled)
170
+ - **Skip Hash Check**: Optional `skip_hash_check` flag to skip content hash verification for faster backups (per-file mode only)
170
171
  - **Type-Safe Booleans**: Strict boolean validation prevents configuration errors (must use `true`/`false`, not strings)
171
- - **Global Settings**: Hostname, compression, encryption, auto_init, allow_remove_source, log_file, borg_path, borg_options, and retention apply to all repositories
172
- - **Per-Repository Overrides**: Any global setting can be overridden at the repository level (including hostname, allow_remove_source, and custom borg_path)
172
+ - **Global Settings**: Hostname, compression, encryption, auto_init, allow_remove_source, skip_hash_check, log_file, borg_path, borg_options, and retention apply to all repositories
173
+ - **Per-Repository Overrides**: Any global setting can be overridden at the repository level (including hostname, allow_remove_source, skip_hash_check, and custom borg_path)
173
174
  - **Custom Borg Path**: Specify a custom Borg executable path if borg is not in PATH or to use a specific version
174
175
  - **Retention Policies**: Define how many backups to keep (hourly, daily, weekly, monthly, yearly)
175
176
  - **Multiple Sources**: Each repository can have multiple backup sources with their own exclude patterns
@@ -184,7 +185,7 @@ Ruborg automatically validates your configuration on startup. All commands check
184
185
  Check your configuration file for errors:
185
186
 
186
187
  ```bash
187
- ruborg validate --config ruborg.yml
188
+ ruborg validate config --config ruborg.yml
188
189
  ```
189
190
 
190
191
  **Validation checks:**
@@ -471,20 +472,20 @@ Group: postgres
471
472
  Type: regular file
472
473
  ```
473
474
 
474
- ### Check Repository Compatibility
475
+ ### Validate Repository Compatibility
475
476
 
476
477
  ```bash
477
478
  # Check specific repository compatibility with installed Borg version
478
- ruborg check --repository documents
479
+ ruborg validate repo --repository documents
479
480
 
480
481
  # Check all repositories
481
- ruborg check --all
482
+ ruborg validate repo --all
482
483
 
483
484
  # Check with data integrity verification (slower)
484
- ruborg check --repository documents --verify-data
485
+ ruborg validate repo --repository documents --verify-data
485
486
  ```
486
487
 
487
- The `check` command verifies:
488
+ The `validate repo` command verifies:
488
489
  - Installed Borg version
489
490
  - Repository format version
490
491
  - Compatibility between Borg and repository versions
@@ -494,11 +495,11 @@ The `check` command verifies:
494
495
  ```
495
496
  Borg version: 1.2.8
496
497
 
497
- --- Checking repository: documents ---
498
+ --- Validating repository: documents ---
498
499
  Repository version: 1
499
500
  ✓ Compatible with Borg 1.2.8
500
501
 
501
- --- Checking repository: databases ---
502
+ --- Validating repository: databases ---
502
503
  Repository version: 2
503
504
  ✗ INCOMPATIBLE with Borg 1.2.8
504
505
  Repository version 2 cannot be read by Borg 1.2.8
@@ -652,26 +653,26 @@ See [SECURITY.md](SECURITY.md) for detailed security information and best practi
652
653
  | Command | Description | Options |
653
654
  |---------|-------------|---------|
654
655
  | `init REPOSITORY` | Initialize a new Borg repository | `--passphrase`, `--passbolt-id`, `--log` |
655
- | `validate` | Validate configuration file for type errors | `--config`, `--log` |
656
+ | `validate config` | Validate configuration file for type errors | `--config`, `--log` |
657
+ | `validate repo` | Validate repository compatibility and integrity | `--config`, `--repository`, `--all`, `--verify-data`, `--log` |
656
658
  | `backup` | Create a backup using config file | `--config`, `--repository`, `--all`, `--name`, `--remove-source`, `--log` |
657
659
  | `list` | List archives or files in repository | `--config`, `--repository`, `--archive`, `--log` |
658
660
  | `restore ARCHIVE` | Restore files from archive | `--config`, `--repository`, `--destination`, `--path`, `--log` |
659
661
  | `metadata ARCHIVE` | Get file metadata from archive | `--config`, `--repository`, `--file`, `--log` |
660
662
  | `info` | Show repository information | `--config`, `--repository`, `--log` |
661
- | `check` | Check repository integrity and compatibility | `--config`, `--repository`, `--all`, `--verify-data`, `--log` |
662
663
  | `version` | Show ruborg version | None |
663
664
 
664
665
  ### Options
665
666
 
666
667
  - `--config`: Path to configuration file (default: `ruborg.yml`)
667
668
  - `--log`: Path to log file (overrides config, default: `~/.ruborg/logs/ruborg.log`)
668
- - `--repository` / `-r`: Repository name (optional for info, required for backup/list/restore/check unless --all)
669
- - `--all`: Process all repositories (backup and check commands)
669
+ - `--repository` / `-r`: Repository name (optional for info, required for backup/list/restore/validate repo unless --all)
670
+ - `--all`: Process all repositories (backup and validate repo commands)
670
671
  - `--name`: Custom archive name (backup command only)
671
672
  - `--remove-source`: Remove source files after successful backup (backup command only)
672
673
  - `--destination`: Destination directory for restore (restore command only)
673
674
  - `--path`: Specific file or directory to restore (restore command only)
674
- - `--verify-data`: Run full data integrity check (check command only, slower)
675
+ - `--verify-data`: Run full data integrity check (validate repo command only, slower)
675
676
 
676
677
  ## Retention Policies
677
678
 
@@ -822,6 +823,60 @@ repositories:
822
823
 
823
824
  **Backup vs Retention:** The per-file `retention_mode` only affects how archives are created and pruned. Traditional backup commands still work normally - you can list, restore, and check per-file archives just like standard archives.
824
825
 
826
+ ### Skip Hash Check for Faster Backups
827
+
828
+ **NEW:** In per-file backup mode, you can optionally skip content hash verification for faster duplicate detection:
829
+
830
+ ```yaml
831
+ repositories:
832
+ - name: project-files
833
+ path: /mnt/backup/project-files
834
+ retention_mode: per_file
835
+ skip_hash_check: true # Skip SHA256 content hash verification
836
+ sources:
837
+ - name: projects
838
+ paths:
839
+ - /home/user/projects
840
+ ```
841
+
842
+ **How it works:**
843
+ - **Default (paranoid mode)**: Ruborg calculates SHA256 hash of file content to verify files haven't changed (even when size and mtime are identical)
844
+ - **With skip_hash_check: true**: Ruborg trusts file path, size, and modification time for duplicate detection (skips hash calculation)
845
+
846
+ **When to use:**
847
+ - ✅ **Large directories** with thousands of files where hash calculation is slow
848
+ - ✅ **Reliable filesystems** where modification time changes are trustworthy
849
+ - ✅ **Regular backups** where files are unlikely to be manually modified with `touch -t`
850
+
851
+ **When NOT to use:**
852
+ - ❌ **Security-critical data** where you want maximum verification
853
+ - ❌ **Untrusted sources** where files might be tampered with
854
+ - ❌ **Systems with unreliable mtime** (rare, but some network filesystems)
855
+
856
+ **Performance impact:**
857
+ ```yaml
858
+ # Example: 10,000 unchanged files, average 50KB each
859
+ # With skip_hash_check: false (default) - ~30 seconds (read + hash all files)
860
+ # With skip_hash_check: true - ~3 seconds (read metadata only)
861
+ ```
862
+
863
+ **Console output:**
864
+ ```
865
+ # With skip_hash_check: true
866
+ [1/10000] Backing up: /home/user/file1.txt - Archive already exists (skipped hash check)
867
+ [2/10000] Backing up: /home/user/file2.txt - Archive already exists (skipped hash check)
868
+ ...
869
+ ✓ Per-file backup completed: 50 file(s) backed up, 9950 skipped (hash check skipped)
870
+
871
+ # With skip_hash_check: false (default)
872
+ [1/10000] Backing up: /home/user/file1.txt - Archive already exists (file unchanged)
873
+ [2/10000] Backing up: /home/user/file2.txt - Archive already exists (file unchanged)
874
+ ...
875
+ ✓ Per-file backup completed: 50 file(s) backed up, 9950 skipped (unchanged)
876
+ ```
877
+
878
+ **Security note:** Even with `skip_hash_check: true`, files are still verified by path, size, and mtime. The only difference is skipping the SHA256 content hash verification, which catches rare edge cases like manual file tampering with preserved timestamps.
879
+
825
880
  ### Automatic Pruning
826
881
 
827
882
  Enable **automatic pruning** to remove old backups after each backup operation:
data/lib/ruborg/backup.rb CHANGED
@@ -3,12 +3,13 @@
3
3
  module Ruborg
4
4
  # Backup operations using Borg
5
5
  class Backup
6
- def initialize(repository, config:, retention_mode: "standard", repo_name: nil, logger: nil)
6
+ def initialize(repository, config:, retention_mode: "standard", repo_name: nil, logger: nil, skip_hash_check: false)
7
7
  @repository = repository
8
8
  @config = config
9
9
  @retention_mode = retention_mode
10
10
  @repo_name = repo_name
11
11
  @logger = logger
12
+ @skip_hash_check = skip_hash_check
12
13
  end
13
14
 
14
15
  def create(name: nil, remove_source: false)
@@ -89,15 +90,13 @@ module Ruborg
89
90
  stored_size = stored_info[:size]
90
91
 
91
92
  if current_size == stored_size
92
- # Size same -> verify content hasn't changed (paranoid mode)
93
- current_hash = calculate_file_hash(file_path)
94
- stored_hash = stored_info[:hash]
95
-
96
- if current_hash == stored_hash
97
- # Content truly unchanged - file is already safely backed up
98
- puts " - Archive already exists (file unchanged)"
93
+ # Size same -> verify content hasn't changed (paranoid mode) unless skip_hash_check is enabled
94
+ if @skip_hash_check
95
+ # Skip hash check - assume file is unchanged based on size and mtime
96
+ puts " - Archive already exists (skipped hash check)"
99
97
  @logger&.info(
100
- "[#{@repo_name}] Skipped #{file_path} - archive #{archive_name} already exists (file unchanged)"
98
+ "[#{@repo_name}] Skipped #{file_path} - archive #{archive_name} already exists " \
99
+ "(hash check skipped)"
101
100
  )
102
101
  skipped_count += 1
103
102
 
@@ -106,12 +105,29 @@ module Ruborg
106
105
 
107
106
  next
108
107
  else
109
- # Size same but content changed (rare: edited + truncated/padded to same size)
110
- archive_name = find_next_version_name(archive_name, existing_archives)
111
- @logger&.warn(
112
- "[#{@repo_name}] File content changed but size/mtime unchanged for #{file_path}, " \
113
- "using #{archive_name}"
114
- )
108
+ current_hash = calculate_file_hash(file_path)
109
+ stored_hash = stored_info[:hash]
110
+
111
+ if current_hash == stored_hash
112
+ # Content truly unchanged - file is already safely backed up
113
+ puts " - Archive already exists (file unchanged)"
114
+ @logger&.info(
115
+ "[#{@repo_name}] Skipped #{file_path} - archive #{archive_name} already exists (file unchanged)"
116
+ )
117
+ skipped_count += 1
118
+
119
+ # If remove_source is enabled, delete the file (it's already safely backed up)
120
+ remove_single_file(file_path) if remove_source
121
+
122
+ next
123
+ else
124
+ # Size same but content changed (rare: edited + truncated/padded to same size)
125
+ archive_name = find_next_version_name(archive_name, existing_archives)
126
+ @logger&.warn(
127
+ "[#{@repo_name}] File content changed but size/mtime unchanged for #{file_path}, " \
128
+ "using #{archive_name}"
129
+ )
130
+ end
115
131
  end
116
132
  else
117
133
  # Size changed but mtime same -> content changed, add version suffix
data/lib/ruborg/cli.rb CHANGED
@@ -184,8 +184,23 @@ module Ruborg
184
184
  raise
185
185
  end
186
186
 
187
- desc "validate", "Validate configuration file for errors and type issues"
188
- def validate_config
187
+ desc "validate TYPE", "Validate configuration file or repository (TYPE: config or repo)"
188
+ option :verify_data, type: :boolean, default: false, desc: "Verify repository data (slower, only for 'repo' type)"
189
+ option :all, type: :boolean, default: false, desc: "Validate all repositories (only for 'repo' type)"
190
+ def validate(type)
191
+ case type
192
+ when "config"
193
+ validate_config_implementation
194
+ when "repo"
195
+ validate_repo_implementation
196
+ else
197
+ raise ConfigError, "Invalid validation type: #{type}. Use 'config' or 'repo'"
198
+ end
199
+ end
200
+
201
+ private
202
+
203
+ def validate_config_implementation
189
204
  @logger.info("Validating configuration file: #{options[:config]}")
190
205
  config = Config.new(options[:config])
191
206
 
@@ -201,6 +216,7 @@ module Ruborg
201
216
  errors.concat(validate_boolean_setting(global_settings, "auto_init", "global"))
202
217
  errors.concat(validate_boolean_setting(global_settings, "auto_prune", "global"))
203
218
  errors.concat(validate_boolean_setting(global_settings, "allow_remove_source", "global"))
219
+ errors.concat(validate_boolean_setting(global_settings, "skip_hash_check", "global"))
204
220
 
205
221
  # Validate borg_options booleans
206
222
  if global_settings["borg_options"]
@@ -214,6 +230,7 @@ module Ruborg
214
230
  errors.concat(validate_boolean_setting(repo, "auto_init", repo_name))
215
231
  errors.concat(validate_boolean_setting(repo, "auto_prune", repo_name))
216
232
  errors.concat(validate_boolean_setting(repo, "allow_remove_source", repo_name))
233
+ errors.concat(validate_boolean_setting(repo, "skip_hash_check", repo_name))
217
234
 
218
235
  if repo["borg_options"]
219
236
  warnings.concat(validate_borg_option(repo["borg_options"], "allow_relocated_repo", repo_name))
@@ -257,64 +274,8 @@ module Ruborg
257
274
  raise
258
275
  end
259
276
 
260
- desc "version", "Show ruborg version"
261
- def version
262
- require_relative "version"
263
- puts "ruborg #{Ruborg::VERSION}"
264
- @logger.info("Version checked: #{Ruborg::VERSION}")
265
- end
266
-
267
- desc "metadata ARCHIVE", "Get file metadata from an archive"
268
- option :file, type: :string, desc: "Specific file path (required for standard archives, auto for per-file)"
269
- def metadata(archive_name)
270
- @logger.info("Getting metadata for archive: #{archive_name}")
271
- config = Config.new(options[:config])
272
-
273
- raise ConfigError, "Please specify --repository" unless options[:repository]
274
-
275
- repo_config = config.get_repository(options[:repository])
276
- raise ConfigError, "Repository '#{options[:repository]}' not found" unless repo_config
277
-
278
- global_settings = config.global_settings
279
- merged_config = global_settings.merge(repo_config)
280
- validate_hostname(merged_config)
281
- passphrase = fetch_passphrase_for_repo(merged_config)
282
- borg_opts = merged_config["borg_options"] || {}
283
- borg_path = merged_config["borg_path"]
284
-
285
- repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts, borg_path: borg_path,
286
- logger: @logger)
287
-
288
- raise BorgError, "Repository does not exist at #{repo_config["path"]}" unless repo.exists?
289
-
290
- # Get file metadata
291
- metadata = repo.get_file_metadata(archive_name, file_path: options[:file])
292
-
293
- # Display metadata
294
- puts "\n═══════════════════════════════════════════════════════════════"
295
- puts " FILE METADATA"
296
- puts "═══════════════════════════════════════════════════════════════\n\n"
297
- puts "Archive: #{archive_name}"
298
- puts "File: #{metadata["path"]}"
299
- puts "Size: #{format_size(metadata["size"])}"
300
- puts "Modified: #{metadata["mtime"]}"
301
- puts "Mode: #{metadata["mode"]}"
302
- puts "User: #{metadata["user"]}"
303
- puts "Group: #{metadata["group"]}"
304
- puts "Type: #{metadata["type"]}"
305
- puts ""
306
-
307
- @logger.info("Successfully retrieved metadata for #{metadata["path"]}")
308
- rescue Error => e
309
- @logger.error("Failed to get metadata: #{e.message}")
310
- raise
311
- end
312
-
313
- desc "check", "Check repository integrity and compatibility"
314
- option :verify_data, type: :boolean, default: false, desc: "Verify repository data (slower)"
315
- option :all, type: :boolean, default: false, desc: "Check all repositories"
316
- def check
317
- @logger.info("Checking repository compatibility")
277
+ def validate_repo_implementation
278
+ @logger.info("Validating repository compatibility")
318
279
  config = Config.new(options[:config])
319
280
  global_settings = config.global_settings
320
281
  validate_hostname(global_settings)
@@ -323,31 +284,29 @@ module Ruborg
323
284
  borg_version = Repository.borg_version
324
285
  puts "\nBorg version: #{borg_version}\n\n"
325
286
 
326
- repos_to_check = if options[:all]
327
- config.repositories
328
- elsif options[:repository]
329
- repo_config = config.get_repository(options[:repository])
330
- raise ConfigError, "Repository '#{options[:repository]}' not found" unless repo_config
287
+ repos_to_validate = if options[:all]
288
+ config.repositories
289
+ elsif options[:repository]
290
+ repo_config = config.get_repository(options[:repository])
291
+ raise ConfigError, "Repository '#{options[:repository]}' not found" unless repo_config
331
292
 
332
- [repo_config]
333
- else
334
- raise ConfigError, "Please specify --repository or --all"
335
- end
293
+ [repo_config]
294
+ else
295
+ raise ConfigError, "Please specify --repository or --all"
296
+ end
336
297
 
337
- repos_to_check.each do |repo_config|
338
- check_repository(repo_config, global_settings)
298
+ repos_to_validate.each do |repo_config|
299
+ validate_repository(repo_config, global_settings)
339
300
  end
340
301
  rescue Error => e
341
- @logger.error("Check failed: #{e.message}")
302
+ @logger.error("Validation failed: #{e.message}")
342
303
  raise
343
304
  end
344
305
 
345
- private
346
-
347
- def check_repository(repo_config, global_settings)
306
+ def validate_repository(repo_config, global_settings)
348
307
  repo_name = repo_config["name"]
349
- puts "--- Checking repository: #{repo_name} ---"
350
- @logger.info("Checking repository: #{repo_name}")
308
+ puts "--- Validating repository: #{repo_name} ---"
309
+ @logger.info("Validating repository: #{repo_name}")
351
310
 
352
311
  merged_config = global_settings.merge(repo_config)
353
312
  validate_hostname(merged_config)
@@ -392,11 +351,96 @@ module Ruborg
392
351
 
393
352
  puts ""
394
353
  rescue BorgError => e
395
- puts " ✗ Check failed: #{e.message}"
396
- @logger.error("Check failed for #{repo_name}: #{e.message}")
354
+ puts " ✗ Validation failed: #{e.message}"
355
+ @logger.error("Validation failed for #{repo_name}: #{e.message}")
397
356
  puts ""
398
357
  end
399
358
 
359
+ public
360
+
361
+ desc "version", "Show ruborg and borg versions"
362
+ def version
363
+ require_relative "version"
364
+ puts "ruborg #{Ruborg::VERSION}"
365
+ @logger.info("Version checked: #{Ruborg::VERSION}")
366
+
367
+ begin
368
+ borg_version = Repository.borg_version
369
+ borg_path = Repository.borg_path
370
+ puts "borg #{borg_version} (#{borg_path})"
371
+ @logger.info("Borg version: #{borg_version}, path: #{borg_path}")
372
+ rescue BorgError => e
373
+ puts "borg: not found or not executable"
374
+ @logger.warn("Could not determine Borg version: #{e.message}")
375
+ end
376
+ end
377
+
378
+ desc "check", "DEPRECATED: Use 'ruborg validate repo' instead"
379
+ option :verify_data, type: :boolean, default: false, desc: "Verify repository data (slower)"
380
+ option :all, type: :boolean, default: false, desc: "Validate all repositories"
381
+ def check
382
+ puts "\n⚠️ DEPRECATED COMMAND"
383
+ puts "══════════════════════════════════════════════════════════════════\n\n"
384
+ puts "The 'ruborg check' command has been renamed for consistency.\n"
385
+ puts "Please use: ruborg validate repo\n\n"
386
+ puts "Examples:"
387
+ puts " ruborg validate repo --repository documents"
388
+ puts " ruborg validate repo --all"
389
+ puts " ruborg validate repo --repository documents --verify-data\n\n"
390
+ puts "══════════════════════════════════════════════════════════════════\n"
391
+
392
+ @logger.warn("Deprecated command 'check' was called. User should use 'validate repo' instead.")
393
+ exit 1
394
+ end
395
+
396
+ desc "metadata ARCHIVE", "Get file metadata from an archive"
397
+ option :file, type: :string, desc: "Specific file path (required for standard archives, auto for per-file)"
398
+ def metadata(archive_name)
399
+ @logger.info("Getting metadata for archive: #{archive_name}")
400
+ config = Config.new(options[:config])
401
+
402
+ raise ConfigError, "Please specify --repository" unless options[:repository]
403
+
404
+ repo_config = config.get_repository(options[:repository])
405
+ raise ConfigError, "Repository '#{options[:repository]}' not found" unless repo_config
406
+
407
+ global_settings = config.global_settings
408
+ merged_config = global_settings.merge(repo_config)
409
+ validate_hostname(merged_config)
410
+ passphrase = fetch_passphrase_for_repo(merged_config)
411
+ borg_opts = merged_config["borg_options"] || {}
412
+ borg_path = merged_config["borg_path"]
413
+
414
+ repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts, borg_path: borg_path,
415
+ logger: @logger)
416
+
417
+ raise BorgError, "Repository does not exist at #{repo_config["path"]}" unless repo.exists?
418
+
419
+ # Get file metadata
420
+ metadata = repo.get_file_metadata(archive_name, file_path: options[:file])
421
+
422
+ # Display metadata
423
+ puts "\n═══════════════════════════════════════════════════════════════"
424
+ puts " FILE METADATA"
425
+ puts "═══════════════════════════════════════════════════════════════\n\n"
426
+ puts "Archive: #{archive_name}"
427
+ puts "File: #{metadata["path"]}"
428
+ puts "Size: #{format_size(metadata["size"])}"
429
+ puts "Modified: #{metadata["mtime"]}"
430
+ puts "Mode: #{metadata["mode"]}"
431
+ puts "User: #{metadata["user"]}"
432
+ puts "Group: #{metadata["group"]}"
433
+ puts "Type: #{metadata["type"]}"
434
+ puts ""
435
+
436
+ @logger.info("Successfully retrieved metadata for #{metadata["path"]}")
437
+ rescue Error => e
438
+ @logger.error("Failed to get metadata: #{e.message}")
439
+ raise
440
+ end
441
+
442
+ private
443
+
400
444
  def show_repositories_summary(config)
401
445
  repositories = config.repositories
402
446
  global_settings = config.global_settings
@@ -578,10 +622,14 @@ module Ruborg
578
622
  end
579
623
  end
580
624
 
625
+ # Get skip_hash_check setting (defaults to false)
626
+ skip_hash_check = merged_config["skip_hash_check"]
627
+ skip_hash_check = false unless skip_hash_check == true
628
+
581
629
  # Create backup config wrapper
582
630
  backup_config = BackupConfig.new(repo_config, merged_config)
583
631
  backup = Backup.new(repo, config: backup_config, retention_mode: retention_mode, repo_name: repo_name,
584
- logger: @logger)
632
+ logger: @logger, skip_hash_check: skip_hash_check)
585
633
 
586
634
  archive_name = options[:name] ? sanitize_archive_name(options[:name]) : nil
587
635
  @logger.info("Creating archive#{"s" if retention_mode == "per_file"}: #{archive_name || "auto-generated"}")
data/lib/ruborg/config.rb CHANGED
@@ -41,7 +41,7 @@ module Ruborg
41
41
 
42
42
  def global_settings
43
43
  @data.slice("passbolt", "compression", "encryption", "auto_init", "borg_options", "log_file", "retention",
44
- "auto_prune", "hostname", "allow_remove_source", "borg_path")
44
+ "auto_prune", "hostname", "allow_remove_source", "borg_path", "skip_hash_check")
45
45
  end
46
46
 
47
47
  private
@@ -54,12 +54,12 @@ module Ruborg
54
54
  # Valid configuration keys at each level
55
55
  VALID_GLOBAL_KEYS = %w[
56
56
  hostname compression encryption auto_init auto_prune allow_remove_source
57
- log_file borg_path passbolt borg_options retention repositories
57
+ log_file borg_path passbolt borg_options retention repositories skip_hash_check
58
58
  ].freeze
59
59
 
60
60
  VALID_REPOSITORY_KEYS = %w[
61
61
  name description path hostname retention_mode passbolt retention sources
62
- compression encryption auto_init auto_prune borg_options allow_remove_source
62
+ compression encryption auto_init auto_prune borg_options allow_remove_source skip_hash_check
63
63
  ].freeze
64
64
 
65
65
  VALID_SOURCE_KEYS = %w[name paths exclude].freeze
@@ -122,8 +122,9 @@ module Ruborg
122
122
  errors.concat(validate_boolean_config(@data, "auto_init", "global"))
123
123
  errors.concat(validate_boolean_config(@data, "auto_prune", "global"))
124
124
  errors.concat(validate_boolean_config(@data, "allow_remove_source", "global"))
125
+ errors.concat(validate_boolean_config(@data, "skip_hash_check", "global"))
125
126
 
126
- # Note: borg_options are validated as warnings in CLI validate command, not as errors here
127
+ # NOTE: borg_options are validated as warnings in CLI validate command, not as errors here
127
128
 
128
129
  # Validate global passbolt
129
130
  errors.concat(validate_passbolt_config(@data["passbolt"], "global")) if @data["passbolt"]
@@ -151,6 +152,7 @@ module Ruborg
151
152
  errors.concat(validate_boolean_config(repo, "auto_init", repo_name))
152
153
  errors.concat(validate_boolean_config(repo, "auto_prune", repo_name))
153
154
  errors.concat(validate_boolean_config(repo, "allow_remove_source", repo_name))
155
+ errors.concat(validate_boolean_config(repo, "skip_hash_check", repo_name))
154
156
 
155
157
  # Validate retention_mode
156
158
  if repo["retention_mode"] && !VALID_RETENTION_MODES.include?(repo["retention_mode"])
@@ -158,7 +160,7 @@ module Ruborg
158
160
  "Must be one of: #{VALID_RETENTION_MODES.join(", ")}"
159
161
  end
160
162
 
161
- # Note: borg_options are validated as warnings in CLI validate command, not as errors here
163
+ # NOTE: borg_options are validated as warnings in CLI validate command, not as errors here
162
164
 
163
165
  errors.concat(validate_passbolt_config(repo["passbolt"], repo_name)) if repo["passbolt"]
164
166
 
@@ -477,6 +477,21 @@ module Ruborg
477
477
  match[1]
478
478
  end
479
479
 
480
+ # Get Borg path (full path to executable)
481
+ def self.borg_path(borg_command = "borg")
482
+ # If it's an absolute or relative path, expand it
483
+ return File.expand_path(borg_command) if borg_command.include?("/")
484
+
485
+ # Otherwise, search in PATH
486
+ ENV["PATH"].split(File::PATH_SEPARATOR).each do |directory|
487
+ path = File.join(directory, borg_command)
488
+ return path if File.executable?(path)
489
+ end
490
+
491
+ # Not found in PATH, return the command as-is
492
+ borg_command
493
+ end
494
+
480
495
  # Execute borg version command (extracted for testing)
481
496
  def self.execute_version_command(borg_path = "borg")
482
497
  require "open3"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruborg
4
- VERSION = "0.8.1"
4
+ VERSION = "0.9.0"
5
5
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruborg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michail Pantelelis
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2025-10-14 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: psych
@@ -160,6 +161,7 @@ metadata:
160
161
  source_code_uri: https://github.com/mpantel/ruborg.git
161
162
  changelog_uri: https://github.com/mpantel/ruborg/blob/main/CHANGELOG.md
162
163
  rubygems_mfa_required: 'true'
164
+ post_install_message:
163
165
  rdoc_options: []
164
166
  require_paths:
165
167
  - lib
@@ -174,7 +176,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
174
176
  - !ruby/object:Gem::Version
175
177
  version: '0'
176
178
  requirements: []
177
- rubygems_version: 3.7.1
179
+ rubygems_version: 3.5.22
180
+ signing_key:
178
181
  specification_version: 4
179
182
  summary: A friendly Ruby frontend for Borg backup
180
183
  test_files: []