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 +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +72 -17
- data/lib/ruborg/backup.rb +31 -15
- data/lib/ruborg/cli.rb +128 -80
- data/lib/ruborg/config.rb +7 -5
- data/lib/ruborg/repository.rb +15 -0
- data/lib/ruborg/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4d69dff5edaf281b1014e64653462df7d7af84be0f341db0c4fe389978f25b1
|
|
4
|
+
data.tar.gz: b206422e04dab022cd19a8d4ea6f9bd399988fc27b6e810fd673dfb7de0fb9ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 (
|
|
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
|
-
###
|
|
475
|
+
### Validate Repository Compatibility
|
|
475
476
|
|
|
476
477
|
```bash
|
|
477
478
|
# Check specific repository compatibility with installed Borg version
|
|
478
|
-
ruborg
|
|
479
|
+
ruborg validate repo --repository documents
|
|
479
480
|
|
|
480
481
|
# Check all repositories
|
|
481
|
-
ruborg
|
|
482
|
+
ruborg validate repo --all
|
|
482
483
|
|
|
483
484
|
# Check with data integrity verification (slower)
|
|
484
|
-
ruborg
|
|
485
|
+
ruborg validate repo --repository documents --verify-data
|
|
485
486
|
```
|
|
486
487
|
|
|
487
|
-
The `
|
|
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
|
-
---
|
|
498
|
+
--- Validating repository: documents ---
|
|
498
499
|
Repository version: 1
|
|
499
500
|
✓ Compatible with Borg 1.2.8
|
|
500
501
|
|
|
501
|
-
---
|
|
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/
|
|
669
|
-
- `--all`: Process all repositories (backup and
|
|
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 (
|
|
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
|
-
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
188
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
293
|
+
[repo_config]
|
|
294
|
+
else
|
|
295
|
+
raise ConfigError, "Please specify --repository or --all"
|
|
296
|
+
end
|
|
336
297
|
|
|
337
|
-
|
|
338
|
-
|
|
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("
|
|
302
|
+
@logger.error("Validation failed: #{e.message}")
|
|
342
303
|
raise
|
|
343
304
|
end
|
|
344
305
|
|
|
345
|
-
|
|
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 "---
|
|
350
|
-
@logger.info("
|
|
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 " ✗
|
|
396
|
-
@logger.error("
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
data/lib/ruborg/repository.rb
CHANGED
|
@@ -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"
|
data/lib/ruborg/version.rb
CHANGED
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.
|
|
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:
|
|
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.
|
|
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: []
|