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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 821f1707180870ec6b2ffccda4ef4d901b3ee274d7402c26170ca294e80efb8b
4
- data.tar.gz: 62e2a5ee53cd75024b78e08e32295c34cf1829cd3bd34be9d6403c7ec1dfb810
3
+ metadata.gz: 310a71ee8419f02ac2bee2fc03438a9d7b85e838e911202d99bb933bd21564d1
4
+ data.tar.gz: c7785d3fa514d20c901bd53b0923d0ccc044fa4fbf69b616009ba1ef16be9451
5
5
  SHA512:
6
- metadata.gz: acd1bcd0b6da0914888c11d50299ea2f365e2bf5a81423edbfc7e935a17d020b841de9043900ca86ed1df2ab8e50e1359806ad2857de3bd849b8f040c2909549
7
- data.tar.gz: 799d52c64a3bf858b2324f5101255a77dd428778b1eced9bfa11e54d9c2ef23642645a886902f83c05336c89a61d1a80b02ad83185fecf3cf0cac866d18e7c09
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
- e- ⏰ **Retention Policies** - Configure backup retention (hourly, daily, weekly, monthly, yearly)
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 (178+ tests)
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
- - Boolean types (must be `true` or `false`, not strings like `'true'`)
192
- - Valid compression values (lz4, zstd, zlib, lzma, none)
193
- - Valid encryption modes
194
- - Required repository fields (name, path)
195
- - Correct borg_options values
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
@@ -3,4 +3,12 @@
3
3
 
4
4
  require_relative "../lib/ruborg"
5
5
 
6
- Ruborg::CLI.start(ARGV)
6
+ begin
7
+ Ruborg::CLI.start(ARGV)
8
+ rescue Ruborg::Error => e
9
+ warn "Error: #{e.message}"
10
+ exit 1
11
+ rescue Interrupt
12
+ warn "\nInterrupted"
13
+ exit 130
14
+ end
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.each do |file_path|
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
- next unless File.exist?(real_path)
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
- error_exit(e)
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
- error_exit(e)
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
- error_exit(e)
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
- error_exit(e)
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
- error_exit(e)
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
- error_exit(e)
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
- # Validate global borg_options
100
- if @data["borg_options"]
101
- errors.concat(validate_boolean_config(@data["borg_options"], "allow_relocated_repo", "global/borg_options"))
102
- errors.concat(validate_boolean_config(@data["borg_options"], "allow_unencrypted_repo", "global/borg_options"))
103
- end
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
- if repo["borg_options"]
123
- errors.concat(validate_boolean_config(repo["borg_options"], "allow_relocated_repo",
124
- "#{repo_name}/borg_options"))
125
- errors.concat(validate_boolean_config(repo["borg_options"], "allow_unencrypted_repo",
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
@@ -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
- cmd = ["passbolt", "get", "resource", @resource_id, "--json"]
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
- raise PassboltError, "Failed to retrieve password from Passbolt" unless status
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
 
@@ -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
- archives_to_delete << archive[:name] if file_mtime && file_mtime < cutoff_time
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
- return if archives_to_delete.empty?
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruborg
4
- VERSION = "0.6.0"
4
+ VERSION = "0.6.2"
5
5
  end
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.0
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.6.9
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: []