ruborg 0.6.0 → 0.6.1
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/.ruby-version +1 -0
- data/CHANGELOG.md +39 -0
- data/README.md +125 -32
- data/SECURITY.md +76 -0
- data/exe/ruborg +9 -1
- data/lib/ruborg/backup.rb +33 -3
- data/lib/ruborg/cli.rb +88 -22
- data/lib/ruborg/config.rb +161 -11
- data/lib/ruborg/passbolt.rb +9 -2
- data/lib/ruborg/repository.rb +18 -4
- data/lib/ruborg/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 53e7193db767d8d7c217f89a4af6e7b4b4e18cdcc38cf0a8dff138767b561c10
|
|
4
|
+
data.tar.gz: 25c0232d3e86628d9ffb350f1d85e08f71ca032e72183e8d592108533ef2bc51
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6149c0535369f3f3b8f9829985fe6fa47777d0e2a6bcaf87630044bb7d7d2426be02ab104c87b50bc309352f9bdb80cad123e1549fc7c941dfe60b052840c95e
|
|
7
|
+
data.tar.gz: c0b036527fe8a70f526eebf07173f740d45fb88cd1fec3cd67a70b7331d7f806346d42b51217eefe4db2e8a8a7257f378a902c7711de657d9ef1603a460aaf46
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.2.9
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.1] - 2025-10-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Enhanced Configuration Validation**: Comprehensive validation system to catch configuration errors early
|
|
14
|
+
- **Unknown Key Detection**: Detects typos and invalid configuration keys at all levels (global, repository, sources, retention, passbolt, borg_options)
|
|
15
|
+
- **Retention Policy Validation**: Validates retention policy structure and values
|
|
16
|
+
- Integer fields (keep_hourly, keep_daily, etc.) must be non-negative integers
|
|
17
|
+
- Time-based fields (keep_within, keep_files_modified_within) must use correct format (e.g., "7d", "30d")
|
|
18
|
+
- Validates time format with h/d/w/m/y suffixes
|
|
19
|
+
- Rejects empty retention policies
|
|
20
|
+
- Detects unknown retention keys
|
|
21
|
+
- **Passbolt Configuration Validation**: Validates passbolt config structure
|
|
22
|
+
- Requires non-empty `resource_id` string
|
|
23
|
+
- Type validation for resource_id field
|
|
24
|
+
- Detects unknown passbolt keys
|
|
25
|
+
- **Retention Mode Validation**: Validates `retention_mode` values (must be "standard" or "per_file")
|
|
26
|
+
- **Source Validation**: Validates source structure (name, paths, exclude fields)
|
|
27
|
+
- **Comprehensive Logging**: Added logging throughout backup, restore, and deletion operations
|
|
28
|
+
- Repository operations (initialization, pruning, archive management)
|
|
29
|
+
- Backup operations (file counts, progress in per-file mode)
|
|
30
|
+
- Restore operations (extraction start/completion)
|
|
31
|
+
- Source file deletion tracking (with `--remove-source`)
|
|
32
|
+
- Passbolt integration events (resource ID logged, never passwords)
|
|
33
|
+
- All sensitive data (passwords, encryption keys) protected from logs
|
|
34
|
+
- **Enhanced Test Suite**: Expanded test coverage to 220 examples (67 new tests added)
|
|
35
|
+
- 23 new configuration validation tests
|
|
36
|
+
- 28 new logging integration tests
|
|
37
|
+
- 10 new CLI validation tests
|
|
38
|
+
- 6 new type checking tests for boolean configurations
|
|
39
|
+
- All tests passing with 0 failures
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- `global_settings` now includes `borg_path` (previously was in whitelist but not propagated)
|
|
43
|
+
- Validation errors are collected and reported together for better user experience
|
|
44
|
+
- All validation runs automatically on configuration load
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
- **Configuration Consistency**: Fixed inconsistency where `borg_path` was allowed in VALID_GLOBAL_KEYS but not returned by `global_settings` method
|
|
48
|
+
|
|
10
49
|
## [0.6.0] - 2025-10-08
|
|
11
50
|
|
|
12
51
|
### Added
|
data/README.md
CHANGED
|
@@ -17,7 +17,7 @@ A friendly Ruby frontend for [Borg Backup](https://www.borgbackup.org/). Ruborg
|
|
|
17
17
|
- 📊 **Logging** - Comprehensive logging with daily rotation
|
|
18
18
|
- 🗄️ **Multi-Repository** - Manage multiple backup repositories with different sources
|
|
19
19
|
- 🔄 **Auto-initialization** - Automatically initialize repositories on first use
|
|
20
|
-
|
|
20
|
+
- ⏰ **Retention Policies** - Configure backup retention (hourly, daily, weekly, monthly, yearly)
|
|
21
21
|
- 🗑️ **Automatic Pruning** - Automatically remove old backups based on retention policies
|
|
22
22
|
- 📁 **Per-File Backup Mode** - NEW! Backup each file as a separate archive with metadata-based retention
|
|
23
23
|
- 🕒 **File Metadata Retention** - NEW! Prune based on file modification time, works even after files are deleted
|
|
@@ -25,7 +25,7 @@ e- ⏰ **Retention Policies** - Configure backup retention (hourly, daily, weekl
|
|
|
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 (220 examples, 0 failures)
|
|
29
29
|
- 🔒 **Security-focused** - Path validation, safe YAML loading, command injection protection
|
|
30
30
|
|
|
31
31
|
## Prerequisites
|
|
@@ -188,11 +188,20 @@ ruborg validate --config ruborg.yml
|
|
|
188
188
|
```
|
|
189
189
|
|
|
190
190
|
**Validation checks:**
|
|
191
|
-
-
|
|
192
|
-
-
|
|
193
|
-
-
|
|
194
|
-
-
|
|
195
|
-
-
|
|
191
|
+
- **Unknown configuration keys**: Detects typos and invalid keys at all levels (catches `auto_prun` vs `auto_prune`)
|
|
192
|
+
- **Boolean types**: Must be `true` or `false`, not strings like `'true'`
|
|
193
|
+
- **Retention policies**: Validates structure and values
|
|
194
|
+
- Integer fields (keep_hourly, keep_daily, etc.) must be non-negative integers
|
|
195
|
+
- Time-based fields (keep_within, keep_files_modified_within) must use format like "7d", "30d"
|
|
196
|
+
- Rejects empty retention policies
|
|
197
|
+
- Detects unknown retention keys
|
|
198
|
+
- **Passbolt configuration**: Validates resource_id is non-empty string
|
|
199
|
+
- **Retention mode**: Must be "standard" or "per_file"
|
|
200
|
+
- **Compression values**: Must be one of: lz4, zstd, zlib, lzma, none
|
|
201
|
+
- **Encryption modes**: Must be valid Borg encryption mode
|
|
202
|
+
- **Repository structure**: Required fields (name, path, sources)
|
|
203
|
+
- **Source structure**: Required fields (name, paths), validates exclude arrays
|
|
204
|
+
- **Borg options**: Validates allow_relocated_repo and allow_unencrypted_repo
|
|
196
205
|
|
|
197
206
|
**Example validation output:**
|
|
198
207
|
|
|
@@ -211,6 +220,115 @@ Or with errors:
|
|
|
211
220
|
Configuration has errors that must be fixed.
|
|
212
221
|
```
|
|
213
222
|
|
|
223
|
+
## Logging
|
|
224
|
+
|
|
225
|
+
Ruborg v0.6.1 includes comprehensive logging to help you track backup operations, troubleshoot issues, and maintain audit trails. Logs are written to `~/.ruborg/logs/ruborg.log` by default, or to a custom location specified in your configuration.
|
|
226
|
+
|
|
227
|
+
### What Gets Logged
|
|
228
|
+
|
|
229
|
+
Ruborg logs operational information at various levels to help you monitor and debug backup operations:
|
|
230
|
+
|
|
231
|
+
#### Repository Operations
|
|
232
|
+
- Repository creation and initialization
|
|
233
|
+
- Repository path and encryption mode
|
|
234
|
+
- Per-file pruning operations (archive counts, file modification times)
|
|
235
|
+
- Archive deletion during pruning
|
|
236
|
+
|
|
237
|
+
#### Backup Operations
|
|
238
|
+
- Number of files found for backup (per-file mode)
|
|
239
|
+
- Individual file backup progress (per-file mode)
|
|
240
|
+
- Backup completion status
|
|
241
|
+
- Archive names (user-provided or auto-generated)
|
|
242
|
+
|
|
243
|
+
#### Restore Operations
|
|
244
|
+
- Archive extraction start (archive name, destination path)
|
|
245
|
+
- Specific paths being restored (if using `--path` option)
|
|
246
|
+
- Extraction completion status
|
|
247
|
+
|
|
248
|
+
#### Source File Deletion (when using `--remove-source`)
|
|
249
|
+
- Start of source file removal process
|
|
250
|
+
- Each file/directory being removed (with full resolved path)
|
|
251
|
+
- Warnings for non-existent or missing paths
|
|
252
|
+
- Errors when attempting to delete system directories (with path)
|
|
253
|
+
- Count of items successfully removed
|
|
254
|
+
|
|
255
|
+
#### Passbolt Integration
|
|
256
|
+
- Password retrieval start (includes Passbolt resource UUID)
|
|
257
|
+
- Password retrieval failures (includes resource UUID)
|
|
258
|
+
|
|
259
|
+
### What Is NOT Logged
|
|
260
|
+
|
|
261
|
+
To protect sensitive information, the following are **never logged**:
|
|
262
|
+
|
|
263
|
+
- ✅ **Passwords and passphrases** - Neither from command line nor from Passbolt
|
|
264
|
+
- ✅ **File contents** - Only file paths and metadata
|
|
265
|
+
- ✅ **Encryption keys** - Repository encryption passphrases are never written to logs
|
|
266
|
+
- ✅ **Passbolt passwords** - Only resource IDs (UUIDs) are logged, never the actual passwords retrieved
|
|
267
|
+
|
|
268
|
+
### Log Levels
|
|
269
|
+
|
|
270
|
+
- **INFO**: Normal operation events (backups, restores, deletions)
|
|
271
|
+
- **WARN**: Non-critical issues (missing paths, skipped operations)
|
|
272
|
+
- **ERROR**: Critical errors (system path deletion attempts, command failures)
|
|
273
|
+
- **DEBUG**: Detailed information for troubleshooting (requires DEBUG level configuration)
|
|
274
|
+
|
|
275
|
+
### Configuring Logging
|
|
276
|
+
|
|
277
|
+
```yaml
|
|
278
|
+
# Log to default location: ~/.ruborg/logs/ruborg.log
|
|
279
|
+
log_file: default
|
|
280
|
+
|
|
281
|
+
# OR custom log file path
|
|
282
|
+
log_file: /var/log/ruborg/backup.log
|
|
283
|
+
|
|
284
|
+
# OR disable file logging (stdout only)
|
|
285
|
+
log_file: stdout
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
You can also override the log file location using the `--log` command-line option:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
ruborg backup --repository documents --log /tmp/debug.log
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Log Security Considerations
|
|
295
|
+
|
|
296
|
+
- **File Paths**: Logs contain file and directory paths being backed up. Secure your log files with appropriate permissions (recommended: `chmod 600` or `640`)
|
|
297
|
+
- **Passbolt Resource IDs**: UUID identifiers for Passbolt resources are logged. These are safe to log as they are unguessable and don't expose credentials, but logs should still be protected
|
|
298
|
+
- **Archive Names**: User-provided or auto-generated archive names are logged for audit purposes
|
|
299
|
+
- **System Paths**: When `--remove-source` attempts to delete system directories, the full path is logged in error messages for security auditing
|
|
300
|
+
|
|
301
|
+
### Best Practices
|
|
302
|
+
|
|
303
|
+
1. **Secure Log Files**: Set restrictive permissions on log files
|
|
304
|
+
```bash
|
|
305
|
+
chmod 600 ~/.ruborg/logs/ruborg.log
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
2. **Log Rotation**: Configure log rotation to prevent logs from consuming excessive disk space
|
|
309
|
+
```bash
|
|
310
|
+
# Example logrotate configuration
|
|
311
|
+
/home/user/.ruborg/logs/ruborg.log {
|
|
312
|
+
weekly
|
|
313
|
+
rotate 4
|
|
314
|
+
compress
|
|
315
|
+
missingok
|
|
316
|
+
notifempty
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
3. **Monitoring**: Review logs regularly to detect:
|
|
321
|
+
- Failed backup operations
|
|
322
|
+
- Unauthorized deletion attempts
|
|
323
|
+
- Passbolt password retrieval failures
|
|
324
|
+
- Unexpected file paths
|
|
325
|
+
|
|
326
|
+
4. **Audit Trail**: Logs provide an audit trail for compliance purposes:
|
|
327
|
+
- What was backed up and when
|
|
328
|
+
- What was restored and where
|
|
329
|
+
- What was deleted (with `--remove-source`)
|
|
330
|
+
- Any errors or security-related events
|
|
331
|
+
|
|
214
332
|
## Usage
|
|
215
333
|
|
|
216
334
|
### Initialize a Repository
|
|
@@ -350,31 +468,6 @@ Borg version: 1.2.8
|
|
|
350
468
|
Please upgrade Borg or migrate the repository
|
|
351
469
|
```
|
|
352
470
|
|
|
353
|
-
## Logging
|
|
354
|
-
|
|
355
|
-
Ruborg automatically logs all operations with daily rotation. Log file location priority:
|
|
356
|
-
|
|
357
|
-
1. **CLI option** (highest priority): `--log /path/to/custom.log`
|
|
358
|
-
2. **Config file**: `log_file: /path/to/log.log`
|
|
359
|
-
3. **Default**: `~/.ruborg/logs/ruborg.log`
|
|
360
|
-
|
|
361
|
-
**Examples:**
|
|
362
|
-
|
|
363
|
-
```bash
|
|
364
|
-
# Use CLI option (overrides config)
|
|
365
|
-
ruborg backup --log /var/log/ruborg.log
|
|
366
|
-
|
|
367
|
-
# Or set in config file
|
|
368
|
-
log_file: /var/log/ruborg.log
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
**Logs include:**
|
|
372
|
-
- Operation start/completion timestamps
|
|
373
|
-
- Paths being backed up
|
|
374
|
-
- Archive names created
|
|
375
|
-
- Success and error messages
|
|
376
|
-
- Source file removal actions
|
|
377
|
-
|
|
378
471
|
## Passbolt Integration
|
|
379
472
|
|
|
380
473
|
Ruborg can retrieve encryption passphrases from Passbolt using the Passbolt CLI:
|
data/SECURITY.md
CHANGED
|
@@ -75,6 +75,72 @@ Ruborg implements several security measures to protect your backup operations:
|
|
|
75
75
|
- Prevents configuration errors that could lead to unintended data loss
|
|
76
76
|
- **CWE-843 Mitigation**: Protects against type confusion vulnerabilities
|
|
77
77
|
|
|
78
|
+
### 14. Logging Security (v0.6.1+)
|
|
79
|
+
- **Comprehensive logging** of backup operations for audit trails and troubleshooting
|
|
80
|
+
- **Sensitive data protection**: Passwords and passphrases are NEVER logged
|
|
81
|
+
- **Safe operational logging**: File paths, archive names, and operation status are logged
|
|
82
|
+
- **Passbolt resource IDs** (UUIDs) are logged but actual passwords are not
|
|
83
|
+
- **System path protection**: Failed deletion attempts are logged with full paths for security auditing
|
|
84
|
+
- **Log level support**: INFO, WARN, ERROR, DEBUG levels for appropriate detail
|
|
85
|
+
|
|
86
|
+
#### What Is Logged (Safe)
|
|
87
|
+
- Repository creation and initialization events
|
|
88
|
+
- Backup operation start/completion with file counts
|
|
89
|
+
- Individual file paths being backed up (per-file mode)
|
|
90
|
+
- Archive names (user-provided or auto-generated)
|
|
91
|
+
- Restore operations with destination paths
|
|
92
|
+
- Source file deletion events (when using `--remove-source`)
|
|
93
|
+
- Passbolt resource IDs (UUIDs) for password retrieval attempts
|
|
94
|
+
- System path deletion refusals with full path (security audit)
|
|
95
|
+
- Pruning operations with archive counts
|
|
96
|
+
|
|
97
|
+
#### What Is NEVER Logged (Protected)
|
|
98
|
+
- ✅ **Passwords and passphrases** - Neither from CLI nor Passbolt
|
|
99
|
+
- ✅ **Encryption keys** - Repository encryption keys never written to logs
|
|
100
|
+
- ✅ **Passbolt passwords** - Only resource UUIDs logged, not actual retrieved passwords
|
|
101
|
+
- ✅ **File contents** - Only paths and metadata, never file contents
|
|
102
|
+
- ✅ **Environment variables** with sensitive data
|
|
103
|
+
|
|
104
|
+
#### Log Security Recommendations
|
|
105
|
+
1. **Protect log files** with restrictive permissions:
|
|
106
|
+
```bash
|
|
107
|
+
chmod 600 ~/.ruborg/logs/ruborg.log
|
|
108
|
+
# Or for shared access with backup group:
|
|
109
|
+
chmod 640 ~/.ruborg/logs/ruborg.log
|
|
110
|
+
chown user:backup ~/.ruborg/logs/ruborg.log
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
2. **Configure log rotation** to prevent log files from growing indefinitely:
|
|
114
|
+
```bash
|
|
115
|
+
# /etc/logrotate.d/ruborg
|
|
116
|
+
/home/user/.ruborg/logs/ruborg.log {
|
|
117
|
+
weekly
|
|
118
|
+
rotate 4
|
|
119
|
+
compress
|
|
120
|
+
missingok
|
|
121
|
+
notifempty
|
|
122
|
+
create 0600 user user
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
3. **Review logs regularly** for:
|
|
127
|
+
- Failed backup or restore operations
|
|
128
|
+
- Unauthorized `--remove-source` attempts
|
|
129
|
+
- Passbolt password retrieval failures
|
|
130
|
+
- System path deletion attempts (potential security issues)
|
|
131
|
+
- Unexpected file paths being backed up
|
|
132
|
+
|
|
133
|
+
4. **Secure log storage locations**:
|
|
134
|
+
- Use absolute paths in `log_file` configuration
|
|
135
|
+
- Avoid logging to world-readable directories
|
|
136
|
+
- Consider logging to `/var/log/ruborg/` with proper permissions
|
|
137
|
+
|
|
138
|
+
5. **Passbolt Resource IDs in Logs**:
|
|
139
|
+
- Resource IDs (UUIDs) are logged for operational debugging
|
|
140
|
+
- These are identifiers, not credentials - safe to log
|
|
141
|
+
- They cannot be used to access Passbolt without proper authentication
|
|
142
|
+
- Still, protect logs as they reveal which Passbolt resources are used
|
|
143
|
+
|
|
78
144
|
## Security Best Practices
|
|
79
145
|
|
|
80
146
|
### When Using `--remove-source`
|
|
@@ -163,6 +229,16 @@ We will respond within 48 hours and work with you to address the issue.
|
|
|
163
229
|
|
|
164
230
|
## Security Audit History
|
|
165
231
|
|
|
232
|
+
- **v0.6.1** (2025-10-08): Enhanced logging with sensitive data protection
|
|
233
|
+
- **NEW FEATURE**: Comprehensive logging for backup operations, restoration, and deletion
|
|
234
|
+
- Passwords and passphrases are NEVER logged (neither CLI nor Passbolt passwords)
|
|
235
|
+
- Passbolt resource IDs (UUIDs) logged for debugging - identifiers only, not credentials
|
|
236
|
+
- File paths and archive names logged for audit trails
|
|
237
|
+
- System path deletion attempts logged with full paths for security monitoring
|
|
238
|
+
- Log levels: INFO, WARN, ERROR, DEBUG for appropriate detail
|
|
239
|
+
- Documentation added for logging security best practices
|
|
240
|
+
- Enhanced configuration validation with unknown key detection across all levels
|
|
241
|
+
|
|
166
242
|
- **v0.6.0** (2025-10-08): Configuration validation and type confusion protection
|
|
167
243
|
- **SECURITY FIX**: Implemented strict boolean type checking for `allow_remove_source`
|
|
168
244
|
- Prevents type confusion attacks (CWE-843) where string values bypass safety checks
|
data/exe/ruborg
CHANGED
data/lib/ruborg/backup.rb
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
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)
|
|
6
|
+
def initialize(repository, config:, retention_mode: "standard", repo_name: nil, logger: nil)
|
|
7
7
|
@repository = repository
|
|
8
8
|
@config = config
|
|
9
9
|
@retention_mode = retention_mode
|
|
10
10
|
@repo_name = repo_name
|
|
11
|
+
@logger = logger
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def create(name: nil, remove_source: false)
|
|
@@ -37,19 +38,25 @@ module Ruborg
|
|
|
37
38
|
|
|
38
39
|
raise BorgError, "No files found to backup" if files_to_backup.empty?
|
|
39
40
|
|
|
41
|
+
@logger&.info("Per-file mode: Found #{files_to_backup.size} file(s) to backup")
|
|
42
|
+
|
|
40
43
|
timestamp = Time.now.strftime("%Y-%m-%d_%H-%M-%S")
|
|
41
44
|
|
|
42
|
-
files_to_backup.
|
|
45
|
+
files_to_backup.each_with_index do |file_path, index|
|
|
43
46
|
# Generate hash-based archive name
|
|
44
47
|
path_hash = generate_path_hash(file_path)
|
|
45
48
|
archive_name = name_prefix || "#{@repo_name}-#{path_hash}-#{timestamp}"
|
|
46
49
|
|
|
50
|
+
@logger&.info("Backing up file #{index + 1}/#{files_to_backup.size}: #{file_path}")
|
|
51
|
+
|
|
47
52
|
# Create archive for single file with original path as comment
|
|
48
53
|
cmd = build_per_file_create_command(archive_name, file_path)
|
|
49
54
|
|
|
50
55
|
execute_borg_command(cmd)
|
|
51
56
|
end
|
|
52
57
|
|
|
58
|
+
@logger&.info("Per-file backup completed: #{files_to_backup.size} file(s) backed up")
|
|
59
|
+
|
|
53
60
|
# NOTE: remove_source handled per file after successful backup
|
|
54
61
|
remove_source_files if remove_source
|
|
55
62
|
end
|
|
@@ -108,6 +115,9 @@ module Ruborg
|
|
|
108
115
|
def extract(archive_name, destination: ".", path: nil)
|
|
109
116
|
raise BorgError, "Repository does not exist" unless @repository.exists?
|
|
110
117
|
|
|
118
|
+
extract_target = path ? "#{path} from #{archive_name}" : archive_name
|
|
119
|
+
@logger&.info("Extracting #{extract_target} to #{destination}")
|
|
120
|
+
|
|
111
121
|
cmd = [@repository.borg_path, "extract", "#{@repository.path}::#{archive_name}"]
|
|
112
122
|
cmd << path if path
|
|
113
123
|
|
|
@@ -125,6 +135,8 @@ module Ruborg
|
|
|
125
135
|
execute_borg_command(cmd)
|
|
126
136
|
end
|
|
127
137
|
end
|
|
138
|
+
|
|
139
|
+
@logger&.info("Extraction completed successfully")
|
|
128
140
|
end
|
|
129
141
|
|
|
130
142
|
def list_archives
|
|
@@ -132,8 +144,10 @@ module Ruborg
|
|
|
132
144
|
end
|
|
133
145
|
|
|
134
146
|
def delete(archive_name)
|
|
147
|
+
@logger&.info("Deleting archive: #{archive_name}")
|
|
135
148
|
cmd = [@repository.borg_path, "delete", "#{@repository.path}::#{archive_name}"]
|
|
136
149
|
execute_borg_command(cmd)
|
|
150
|
+
@logger&.info("Archive deleted successfully: #{archive_name}")
|
|
137
151
|
end
|
|
138
152
|
|
|
139
153
|
private
|
|
@@ -171,29 +185,45 @@ module Ruborg
|
|
|
171
185
|
def remove_source_files
|
|
172
186
|
require "fileutils"
|
|
173
187
|
|
|
188
|
+
@logger&.info("Removing source files after successful backup")
|
|
189
|
+
|
|
190
|
+
removed_count = 0
|
|
191
|
+
|
|
174
192
|
@config.backup_paths.each do |path|
|
|
175
193
|
# Resolve symlinks and validate path
|
|
176
194
|
begin
|
|
177
195
|
real_path = File.realpath(path)
|
|
178
196
|
rescue Errno::ENOENT
|
|
179
197
|
# Path doesn't exist, skip
|
|
198
|
+
@logger&.warn("Source path does not exist, skipping: #{path}")
|
|
180
199
|
next
|
|
181
200
|
end
|
|
182
201
|
|
|
183
202
|
# Security check: ensure path hasn't been tampered with
|
|
184
|
-
|
|
203
|
+
unless File.exist?(real_path)
|
|
204
|
+
@logger&.warn("Source path no longer exists, skipping: #{real_path}")
|
|
205
|
+
next
|
|
206
|
+
end
|
|
185
207
|
|
|
186
208
|
# Additional safety: don't delete root or system directories
|
|
187
209
|
if real_path == "/" || real_path.start_with?("/bin", "/sbin", "/usr", "/etc", "/sys", "/proc")
|
|
210
|
+
@logger&.error("Refusing to delete system path: #{real_path}")
|
|
188
211
|
raise BorgError, "Refusing to delete system path: #{real_path}"
|
|
189
212
|
end
|
|
190
213
|
|
|
214
|
+
file_type = File.directory?(real_path) ? "directory" : "file"
|
|
215
|
+
@logger&.info("Removing #{file_type}: #{real_path}")
|
|
216
|
+
|
|
191
217
|
if File.directory?(real_path)
|
|
192
218
|
FileUtils.rm_rf(real_path, secure: true)
|
|
193
219
|
elsif File.file?(real_path)
|
|
194
220
|
FileUtils.rm(real_path)
|
|
195
221
|
end
|
|
222
|
+
|
|
223
|
+
removed_count += 1
|
|
196
224
|
end
|
|
225
|
+
|
|
226
|
+
@logger&.info("Source file removal completed: #{removed_count} item(s) removed")
|
|
197
227
|
end
|
|
198
228
|
|
|
199
229
|
def validate_destination_path(destination)
|
data/lib/ruborg/cli.rb
CHANGED
|
@@ -38,13 +38,13 @@ module Ruborg
|
|
|
38
38
|
def init(repository_path)
|
|
39
39
|
@logger.info("Initializing repository at #{repository_path}")
|
|
40
40
|
passphrase = get_passphrase(options[:passphrase], options[:passbolt_id])
|
|
41
|
-
repo = Repository.new(repository_path, passphrase: passphrase)
|
|
41
|
+
repo = Repository.new(repository_path, passphrase: passphrase, logger: @logger)
|
|
42
42
|
repo.create
|
|
43
43
|
@logger.info("Repository successfully initialized at #{repository_path}")
|
|
44
44
|
puts "Repository initialized at #{repository_path}"
|
|
45
45
|
rescue Error => e
|
|
46
46
|
@logger.error("Failed to initialize repository: #{e.message}")
|
|
47
|
-
|
|
47
|
+
raise
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
desc "backup", "Create a backup using configuration file"
|
|
@@ -54,11 +54,10 @@ module Ruborg
|
|
|
54
54
|
def backup
|
|
55
55
|
@logger.info("Starting backup operation with config: #{options[:config]}")
|
|
56
56
|
config = Config.new(options[:config])
|
|
57
|
-
validate_hostname(config.global_settings)
|
|
58
57
|
backup_repositories(config)
|
|
59
58
|
rescue Error => e
|
|
60
59
|
@logger.error("Backup failed: #{e.message}")
|
|
61
|
-
|
|
60
|
+
raise
|
|
62
61
|
end
|
|
63
62
|
|
|
64
63
|
desc "list", "List all archives in the repository"
|
|
@@ -78,7 +77,7 @@ module Ruborg
|
|
|
78
77
|
borg_opts = merged_config["borg_options"] || {}
|
|
79
78
|
borg_path = merged_config["borg_path"]
|
|
80
79
|
|
|
81
|
-
repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts, borg_path: borg_path)
|
|
80
|
+
repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts, borg_path: borg_path, logger: @logger)
|
|
82
81
|
|
|
83
82
|
# Auto-initialize repository if configured
|
|
84
83
|
# Use strict boolean checking: only true enables, everything else disables
|
|
@@ -94,7 +93,7 @@ module Ruborg
|
|
|
94
93
|
@logger.info("Successfully listed archives")
|
|
95
94
|
rescue Error => e
|
|
96
95
|
@logger.error("Failed to list archives: #{e.message}")
|
|
97
|
-
|
|
96
|
+
raise
|
|
98
97
|
end
|
|
99
98
|
|
|
100
99
|
desc "restore ARCHIVE", "Restore files from an archive"
|
|
@@ -117,11 +116,11 @@ module Ruborg
|
|
|
117
116
|
borg_opts = merged_config["borg_options"] || {}
|
|
118
117
|
borg_path = merged_config["borg_path"]
|
|
119
118
|
|
|
120
|
-
repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts, borg_path: borg_path)
|
|
119
|
+
repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts, borg_path: borg_path, logger: @logger)
|
|
121
120
|
|
|
122
121
|
# Create backup config wrapper for compatibility
|
|
123
122
|
backup_config = BackupConfig.new(repo_config, merged_config)
|
|
124
|
-
backup = Backup.new(repo, config: backup_config)
|
|
123
|
+
backup = Backup.new(repo, config: backup_config, logger: @logger)
|
|
125
124
|
|
|
126
125
|
backup.extract(archive_name, destination: options[:destination], path: options[:path])
|
|
127
126
|
@logger.info("Successfully restored #{restore_target} to #{options[:destination]}")
|
|
@@ -133,7 +132,7 @@ module Ruborg
|
|
|
133
132
|
end
|
|
134
133
|
rescue Error => e
|
|
135
134
|
@logger.error("Failed to restore archive: #{e.message}")
|
|
136
|
-
|
|
135
|
+
raise
|
|
137
136
|
end
|
|
138
137
|
|
|
139
138
|
desc "info", "Show repository information"
|
|
@@ -156,7 +155,7 @@ module Ruborg
|
|
|
156
155
|
borg_opts = merged_config["borg_options"] || {}
|
|
157
156
|
borg_path = merged_config["borg_path"]
|
|
158
157
|
|
|
159
|
-
repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts, borg_path: borg_path)
|
|
158
|
+
repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts, borg_path: borg_path, logger: @logger)
|
|
160
159
|
|
|
161
160
|
# Auto-initialize repository if configured
|
|
162
161
|
# Use strict boolean checking: only true enables, everything else disables
|
|
@@ -172,7 +171,79 @@ module Ruborg
|
|
|
172
171
|
@logger.info("Successfully retrieved repository information")
|
|
173
172
|
rescue Error => e
|
|
174
173
|
@logger.error("Failed to get repository info: #{e.message}")
|
|
175
|
-
|
|
174
|
+
raise
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
desc "validate", "Validate configuration file for errors and type issues"
|
|
178
|
+
def validate_config
|
|
179
|
+
@logger.info("Validating configuration file: #{options[:config]}")
|
|
180
|
+
config = Config.new(options[:config])
|
|
181
|
+
|
|
182
|
+
puts "\n═══════════════════════════════════════════════════════════════"
|
|
183
|
+
puts " CONFIGURATION VALIDATION"
|
|
184
|
+
puts "═══════════════════════════════════════════════════════════════\n\n"
|
|
185
|
+
|
|
186
|
+
errors = []
|
|
187
|
+
warnings = []
|
|
188
|
+
|
|
189
|
+
# Validate global boolean settings
|
|
190
|
+
global_settings = config.global_settings
|
|
191
|
+
errors.concat(validate_boolean_setting(global_settings, "auto_init", "global"))
|
|
192
|
+
errors.concat(validate_boolean_setting(global_settings, "auto_prune", "global"))
|
|
193
|
+
errors.concat(validate_boolean_setting(global_settings, "allow_remove_source", "global"))
|
|
194
|
+
|
|
195
|
+
# Validate borg_options booleans
|
|
196
|
+
if global_settings["borg_options"]
|
|
197
|
+
warnings.concat(validate_borg_option(global_settings["borg_options"], "allow_relocated_repo", "global"))
|
|
198
|
+
warnings.concat(validate_borg_option(global_settings["borg_options"], "allow_unencrypted_repo", "global"))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Validate per-repository settings
|
|
202
|
+
config.repositories.each do |repo|
|
|
203
|
+
repo_name = repo["name"]
|
|
204
|
+
errors.concat(validate_boolean_setting(repo, "auto_init", repo_name))
|
|
205
|
+
errors.concat(validate_boolean_setting(repo, "auto_prune", repo_name))
|
|
206
|
+
errors.concat(validate_boolean_setting(repo, "allow_remove_source", repo_name))
|
|
207
|
+
|
|
208
|
+
if repo["borg_options"]
|
|
209
|
+
warnings.concat(validate_borg_option(repo["borg_options"], "allow_relocated_repo", repo_name))
|
|
210
|
+
warnings.concat(validate_borg_option(repo["borg_options"], "allow_unencrypted_repo", repo_name))
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Display results
|
|
215
|
+
if errors.empty? && warnings.empty?
|
|
216
|
+
puts "✓ Configuration is valid"
|
|
217
|
+
puts " No type errors or warnings found\n\n"
|
|
218
|
+
else
|
|
219
|
+
unless errors.empty?
|
|
220
|
+
puts "❌ ERRORS FOUND (#{errors.size}):"
|
|
221
|
+
errors.each do |error|
|
|
222
|
+
puts " - #{error}"
|
|
223
|
+
end
|
|
224
|
+
puts ""
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
unless warnings.empty?
|
|
228
|
+
puts "⚠️ WARNINGS (#{warnings.size}):"
|
|
229
|
+
warnings.each do |warning|
|
|
230
|
+
puts " - #{warning}"
|
|
231
|
+
end
|
|
232
|
+
puts ""
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
if errors.any?
|
|
236
|
+
puts "Configuration has errors that must be fixed.\n\n"
|
|
237
|
+
raise ConfigError, "Configuration validation failed"
|
|
238
|
+
else
|
|
239
|
+
puts "Configuration is valid but has warnings.\n\n"
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
@logger.info("Configuration validation completed")
|
|
244
|
+
rescue Error => e
|
|
245
|
+
@logger.error("Validation failed: #{e.message}")
|
|
246
|
+
raise
|
|
176
247
|
end
|
|
177
248
|
|
|
178
249
|
desc "validate", "Validate configuration file for errors and type issues"
|
|
@@ -276,7 +347,7 @@ module Ruborg
|
|
|
276
347
|
end
|
|
277
348
|
rescue Error => e
|
|
278
349
|
@logger.error("Check failed: #{e.message}")
|
|
279
|
-
|
|
350
|
+
raise
|
|
280
351
|
end
|
|
281
352
|
|
|
282
353
|
private
|
|
@@ -292,7 +363,7 @@ module Ruborg
|
|
|
292
363
|
borg_opts = merged_config["borg_options"] || {}
|
|
293
364
|
borg_path = merged_config["borg_path"]
|
|
294
365
|
|
|
295
|
-
repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts, borg_path: borg_path)
|
|
366
|
+
repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts, borg_path: borg_path, logger: @logger)
|
|
296
367
|
|
|
297
368
|
unless repo.exists?
|
|
298
369
|
puts " ✗ Repository does not exist at #{repo_config["path"]}"
|
|
@@ -411,16 +482,11 @@ module Ruborg
|
|
|
411
482
|
|
|
412
483
|
def get_passphrase(passphrase, passbolt_id)
|
|
413
484
|
return passphrase if passphrase
|
|
414
|
-
return Passbolt.new(resource_id: passbolt_id).get_password if passbolt_id
|
|
485
|
+
return Passbolt.new(resource_id: passbolt_id, logger: @logger).get_password if passbolt_id
|
|
415
486
|
|
|
416
487
|
nil
|
|
417
488
|
end
|
|
418
489
|
|
|
419
|
-
def error_exit(error)
|
|
420
|
-
puts "Error: #{error.message}"
|
|
421
|
-
exit 1
|
|
422
|
-
end
|
|
423
|
-
|
|
424
490
|
def validate_log_path(log_path)
|
|
425
491
|
# Expand to absolute path
|
|
426
492
|
normalized_path = File.expand_path(log_path)
|
|
@@ -477,7 +543,7 @@ module Ruborg
|
|
|
477
543
|
passphrase = fetch_passphrase_for_repo(merged_config)
|
|
478
544
|
borg_opts = merged_config["borg_options"] || {}
|
|
479
545
|
borg_path = merged_config["borg_path"]
|
|
480
|
-
repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts, borg_path: borg_path)
|
|
546
|
+
repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts, borg_path: borg_path, logger: @logger)
|
|
481
547
|
|
|
482
548
|
# Auto-initialize if configured
|
|
483
549
|
# Use strict boolean checking: only true enables, everything else disables
|
|
@@ -505,7 +571,7 @@ module Ruborg
|
|
|
505
571
|
|
|
506
572
|
# Create backup config wrapper
|
|
507
573
|
backup_config = BackupConfig.new(repo_config, merged_config)
|
|
508
|
-
backup = Backup.new(repo, config: backup_config, retention_mode: retention_mode, repo_name: repo_name)
|
|
574
|
+
backup = Backup.new(repo, config: backup_config, retention_mode: retention_mode, repo_name: repo_name, logger: @logger)
|
|
509
575
|
|
|
510
576
|
archive_name = options[:name] ? sanitize_archive_name(options[:name]) : nil
|
|
511
577
|
@logger.info("Creating archive#{"s" if retention_mode == "per_file"}: #{archive_name || "auto-generated"}")
|
|
@@ -543,7 +609,7 @@ module Ruborg
|
|
|
543
609
|
passbolt_config = repo_config["passbolt"]
|
|
544
610
|
return nil if passbolt_config.nil? || passbolt_config.empty?
|
|
545
611
|
|
|
546
|
-
Passbolt.new(resource_id: passbolt_config["resource_id"]).get_password
|
|
612
|
+
Passbolt.new(resource_id: passbolt_config["resource_id"], logger: @logger).get_password
|
|
547
613
|
end
|
|
548
614
|
|
|
549
615
|
def sanitize_archive_name(name)
|
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")
|
|
44
|
+
"auto_prune", "hostname", "allow_remove_source", "borg_path")
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
private
|
|
@@ -49,6 +49,29 @@ module Ruborg
|
|
|
49
49
|
VALID_COMPRESSION = %w[lz4 zstd zlib lzma none].freeze
|
|
50
50
|
VALID_ENCRYPTION = %w[repokey keyfile none authenticated repokey-blake2
|
|
51
51
|
keyfile-blake2 authenticated-blake2].freeze
|
|
52
|
+
VALID_RETENTION_MODES = %w[standard per_file].freeze
|
|
53
|
+
|
|
54
|
+
# Valid configuration keys at each level
|
|
55
|
+
VALID_GLOBAL_KEYS = %w[
|
|
56
|
+
hostname compression encryption auto_init auto_prune allow_remove_source
|
|
57
|
+
log_file borg_path passbolt borg_options retention repositories
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
60
|
+
VALID_REPOSITORY_KEYS = %w[
|
|
61
|
+
name description path hostname retention_mode passbolt retention sources
|
|
62
|
+
compression encryption auto_init auto_prune borg_options allow_remove_source
|
|
63
|
+
].freeze
|
|
64
|
+
|
|
65
|
+
VALID_SOURCE_KEYS = %w[name paths exclude].freeze
|
|
66
|
+
|
|
67
|
+
VALID_RETENTION_KEYS = %w[
|
|
68
|
+
keep_hourly keep_daily keep_weekly keep_monthly keep_yearly
|
|
69
|
+
keep_within keep_last keep_files_modified_within
|
|
70
|
+
].freeze
|
|
71
|
+
|
|
72
|
+
VALID_PASSBOLT_KEYS = %w[resource_id].freeze
|
|
73
|
+
|
|
74
|
+
VALID_BORG_OPTIONS_KEYS = %w[allow_relocated_repo allow_unencrypted_repo].freeze
|
|
52
75
|
|
|
53
76
|
def validate_format
|
|
54
77
|
return if @data.key?("repositories")
|
|
@@ -88,19 +111,25 @@ module Ruborg
|
|
|
88
111
|
end
|
|
89
112
|
|
|
90
113
|
# Validate YAML schema for type correctness
|
|
114
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
91
115
|
def validate_schema
|
|
92
116
|
errors = []
|
|
93
117
|
|
|
118
|
+
# Validate unknown keys
|
|
119
|
+
errors.concat(validate_unknown_keys(@data, VALID_GLOBAL_KEYS, "global"))
|
|
120
|
+
|
|
94
121
|
# Validate global boolean settings
|
|
95
122
|
errors.concat(validate_boolean_config(@data, "auto_init", "global"))
|
|
96
123
|
errors.concat(validate_boolean_config(@data, "auto_prune", "global"))
|
|
97
124
|
errors.concat(validate_boolean_config(@data, "allow_remove_source", "global"))
|
|
98
125
|
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
126
|
+
# Note: borg_options are validated as warnings in CLI validate command, not as errors here
|
|
127
|
+
|
|
128
|
+
# Validate global passbolt
|
|
129
|
+
errors.concat(validate_passbolt_config(@data["passbolt"], "global")) if @data["passbolt"]
|
|
130
|
+
|
|
131
|
+
# Validate global retention
|
|
132
|
+
errors.concat(validate_retention_policy(@data["retention"], "global")) if @data["retention"]
|
|
104
133
|
|
|
105
134
|
# Validate compression and encryption if present
|
|
106
135
|
if @data["compression"] && !VALID_COMPRESSION.include?(@data["compression"])
|
|
@@ -112,20 +141,29 @@ module Ruborg
|
|
|
112
141
|
end
|
|
113
142
|
|
|
114
143
|
# Validate per-repository settings
|
|
144
|
+
# rubocop:disable Metrics/BlockLength
|
|
115
145
|
repositories.each do |repo|
|
|
116
146
|
repo_name = repo["name"] || "unnamed"
|
|
117
147
|
|
|
148
|
+
# Validate unknown keys in repository
|
|
149
|
+
errors.concat(validate_unknown_keys(repo, VALID_REPOSITORY_KEYS, repo_name))
|
|
150
|
+
|
|
118
151
|
errors.concat(validate_boolean_config(repo, "auto_init", repo_name))
|
|
119
152
|
errors.concat(validate_boolean_config(repo, "auto_prune", repo_name))
|
|
120
153
|
errors.concat(validate_boolean_config(repo, "allow_remove_source", repo_name))
|
|
121
154
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
"#{repo_name}/borg_options"))
|
|
155
|
+
# Validate retention_mode
|
|
156
|
+
if repo["retention_mode"] && !VALID_RETENTION_MODES.include?(repo["retention_mode"])
|
|
157
|
+
errors << "#{repo_name}/retention_mode: invalid value '#{repo["retention_mode"]}'. " \
|
|
158
|
+
"Must be one of: #{VALID_RETENTION_MODES.join(", ")}"
|
|
127
159
|
end
|
|
128
160
|
|
|
161
|
+
# Note: borg_options are validated as warnings in CLI validate command, not as errors here
|
|
162
|
+
|
|
163
|
+
errors.concat(validate_passbolt_config(repo["passbolt"], repo_name)) if repo["passbolt"]
|
|
164
|
+
|
|
165
|
+
errors.concat(validate_retention_policy(repo["retention"], repo_name)) if repo["retention"]
|
|
166
|
+
|
|
129
167
|
# Validate compression and encryption if present
|
|
130
168
|
if repo["compression"] && !VALID_COMPRESSION.include?(repo["compression"])
|
|
131
169
|
errors << "#{repo_name}/compression: invalid value '#{repo["compression"]}'"
|
|
@@ -138,7 +176,24 @@ module Ruborg
|
|
|
138
176
|
# Validate repository structure
|
|
139
177
|
errors << "#{repo_name}: missing 'path' key" unless repo["path"]
|
|
140
178
|
errors << "#{repo_name}: 'sources' must be an array" if repo["sources"] && !repo["sources"].is_a?(Array)
|
|
179
|
+
|
|
180
|
+
# Validate sources
|
|
181
|
+
next unless repo["sources"].is_a?(Array)
|
|
182
|
+
|
|
183
|
+
repo["sources"].each_with_index do |source, idx|
|
|
184
|
+
source_context = "#{repo_name}/sources[#{idx}]"
|
|
185
|
+
source_name = source["name"] || "unnamed"
|
|
186
|
+
|
|
187
|
+
errors.concat(validate_unknown_keys(source, VALID_SOURCE_KEYS, "#{repo_name}/sources/#{source_name}"))
|
|
188
|
+
errors << "#{source_context}: missing 'name' key" unless source["name"]
|
|
189
|
+
errors << "#{source_context}: missing 'paths' key" unless source["paths"]
|
|
190
|
+
errors << "#{source_context}: 'paths' must be an array" if source["paths"] && !source["paths"].is_a?(Array)
|
|
191
|
+
if source["exclude"] && !source["exclude"].is_a?(Array)
|
|
192
|
+
errors << "#{source_context}: 'exclude' must be an array"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
141
195
|
end
|
|
196
|
+
# rubocop:enable Metrics/BlockLength
|
|
142
197
|
|
|
143
198
|
return if errors.empty?
|
|
144
199
|
|
|
@@ -146,6 +201,7 @@ module Ruborg
|
|
|
146
201
|
"Configuration validation failed:\n - #{errors.join("\n - ")}\n\n" \
|
|
147
202
|
"Run 'ruborg validate' for detailed validation information."
|
|
148
203
|
end
|
|
204
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
149
205
|
|
|
150
206
|
def validate_boolean_config(config, key, context)
|
|
151
207
|
errors = []
|
|
@@ -159,5 +215,99 @@ module Ruborg
|
|
|
159
215
|
|
|
160
216
|
errors
|
|
161
217
|
end
|
|
218
|
+
|
|
219
|
+
def validate_unknown_keys(config, valid_keys, context)
|
|
220
|
+
errors = []
|
|
221
|
+
return errors unless config.is_a?(Hash)
|
|
222
|
+
|
|
223
|
+
unknown_keys = config.keys - valid_keys
|
|
224
|
+
unknown_keys.each do |key|
|
|
225
|
+
errors << "#{context}: unknown configuration key '#{key}'"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
errors
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def validate_borg_options(borg_options, context)
|
|
232
|
+
errors = []
|
|
233
|
+
|
|
234
|
+
unless borg_options.is_a?(Hash)
|
|
235
|
+
errors << "#{context}/borg_options: must be a hash"
|
|
236
|
+
return errors
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
errors.concat(validate_unknown_keys(borg_options, VALID_BORG_OPTIONS_KEYS, "#{context}/borg_options"))
|
|
240
|
+
errors.concat(validate_boolean_config(borg_options, "allow_relocated_repo", "#{context}/borg_options"))
|
|
241
|
+
errors.concat(validate_boolean_config(borg_options, "allow_unencrypted_repo", "#{context}/borg_options"))
|
|
242
|
+
|
|
243
|
+
errors
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def validate_passbolt_config(passbolt, context)
|
|
247
|
+
errors = []
|
|
248
|
+
|
|
249
|
+
unless passbolt.is_a?(Hash)
|
|
250
|
+
errors << "#{context}/passbolt: must be a hash"
|
|
251
|
+
return errors
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
errors.concat(validate_unknown_keys(passbolt, VALID_PASSBOLT_KEYS, "#{context}/passbolt"))
|
|
255
|
+
|
|
256
|
+
errors << "#{context}/passbolt: missing required 'resource_id' key" unless passbolt["resource_id"]
|
|
257
|
+
|
|
258
|
+
if passbolt["resource_id"] && !passbolt["resource_id"].is_a?(String)
|
|
259
|
+
errors << "#{context}/passbolt/resource_id: must be a string"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
if passbolt["resource_id"].is_a?(String) && passbolt["resource_id"].strip.empty?
|
|
263
|
+
errors << "#{context}/passbolt/resource_id: cannot be empty"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
errors
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
270
|
+
def validate_retention_policy(retention, context)
|
|
271
|
+
errors = []
|
|
272
|
+
|
|
273
|
+
unless retention.is_a?(Hash)
|
|
274
|
+
errors << "#{context}/retention: must be a hash"
|
|
275
|
+
return errors
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
errors.concat(validate_unknown_keys(retention, VALID_RETENTION_KEYS, "#{context}/retention"))
|
|
279
|
+
|
|
280
|
+
# Validate integer retention values
|
|
281
|
+
%w[keep_hourly keep_daily keep_weekly keep_monthly keep_yearly keep_last].each do |key|
|
|
282
|
+
next unless retention[key]
|
|
283
|
+
|
|
284
|
+
unless retention[key].is_a?(Integer) && retention[key] >= 0
|
|
285
|
+
errors << "#{context}/retention/#{key}: must be a non-negative integer, " \
|
|
286
|
+
"got #{retention[key].class}: #{retention[key].inspect}"
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Validate time-based retention values (strings)
|
|
291
|
+
%w[keep_within keep_files_modified_within].each do |key|
|
|
292
|
+
next unless retention[key]
|
|
293
|
+
|
|
294
|
+
unless retention[key].is_a?(String)
|
|
295
|
+
errors << "#{context}/retention/#{key}: must be a string (e.g., '7d', '30d'), " \
|
|
296
|
+
"got #{retention[key].class}: #{retention[key].inspect}"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Validate time format (e.g., "7d", "30d", "2w", "3m", "1y")
|
|
300
|
+
if retention[key].is_a?(String) && !retention[key].match?(/^\d+[hdwmy]$/)
|
|
301
|
+
errors << "#{context}/retention/#{key}: invalid time format '#{retention[key]}'. " \
|
|
302
|
+
"Must be a number followed by h/d/w/m/y (e.g., '7d', '30d')"
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Warn if retention policy is empty
|
|
307
|
+
errors << "#{context}/retention: retention policy is empty" if retention.empty?
|
|
308
|
+
|
|
309
|
+
errors
|
|
310
|
+
end
|
|
311
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
162
312
|
end
|
|
163
313
|
end
|
data/lib/ruborg/passbolt.rb
CHANGED
|
@@ -5,19 +5,26 @@ require "json"
|
|
|
5
5
|
module Ruborg
|
|
6
6
|
# Passbolt CLI integration for password management
|
|
7
7
|
class Passbolt
|
|
8
|
-
def initialize(resource_id: nil)
|
|
8
|
+
def initialize(resource_id: nil, logger: nil)
|
|
9
9
|
@resource_id = resource_id
|
|
10
|
+
@logger = logger
|
|
10
11
|
check_passbolt_cli
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def get_password
|
|
14
15
|
raise PassboltError, "Resource ID not configured" unless @resource_id
|
|
15
16
|
|
|
17
|
+
@logger&.info("Retrieving password from Passbolt (resource_id: #{@resource_id})")
|
|
18
|
+
|
|
16
19
|
cmd = ["passbolt", "get", "resource", @resource_id, "--json"]
|
|
17
20
|
output, status = execute_command(cmd)
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
unless status
|
|
23
|
+
@logger&.error("Failed to retrieve password from Passbolt for resource #{@resource_id}")
|
|
24
|
+
raise PassboltError, "Failed to retrieve password from Passbolt"
|
|
25
|
+
end
|
|
20
26
|
|
|
27
|
+
@logger&.info("Successfully retrieved password from Passbolt")
|
|
21
28
|
parse_password(output)
|
|
22
29
|
end
|
|
23
30
|
|
data/lib/ruborg/repository.rb
CHANGED
|
@@ -6,11 +6,12 @@ module Ruborg
|
|
|
6
6
|
class Repository
|
|
7
7
|
attr_reader :path, :borg_path
|
|
8
8
|
|
|
9
|
-
def initialize(path, passphrase: nil, borg_options: {}, borg_path: nil)
|
|
9
|
+
def initialize(path, passphrase: nil, borg_options: {}, borg_path: nil, logger: nil)
|
|
10
10
|
@path = validate_repo_path(path)
|
|
11
11
|
@passphrase = passphrase
|
|
12
12
|
@borg_options = borg_options
|
|
13
13
|
@borg_path = validate_borg_path(borg_path || "borg")
|
|
14
|
+
@logger = logger
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def exists?
|
|
@@ -20,8 +21,10 @@ module Ruborg
|
|
|
20
21
|
def create
|
|
21
22
|
raise BorgError, "Repository already exists at #{@path}" if exists?
|
|
22
23
|
|
|
24
|
+
@logger&.info("Creating Borg repository at #{@path} with repokey encryption")
|
|
23
25
|
cmd = [@borg_path, "init", "--encryption=repokey", @path]
|
|
24
26
|
execute_borg_command(cmd)
|
|
27
|
+
@logger&.info("Repository created successfully at #{@path}")
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
def info
|
|
@@ -74,15 +77,19 @@ module Ruborg
|
|
|
74
77
|
|
|
75
78
|
unless keep_files_modified_within
|
|
76
79
|
# Fall back to standard pruning if no file metadata retention specified
|
|
80
|
+
@logger&.info("No file metadata retention specified, using standard pruning")
|
|
77
81
|
prune_standard_archives(retention_policy)
|
|
78
82
|
return
|
|
79
83
|
end
|
|
80
84
|
|
|
85
|
+
@logger&.info("Pruning per-file archives based on file modification time (keep within: #{keep_files_modified_within})")
|
|
86
|
+
|
|
81
87
|
# Parse time duration (e.g., "30d" -> 30 days)
|
|
82
88
|
cutoff_time = Time.now - parse_time_duration(keep_files_modified_within)
|
|
83
89
|
|
|
84
90
|
# Get all archives with metadata
|
|
85
91
|
archives = list_archives_with_metadata
|
|
92
|
+
@logger&.info("Found #{archives.size} archive(s) to evaluate for pruning")
|
|
86
93
|
|
|
87
94
|
archives_to_delete = []
|
|
88
95
|
|
|
@@ -91,16 +98,23 @@ module Ruborg
|
|
|
91
98
|
file_mtime = get_file_mtime_from_archive(archive[:name])
|
|
92
99
|
|
|
93
100
|
# Delete archive if file was modified before cutoff
|
|
94
|
-
|
|
101
|
+
if file_mtime && file_mtime < cutoff_time
|
|
102
|
+
archives_to_delete << archive[:name]
|
|
103
|
+
@logger&.debug("Archive #{archive[:name]} marked for deletion (file mtime: #{file_mtime})")
|
|
104
|
+
end
|
|
95
105
|
end
|
|
96
106
|
|
|
107
|
+
return if archives_to_delete.empty?
|
|
108
|
+
|
|
109
|
+
@logger&.info("Deleting #{archives_to_delete.size} archive(s)")
|
|
110
|
+
|
|
97
111
|
# Delete archives
|
|
98
112
|
archives_to_delete.each do |archive_name|
|
|
113
|
+
@logger&.debug("Deleting archive: #{archive_name}")
|
|
99
114
|
delete_archive(archive_name)
|
|
100
115
|
end
|
|
101
116
|
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
@logger&.info("Pruned #{archives_to_delete.size} archive(s) based on file modification time")
|
|
104
118
|
puts "Pruned #{archives_to_delete.size} archive(s) based on file modification time"
|
|
105
119
|
end
|
|
106
120
|
|
data/lib/ruborg/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruborg
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michail Pantelelis
|
|
@@ -133,6 +133,7 @@ extra_rdoc_files: []
|
|
|
133
133
|
files:
|
|
134
134
|
- ".rspec"
|
|
135
135
|
- ".rubocop.yml"
|
|
136
|
+
- ".ruby-version"
|
|
136
137
|
- CHANGELOG.md
|
|
137
138
|
- CLAUDE.md
|
|
138
139
|
- LICENSE
|
|
@@ -172,7 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
172
173
|
- !ruby/object:Gem::Version
|
|
173
174
|
version: '0'
|
|
174
175
|
requirements: []
|
|
175
|
-
rubygems_version: 3.
|
|
176
|
+
rubygems_version: 3.7.1
|
|
176
177
|
specification_version: 4
|
|
177
178
|
summary: A friendly Ruby frontend for Borg backup
|
|
178
179
|
test_files: []
|