ruborg 0.6.0 → 0.6.2
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 +47 -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 +10 -3
 - 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: 310a71ee8419f02ac2bee2fc03438a9d7b85e838e911202d99bb933bd21564d1
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: c7785d3fa514d20c901bd53b0923d0ccc044fa4fbf69b616009ba1ef16be9451
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 3532053d345494458c7c4eeee5c79808e73748d95360d71cac880d3db29a14205a4028c8d83aecff4a3fce3db217cf090403053be6fd860609d306271ee9c687
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 0a22d49b96fb91c79eb3d5ee264f524f8712ca40c61523be12b0186e9a604c0e77e1d8f9507f970edefa21802c0bd74176e6db89c8d75c8234caef25fad0a57a
         
     | 
    
        data/.ruby-version
    ADDED
    
    | 
         @@ -0,0 +1 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            3.2.9
         
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    | 
         @@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 
     | 
|
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
            ## [Unreleased]
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
      
 10 
     | 
    
         
            +
            ## [0.6.2] - 2025-10-08
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            ### Fixed
         
     | 
| 
      
 13 
     | 
    
         
            +
            - **Passbolt Integration**: Fixed Passbolt CLI command to include required `--id` flag
         
     | 
| 
      
 14 
     | 
    
         
            +
              - Changed command from `passbolt get resource <id> --json` to `passbolt get resource --id <id> --json`
         
     | 
| 
      
 15 
     | 
    
         
            +
              - Resolves "Error: required flag(s) 'id' not set" when retrieving passwords
         
     | 
| 
      
 16 
     | 
    
         
            +
              - Updated security tests to verify correct command format
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
            ## [0.6.1] - 2025-10-08
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            ### Added
         
     | 
| 
      
 21 
     | 
    
         
            +
            - **Enhanced Configuration Validation**: Comprehensive validation system to catch configuration errors early
         
     | 
| 
      
 22 
     | 
    
         
            +
              - **Unknown Key Detection**: Detects typos and invalid configuration keys at all levels (global, repository, sources, retention, passbolt, borg_options)
         
     | 
| 
      
 23 
     | 
    
         
            +
              - **Retention Policy Validation**: Validates retention policy structure and values
         
     | 
| 
      
 24 
     | 
    
         
            +
                - Integer fields (keep_hourly, keep_daily, etc.) must be non-negative integers
         
     | 
| 
      
 25 
     | 
    
         
            +
                - Time-based fields (keep_within, keep_files_modified_within) must use correct format (e.g., "7d", "30d")
         
     | 
| 
      
 26 
     | 
    
         
            +
                - Validates time format with h/d/w/m/y suffixes
         
     | 
| 
      
 27 
     | 
    
         
            +
                - Rejects empty retention policies
         
     | 
| 
      
 28 
     | 
    
         
            +
                - Detects unknown retention keys
         
     | 
| 
      
 29 
     | 
    
         
            +
              - **Passbolt Configuration Validation**: Validates passbolt config structure
         
     | 
| 
      
 30 
     | 
    
         
            +
                - Requires non-empty `resource_id` string
         
     | 
| 
      
 31 
     | 
    
         
            +
                - Type validation for resource_id field
         
     | 
| 
      
 32 
     | 
    
         
            +
                - Detects unknown passbolt keys
         
     | 
| 
      
 33 
     | 
    
         
            +
              - **Retention Mode Validation**: Validates `retention_mode` values (must be "standard" or "per_file")
         
     | 
| 
      
 34 
     | 
    
         
            +
              - **Source Validation**: Validates source structure (name, paths, exclude fields)
         
     | 
| 
      
 35 
     | 
    
         
            +
            - **Comprehensive Logging**: Added logging throughout backup, restore, and deletion operations
         
     | 
| 
      
 36 
     | 
    
         
            +
              - Repository operations (initialization, pruning, archive management)
         
     | 
| 
      
 37 
     | 
    
         
            +
              - Backup operations (file counts, progress in per-file mode)
         
     | 
| 
      
 38 
     | 
    
         
            +
              - Restore operations (extraction start/completion)
         
     | 
| 
      
 39 
     | 
    
         
            +
              - Source file deletion tracking (with `--remove-source`)
         
     | 
| 
      
 40 
     | 
    
         
            +
              - Passbolt integration events (resource ID logged, never passwords)
         
     | 
| 
      
 41 
     | 
    
         
            +
              - All sensitive data (passwords, encryption keys) protected from logs
         
     | 
| 
      
 42 
     | 
    
         
            +
            - **Enhanced Test Suite**: Expanded test coverage to 220 examples (67 new tests added)
         
     | 
| 
      
 43 
     | 
    
         
            +
              - 23 new configuration validation tests
         
     | 
| 
      
 44 
     | 
    
         
            +
              - 28 new logging integration tests
         
     | 
| 
      
 45 
     | 
    
         
            +
              - 10 new CLI validation tests
         
     | 
| 
      
 46 
     | 
    
         
            +
              - 6 new type checking tests for boolean configurations
         
     | 
| 
      
 47 
     | 
    
         
            +
              - All tests passing with 0 failures
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
            ### Changed
         
     | 
| 
      
 50 
     | 
    
         
            +
            - `global_settings` now includes `borg_path` (previously was in whitelist but not propagated)
         
     | 
| 
      
 51 
     | 
    
         
            +
            - Validation errors are collected and reported together for better user experience
         
     | 
| 
      
 52 
     | 
    
         
            +
            - All validation runs automatically on configuration load
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
            ### Fixed
         
     | 
| 
      
 55 
     | 
    
         
            +
            - **Configuration Consistency**: Fixed inconsistency where `borg_path` was allowed in VALID_GLOBAL_KEYS but not returned by `global_settings` method
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
       10 
57 
     | 
    
         
             
            ## [0.6.0] - 2025-10-08
         
     | 
| 
       11 
58 
     | 
    
         | 
| 
       12 
59 
     | 
    
         
             
            ### 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 
     | 
    
         | 
| 
       16 
     | 
    
         
            -
                   
     | 
| 
      
 17 
     | 
    
         
            +
                  @logger&.info("Retrieving password from Passbolt (resource_id: #{@resource_id})")
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                  cmd = ["passbolt", "get", "resource", "--id", @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.2
         
     | 
| 
       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: []
         
     |