ruborg 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e48935c72c444956ca0b7918efb14bd3d5b8a38273ec6843adec2fbb2da6968
4
- data.tar.gz: f200cb546d62431ec1ff84544da0b96b9f84cf7877aa29627608f8c43119d2b7
3
+ metadata.gz: b3dfe277e22cf29ef1132f479b8e8a969e87c69b383529b7f05eb7238ac5ea07
4
+ data.tar.gz: fcf98cabcd5f4636fc513e69e3b446747c9ca2f054f6480f4da60416ec865f0d
5
5
  SHA512:
6
- metadata.gz: 4614d7f78363f420ae4ce80355725fb84e068bfac0d94591f1cee56a6d14c7c6664dd415405462086bdf2fa7543187702f346645655b2bb32b5ffb62fea288a9
7
- data.tar.gz: f70898563085b03b78cf4cf140abb5efd9bd3bd6881918aa7a77c976823a15469688e3aeafd6814c4ff880b3e5c8f949264a1c03f9b1251f3467d409ebe1f302
6
+ metadata.gz: 159296240a1cec2e7791edb3689bf032569d8c1f4fda0ac5c1522e92076fec8c719dba3117326c9fcb9e074ffa295ed71a75b500657625d1ce71fab391b441c2
7
+ data.tar.gz: f6385fa1ff56520719b1f8b3985931b4083b3139889b34ed4d1dcb5d449eec8ba98e96f030988cf03cdcb5744f47255ef3b7a605e7bb040d6b1f2c0446ab3b81
data/.rspec CHANGED
@@ -1,3 +1,3 @@
1
1
  --require spec_helper
2
2
  --color
3
- --format documentation
3
+ --format documentation
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.3.0] - 2025-10-05
11
+
12
+ ### Added
13
+ - Auto-initialization feature: Set `auto_init: true` in config to automatically initialize repositories on first use
14
+ - Multi-repository configuration support with per-repository sources
15
+ - `--repository` / `-r` option to target specific repository in multi-repo configs
16
+ - `--all` option to backup all repositories at once
17
+ - Repository-specific Passbolt integration (overrides global settings)
18
+ - Per-source exclude patterns in multi-repo configs
19
+ - BackupConfig wrapper class for multi-repo compatibility
20
+ - Automatic format detection (single vs multi-repo)
21
+ - Support for multiple backup sources per repository
22
+ - Global settings with per-repository overrides
23
+ - `log_file` configuration option to set log path in config file
24
+ - Log file priority: CLI option > config file > default
25
+
26
+ ### Changed
27
+ - Config class now detects and handles both single-repo and multi-repo formats
28
+ - Backup command automatically routes to single or multi-repo implementation
29
+ - Archive naming includes repository name for multi-repo configs
30
+ - CLI now reads log_file from config if --log option not provided
31
+
32
+ ## [0.2.0] - 2025-10-05
33
+
34
+ ### Added
35
+ - `--remove-source` option to delete source files after successful backup
36
+ - Comprehensive logging system with daily rotation (default: `~/.ruborg/logs/ruborg.log`)
37
+ - Custom log file support via `--log` option for all commands
38
+ - `--path` option for restore command to extract single files/directories from archives
39
+ - Comprehensive RSpec test suite with mocked Passbolt and actual Borg integration tests
40
+ - Borg installation instructions for macOS and Ubuntu in README
41
+ - Support for environment variables to prevent interactive prompts (`BORG_RELOCATED_REPO_ACCESS_IS_OK`, etc.)
42
+ - Automatic destination directory creation for restore operations
43
+ - Test helpers and fixtures for easier testing
44
+
45
+ ### Fixed
46
+ - Passbolt CLI command corrected from `passbolt get <id>` to `passbolt get resource <id>`
47
+ - Borg commands now properly redirect stdin to prevent interactive passphrase prompts
48
+ - Improved error handling and logging throughout the application
49
+
50
+ ### Changed
51
+ - Refactored Passbolt class to use testable `execute_command` method
52
+ - Enhanced Repository and Backup classes to properly handle environment variables
53
+ - Improved CLI integration with better Passbolt mock support in tests
54
+
55
+ ## [0.1.0] - 2025-10-04
56
+
10
57
  ### Added
11
58
  - Initial gem structure
12
59
  - Borg repository initialization and management
@@ -14,8 +61,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
61
  - YAML configuration file support
15
62
  - Passbolt CLI integration for password management
16
63
  - Command-line interface with Thor
17
-
18
- ## [0.1.0] - 2025-10-04
19
-
20
- ### Added
21
- - Initial release
64
+ - Basic error handling
data/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  # Ruborg
2
2
 
3
- > **⚠️ WARNING: This project is under heavy development and is not yet functional. Do not use in production.**
4
- >
5
3
  > This gem is being developed with the assistance of Claude AI.
6
4
 
7
5
  A friendly Ruby frontend for [Borg Backup](https://www.borgbackup.org/). Ruborg simplifies backup management by providing a YAML-based configuration system and seamless integration with Passbolt for encryption password management.
@@ -14,13 +12,37 @@ A friendly Ruby frontend for [Borg Backup](https://www.borgbackup.org/). Ruborg
14
12
  - 🔐 **Passbolt Integration** - Secure password management via Passbolt CLI
15
13
  - 🎯 **Pattern Exclusions** - Flexible file exclusion patterns
16
14
  - 🗜️ **Compression Options** - Support for multiple compression algorithms
15
+ - 🗂️ **Selective Restore** - Restore individual files or directories from archives
16
+ - 🧹 **Auto-cleanup** - Optionally remove source files after successful backup
17
+ - 📊 **Logging** - Comprehensive logging with daily rotation
18
+ - 🗄️ **Multi-Repository** - Manage multiple backup repositories with different sources
19
+ - 🔄 **Auto-initialization** - Automatically initialize repositories on first use
20
+ - ✅ **Well-tested** - Comprehensive test suite with RSpec
17
21
 
18
22
  ## Prerequisites
19
23
 
20
- - Ruby >= 2.7.0
24
+ - Ruby >= 3.2.0
21
25
  - [Borg Backup](https://www.borgbackup.org/) installed and available in PATH
22
26
  - [Passbolt CLI](https://github.com/passbolt/go-passbolt-cli) (optional, for password management)
23
27
 
28
+ ### Installing Borg Backup
29
+
30
+ **macOS:**
31
+ ```bash
32
+ brew install borgbackup
33
+ ```
34
+
35
+ **Ubuntu/Debian:**
36
+ ```bash
37
+ sudo apt update
38
+ sudo apt install borgbackup
39
+ ```
40
+
41
+ **Verify installation:**
42
+ ```bash
43
+ borg --version
44
+ ```
45
+
24
46
  ## Installation
25
47
 
26
48
  Add this line to your application's Gemfile:
@@ -43,7 +65,9 @@ gem install ruborg
43
65
 
44
66
  ## Configuration
45
67
 
46
- Create a `ruborg.yml` configuration file:
68
+ Ruborg supports two configuration formats: **single repository** (legacy) and **multi-repository** (recommended for complex setups).
69
+
70
+ ### Single Repository Configuration
47
71
 
48
72
  ```yaml
49
73
  # Repository path
@@ -53,15 +77,11 @@ repository: /path/to/borg/repository
53
77
  backup_paths:
54
78
  - /home/user/documents
55
79
  - /home/user/projects
56
- - /etc
57
80
 
58
81
  # Exclude patterns
59
82
  exclude_patterns:
60
83
  - "*.tmp"
61
84
  - "*.log"
62
- - "*/.cache/*"
63
- - "*/node_modules/*"
64
- - "*/.git/*"
65
85
 
66
86
  # Compression algorithm (lz4, zstd, zlib, lzma, none)
67
87
  compression: lz4
@@ -72,9 +92,62 @@ encryption: repokey
72
92
  # Passbolt integration (optional)
73
93
  passbolt:
74
94
  resource_id: "your-passbolt-resource-uuid"
95
+
96
+ # Auto-initialize repository (optional, default: false)
97
+ auto_init: true
98
+
99
+ # Log file path (optional, default: ~/.ruborg/logs/ruborg.log)
100
+ log_file: /var/log/ruborg.log
75
101
  ```
76
102
 
77
- See `ruborg.yml.example` for a complete configuration template.
103
+ ### Multi-Repository Configuration
104
+
105
+ For managing multiple repositories with different sources:
106
+
107
+ ```yaml
108
+ # Global settings (applied to all repositories unless overridden)
109
+ compression: lz4
110
+ encryption: repokey
111
+ auto_init: true
112
+ passbolt:
113
+ resource_id: "global-passbolt-id"
114
+
115
+ # Multiple repositories
116
+ repositories:
117
+ - name: documents
118
+ path: /mnt/backup/documents
119
+ sources:
120
+ - name: home-docs
121
+ paths:
122
+ - /home/user/documents
123
+ exclude:
124
+ - "*.tmp"
125
+ - name: work-docs
126
+ paths:
127
+ - /home/user/work
128
+ exclude:
129
+ - "*.log"
130
+
131
+ - name: databases
132
+ path: /mnt/backup/databases
133
+ # Repository-specific passbolt (overrides global)
134
+ passbolt:
135
+ resource_id: "db-specific-passbolt-id"
136
+ sources:
137
+ - name: mysql
138
+ paths:
139
+ - /var/lib/mysql/dumps
140
+ - name: postgres
141
+ paths:
142
+ - /var/lib/postgresql/dumps
143
+ ```
144
+
145
+ **Multi-repo benefits:**
146
+ - Organize backups by type (documents, databases, media)
147
+ - Different encryption keys per repository
148
+ - Multiple sources per repository
149
+ - Per-source exclude patterns
150
+ - Repository-specific settings override global ones
78
151
 
79
152
  ## Usage
80
153
 
@@ -90,6 +163,7 @@ ruborg init /path/to/repository --passbolt-id "resource-uuid"
90
163
 
91
164
  ### Create a Backup
92
165
 
166
+ **Single repository:**
93
167
  ```bash
94
168
  # Using default configuration (ruborg.yml)
95
169
  ruborg backup
@@ -99,6 +173,21 @@ ruborg backup --config /path/to/config.yml
99
173
 
100
174
  # With custom archive name
101
175
  ruborg backup --name "my-backup-2025-10-04"
176
+
177
+ # Remove source files after successful backup
178
+ ruborg backup --remove-source
179
+ ```
180
+
181
+ **Multi-repository:**
182
+ ```bash
183
+ # Backup specific repository
184
+ ruborg backup --repository documents
185
+
186
+ # Backup all repositories
187
+ ruborg backup --all
188
+
189
+ # Backup specific repository with custom name
190
+ ruborg backup --repository databases --name "db-backup-2025-10-05"
102
191
  ```
103
192
 
104
193
  ### List Archives
@@ -110,11 +199,14 @@ ruborg list
110
199
  ### Restore from Archive
111
200
 
112
201
  ```bash
113
- # Restore to current directory
202
+ # Restore entire archive to current directory
114
203
  ruborg restore archive-name
115
204
 
116
205
  # Restore to specific directory
117
206
  ruborg restore archive-name --destination /path/to/restore
207
+
208
+ # Restore a single file from archive
209
+ ruborg restore archive-name --path /path/to/file.txt --destination /new/location
118
210
  ```
119
211
 
120
212
  ### View Repository Information
@@ -123,13 +215,50 @@ ruborg restore archive-name --destination /path/to/restore
123
215
  ruborg info
124
216
  ```
125
217
 
218
+ ## Logging
219
+
220
+ Ruborg automatically logs all operations with daily rotation. Log file location priority:
221
+
222
+ 1. **CLI option** (highest priority): `--log /path/to/custom.log`
223
+ 2. **Config file**: `log_file: /path/to/log.log`
224
+ 3. **Default**: `~/.ruborg/logs/ruborg.log`
225
+
226
+ **Examples:**
227
+
228
+ ```bash
229
+ # Use CLI option (overrides config)
230
+ ruborg backup --log /var/log/ruborg.log
231
+
232
+ # Or set in config file
233
+ log_file: /var/log/ruborg.log
234
+ ```
235
+
236
+ **Logs include:**
237
+ - Operation start/completion timestamps
238
+ - Paths being backed up
239
+ - Archive names created
240
+ - Success and error messages
241
+ - Source file removal actions
242
+
126
243
  ## Passbolt Integration
127
244
 
128
245
  Ruborg can retrieve encryption passphrases from Passbolt using the Passbolt CLI:
129
246
 
130
247
  1. Install and configure [Passbolt CLI](https://github.com/passbolt/go-passbolt-cli)
131
- 2. Store your Borg repository passphrase in Passbolt
132
- 3. Add the resource ID to your `ruborg.yml`:
248
+ 2. Configure Passbolt CLI with your server credentials:
249
+ ```bash
250
+ passbolt configure --serverAddress https://server.address \
251
+ --userPrivateKeyFile /path/to/private.key \
252
+ --userPassword YOUR_PASSWORD
253
+ ```
254
+ Or set environment variables:
255
+ ```bash
256
+ export PASSBOLT_SERVER_ADDRESS=https://server.address
257
+ export PASSBOLT_USER_PRIVATE_KEY_FILE=/path/to/private.key
258
+ export PASSBOLT_USER_PASSWORD=YOUR_PASSWORD
259
+ ```
260
+ 3. Store your Borg repository passphrase in Passbolt
261
+ 4. Add the resource ID to your `ruborg.yml`:
133
262
 
134
263
  ```yaml
135
264
  passbolt:
@@ -138,15 +267,41 @@ passbolt:
138
267
 
139
268
  Ruborg will automatically retrieve the passphrase when performing backup operations.
140
269
 
270
+ ## Auto-initialization
271
+
272
+ Set `auto_init: true` in your configuration file to automatically initialize the repository on first use:
273
+
274
+ ```yaml
275
+ repository: /path/to/borg/repository
276
+ auto_init: true
277
+ passbolt:
278
+ resource_id: "your-passbolt-resource-uuid"
279
+ backup_paths:
280
+ - /path/to/backup
281
+ ```
282
+
283
+ When enabled, ruborg will automatically run `borg init` if the repository doesn't exist when you run `backup`, `list`, or `info` commands. The passphrase will be retrieved from Passbolt if configured.
284
+
141
285
  ## Command Reference
142
286
 
143
287
  | Command | Description | Options |
144
288
  |---------|-------------|---------|
145
- | `init REPOSITORY` | Initialize a new Borg repository | `--passphrase`, `--passbolt-id` |
146
- | `backup` | Create a backup using config file | `--config`, `--name` |
147
- | `list` | List all archives in repository | `--config` |
148
- | `restore ARCHIVE` | Restore files from archive | `--config`, `--destination` |
149
- | `info` | Show repository information | `--config` |
289
+ | `init REPOSITORY` | Initialize a new Borg repository | `--passphrase`, `--passbolt-id`, `--log` |
290
+ | `backup` | Create a backup using config file | `--config`, `--name`, `--remove-source`, `--repository`, `--all`, `--log` |
291
+ | `list` | List all archives in repository | `--config`, `--repository`, `--log` |
292
+ | `restore ARCHIVE` | Restore files from archive | `--config`, `--destination`, `--path`, `--repository`, `--log` |
293
+ | `info` | Show repository information | `--config`, `--repository`, `--log` |
294
+
295
+ ### Global Options
296
+
297
+ - `--config`: Path to configuration file (default: `ruborg.yml`)
298
+ - `--log`: Path to log file (overrides config, default: `~/.ruborg/logs/ruborg.log`)
299
+ - `--repository` / `-r`: Repository name (required for multi-repo configs)
300
+
301
+ ### Multi-Repository Options
302
+
303
+ - `--all`: Backup all repositories (multi-repo config only)
304
+ - `--repository NAME`: Target specific repository by name
150
305
 
151
306
  ## Development
152
307
 
@@ -169,9 +324,24 @@ bundle exec rake release
169
324
  Run the test suite:
170
325
 
171
326
  ```bash
327
+ # Run all tests
172
328
  bundle exec rspec
329
+
330
+ # Run only unit tests (no Borg required)
331
+ bundle exec rspec --tag ~borg
332
+
333
+ # Run only integration tests (requires Borg)
334
+ bundle exec rspec --tag borg
173
335
  ```
174
336
 
337
+ The test suite includes:
338
+ - Config loading and validation
339
+ - Repository management (with actual Borg integration)
340
+ - Backup and restore operations
341
+ - Passbolt integration (mocked)
342
+ - CLI commands
343
+ - Logging functionality
344
+
175
345
  ## Contributing
176
346
 
177
347
  Bug reports and pull requests are welcome on GitHub at https://github.com/mpantel/ruborg.
data/lib/ruborg/backup.rb CHANGED
@@ -8,22 +8,33 @@ module Ruborg
8
8
  @config = config
9
9
  end
10
10
 
11
- def create(name: nil)
11
+ def create(name: nil, remove_source: false)
12
12
  raise BorgError, "Repository does not exist" unless @repository.exists?
13
13
 
14
14
  archive_name = name || Time.now.strftime("%Y-%m-%d_%H-%M-%S")
15
15
  cmd = build_create_command(archive_name)
16
16
 
17
17
  execute_borg_command(cmd)
18
+
19
+ remove_source_files if remove_source
18
20
  end
19
21
 
20
- def extract(archive_name, destination: ".")
22
+ def extract(archive_name, destination: ".", path: nil)
21
23
  raise BorgError, "Repository does not exist" unless @repository.exists?
22
24
 
23
25
  cmd = ["borg", "extract", "#{@repository.path}::#{archive_name}"]
24
- cmd += ["--destination", destination] if destination != "."
25
-
26
- execute_borg_command(cmd)
26
+ cmd << path if path
27
+
28
+ # Change to destination directory if specified
29
+ if destination != "."
30
+ require "fileutils"
31
+ FileUtils.mkdir_p(destination) unless File.directory?(destination)
32
+ Dir.chdir(destination) do
33
+ execute_borg_command(cmd)
34
+ end
35
+ else
36
+ execute_borg_command(cmd)
37
+ end
27
38
  end
28
39
 
29
40
  def list_archives
@@ -52,10 +63,28 @@ module Ruborg
52
63
  end
53
64
 
54
65
  def execute_borg_command(cmd)
55
- result = system(*cmd)
66
+ env = {}
67
+ passphrase = @repository.instance_variable_get(:@passphrase)
68
+ env["BORG_PASSPHRASE"] = passphrase if passphrase
69
+ env["BORG_RELOCATED_REPO_ACCESS_IS_OK"] = "yes"
70
+ env["BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK"] = "yes"
71
+
72
+ result = system(env, *cmd, in: "/dev/null")
56
73
  raise BorgError, "Borg command failed: #{cmd.join(' ')}" unless result
57
74
 
58
75
  result
59
76
  end
77
+
78
+ def remove_source_files
79
+ require "fileutils"
80
+
81
+ @config.backup_paths.each do |path|
82
+ if File.directory?(path)
83
+ FileUtils.rm_rf(path)
84
+ elsif File.file?(path)
85
+ FileUtils.rm(path)
86
+ end
87
+ end
88
+ end
60
89
  end
61
90
  end
data/lib/ruborg/cli.rb CHANGED
@@ -6,68 +6,123 @@ module Ruborg
6
6
  # Command-line interface for ruborg
7
7
  class CLI < Thor
8
8
  class_option :config, type: :string, default: "ruborg.yml", desc: "Path to configuration file"
9
+ class_option :log, type: :string, desc: "Path to log file"
10
+ class_option :repository, type: :string, aliases: "-r", desc: "Repository name (for multi-repo configs)"
11
+
12
+ def initialize(*args)
13
+ super
14
+ # Priority: CLI option > config file > default
15
+ log_path = options[:log]
16
+ unless log_path
17
+ # Try to load config to get log_file setting
18
+ config_path = options[:config] || "ruborg.yml"
19
+ if File.exist?(config_path)
20
+ config_data = YAML.load_file(config_path) rescue {}
21
+ log_path = config_data["log_file"]
22
+ end
23
+ end
24
+ @logger = RuborgLogger.new(log_file: log_path)
25
+ end
9
26
 
10
27
  desc "init REPOSITORY", "Initialize a new Borg repository"
11
28
  option :passphrase, type: :string, desc: "Repository passphrase"
12
29
  option :passbolt_id, type: :string, desc: "Passbolt resource ID for passphrase"
13
30
  def init(repository_path)
31
+ @logger.info("Initializing repository at #{repository_path}")
14
32
  passphrase = get_passphrase(options[:passphrase], options[:passbolt_id])
15
33
  repo = Repository.new(repository_path, passphrase: passphrase)
16
34
  repo.create
35
+ @logger.info("Repository successfully initialized at #{repository_path}")
17
36
  puts "Repository initialized at #{repository_path}"
18
37
  rescue Error => e
38
+ @logger.error("Failed to initialize repository: #{e.message}")
19
39
  error_exit(e)
20
40
  end
21
41
 
22
42
  desc "backup", "Create a backup using configuration file"
23
43
  option :name, type: :string, desc: "Archive name"
44
+ option :remove_source, type: :boolean, default: false, desc: "Remove source files after successful backup"
45
+ option :all, type: :boolean, default: false, desc: "Backup all repositories (multi-repo config only)"
24
46
  def backup
47
+ @logger.info("Starting backup operation with config: #{options[:config]}")
25
48
  config = Config.new(options[:config])
26
- passphrase = fetch_passphrase_from_config(config)
27
-
28
- repo = Repository.new(config.repository, passphrase: passphrase)
29
- backup = Backup.new(repo, config: config)
30
49
 
31
- backup.create(name: options[:name])
32
- puts "Backup created successfully"
50
+ if config.multi_repo?
51
+ backup_multi_repo(config)
52
+ else
53
+ backup_single_repo(config)
54
+ end
33
55
  rescue Error => e
56
+ @logger.error("Backup failed: #{e.message}")
34
57
  error_exit(e)
35
58
  end
36
59
 
37
60
  desc "list", "List all archives in the repository"
38
61
  def list
62
+ @logger.info("Listing archives in repository")
39
63
  config = Config.new(options[:config])
40
64
  passphrase = fetch_passphrase_from_config(config)
41
65
 
42
66
  repo = Repository.new(config.repository, passphrase: passphrase)
67
+
68
+ # Auto-initialize repository if configured
69
+ if config.auto_init? && !repo.exists?
70
+ @logger.info("Auto-initializing repository at #{config.repository}")
71
+ repo.create
72
+ puts "Repository auto-initialized at #{config.repository}"
73
+ end
74
+
43
75
  repo.list
76
+ @logger.info("Successfully listed archives")
44
77
  rescue Error => e
78
+ @logger.error("Failed to list archives: #{e.message}")
45
79
  error_exit(e)
46
80
  end
47
81
 
48
82
  desc "restore ARCHIVE", "Restore files from an archive"
49
83
  option :destination, type: :string, default: ".", desc: "Destination directory"
84
+ option :path, type: :string, desc: "Specific file or directory path to restore from archive"
50
85
  def restore(archive_name)
86
+ restore_target = options[:path] ? "#{options[:path]} from #{archive_name}" : archive_name
87
+ @logger.info("Restoring #{restore_target} to #{options[:destination]}")
51
88
  config = Config.new(options[:config])
52
89
  passphrase = fetch_passphrase_from_config(config)
53
90
 
54
91
  repo = Repository.new(config.repository, passphrase: passphrase)
55
92
  backup = Backup.new(repo, config: config)
56
93
 
57
- backup.extract(archive_name, destination: options[:destination])
58
- puts "Archive restored to #{options[:destination]}"
94
+ backup.extract(archive_name, destination: options[:destination], path: options[:path])
95
+ @logger.info("Successfully restored #{restore_target} to #{options[:destination]}")
96
+
97
+ if options[:path]
98
+ puts "Restored #{options[:path]} from #{archive_name} to #{options[:destination]}"
99
+ else
100
+ puts "Archive restored to #{options[:destination]}"
101
+ end
59
102
  rescue Error => e
103
+ @logger.error("Failed to restore archive: #{e.message}")
60
104
  error_exit(e)
61
105
  end
62
106
 
63
107
  desc "info", "Show repository information"
64
108
  def info
109
+ @logger.info("Retrieving repository information")
65
110
  config = Config.new(options[:config])
66
111
  passphrase = fetch_passphrase_from_config(config)
67
112
 
68
113
  repo = Repository.new(config.repository, passphrase: passphrase)
114
+
115
+ # Auto-initialize repository if configured
116
+ if config.auto_init? && !repo.exists?
117
+ @logger.info("Auto-initializing repository at #{config.repository}")
118
+ repo.create
119
+ puts "Repository auto-initialized at #{config.repository}"
120
+ end
121
+
69
122
  repo.info
123
+ @logger.info("Successfully retrieved repository information")
70
124
  rescue Error => e
125
+ @logger.error("Failed to get repository info: #{e.message}")
71
126
  error_exit(e)
72
127
  end
73
128
 
@@ -91,5 +146,128 @@ module Ruborg
91
146
  puts "Error: #{error.message}"
92
147
  exit 1
93
148
  end
149
+
150
+ # Single repository backup (legacy)
151
+ def backup_single_repo(config)
152
+ @logger.info("Backing up paths: #{config.backup_paths.join(', ')}")
153
+ passphrase = fetch_passphrase_from_config(config)
154
+
155
+ repo = Repository.new(config.repository, passphrase: passphrase)
156
+
157
+ # Auto-initialize repository if configured
158
+ if config.auto_init? && !repo.exists?
159
+ @logger.info("Auto-initializing repository at #{config.repository}")
160
+ repo.create
161
+ puts "Repository auto-initialized at #{config.repository}"
162
+ end
163
+
164
+ backup = Backup.new(repo, config: config)
165
+
166
+ archive_name = options[:name] || Time.now.strftime("%Y-%m-%d_%H-%M-%S")
167
+ @logger.info("Creating archive: #{archive_name}")
168
+ backup.create(name: options[:name], remove_source: options[:remove_source])
169
+ @logger.info("Backup created successfully: #{archive_name}")
170
+
171
+ if options[:remove_source]
172
+ @logger.info("Removed source files: #{config.backup_paths.join(', ')}")
173
+ end
174
+
175
+ puts "Backup created successfully"
176
+ puts "Source files removed" if options[:remove_source]
177
+ end
178
+
179
+ # Multi-repository backup
180
+ def backup_multi_repo(config)
181
+ global_settings = config.global_settings
182
+ repos_to_backup = if options[:all]
183
+ config.repositories
184
+ elsif options[:repository]
185
+ repo_config = config.get_repository(options[:repository])
186
+ raise ConfigError, "Repository '#{options[:repository]}' not found" unless repo_config
187
+ [repo_config]
188
+ else
189
+ raise ConfigError, "Please specify --repository or --all for multi-repo config"
190
+ end
191
+
192
+ repos_to_backup.each do |repo_config|
193
+ backup_repository(repo_config, global_settings)
194
+ end
195
+ end
196
+
197
+ def backup_repository(repo_config, global_settings)
198
+ repo_name = repo_config["name"]
199
+ puts "\n--- Backing up repository: #{repo_name} ---"
200
+ @logger.info("Backing up repository: #{repo_name}")
201
+
202
+ # Merge global settings with repo-specific settings (repo-specific takes precedence)
203
+ merged_config = global_settings.merge(repo_config)
204
+
205
+ passphrase = fetch_passphrase_for_repo(merged_config)
206
+ repo = Repository.new(repo_config["path"], passphrase: passphrase)
207
+
208
+ # Auto-initialize if configured
209
+ auto_init = merged_config["auto_init"] || false
210
+ if auto_init && !repo.exists?
211
+ @logger.info("Auto-initializing repository at #{repo_config['path']}")
212
+ repo.create
213
+ puts "Repository auto-initialized at #{repo_config['path']}"
214
+ end
215
+
216
+ # Create backup config wrapper
217
+ backup_config = BackupConfig.new(repo_config, merged_config)
218
+ backup = Backup.new(repo, config: backup_config)
219
+
220
+ archive_name = options[:name] || "#{repo_name}-#{Time.now.strftime('%Y-%m-%d_%H-%M-%S')}"
221
+ @logger.info("Creating archive: #{archive_name}")
222
+
223
+ sources = repo_config["sources"] || []
224
+ @logger.info("Backing up #{sources.size} source(s)")
225
+
226
+ backup.create(name: archive_name, remove_source: options[:remove_source])
227
+ @logger.info("Backup created successfully: #{archive_name}")
228
+
229
+ puts "✓ Backup created: #{archive_name}"
230
+ puts " Sources removed" if options[:remove_source]
231
+ end
232
+
233
+ def fetch_passphrase_for_repo(repo_config)
234
+ passbolt_config = repo_config["passbolt"]
235
+ return nil if passbolt_config.nil? || passbolt_config.empty?
236
+
237
+ Passbolt.new(resource_id: passbolt_config["resource_id"]).get_password
238
+ end
239
+
240
+ # Wrapper class to adapt multi-repo config to existing Backup class
241
+ class BackupConfig
242
+ def initialize(repo_config, merged_settings)
243
+ @repo_config = repo_config
244
+ @merged_settings = merged_settings
245
+ end
246
+
247
+ def backup_paths
248
+ sources = @repo_config["sources"] || []
249
+ sources.flat_map do |source|
250
+ source["paths"] || []
251
+ end
252
+ end
253
+
254
+ def exclude_patterns
255
+ patterns = []
256
+ sources = @repo_config["sources"] || []
257
+ sources.each do |source|
258
+ patterns += (source["exclude"] || [])
259
+ end
260
+ patterns += (@merged_settings["exclude_patterns"] || [])
261
+ patterns.uniq
262
+ end
263
+
264
+ def compression
265
+ @merged_settings["compression"] || "lz4"
266
+ end
267
+
268
+ def encryption_mode
269
+ @merged_settings["encryption"] || "repokey"
270
+ end
271
+ end
94
272
  end
95
273
  end
data/lib/ruborg/config.rb CHANGED
@@ -11,6 +11,7 @@ module Ruborg
11
11
  def initialize(config_path)
12
12
  @config_path = config_path
13
13
  load_config
14
+ detect_format
14
15
  end
15
16
 
16
17
  def load_config
@@ -21,6 +22,7 @@ module Ruborg
21
22
  raise ConfigError, "Invalid YAML syntax: #{e.message}"
22
23
  end
23
24
 
25
+ # Legacy single-repo accessors (for backward compatibility)
24
26
  def repository
25
27
  @data["repository"]
26
28
  end
@@ -44,5 +46,43 @@ module Ruborg
44
46
  def passbolt_integration
45
47
  @data["passbolt"] || {}
46
48
  end
49
+
50
+ def auto_init?
51
+ @data["auto_init"] || false
52
+ end
53
+
54
+ def log_file
55
+ @data["log_file"]
56
+ end
57
+
58
+ # New multi-repo support
59
+ def multi_repo?
60
+ @multi_repo
61
+ end
62
+
63
+ def repositories
64
+ return [] unless multi_repo?
65
+ @data["repositories"] || []
66
+ end
67
+
68
+ def get_repository(name)
69
+ return nil unless multi_repo?
70
+ repositories.find { |r| r["name"] == name }
71
+ end
72
+
73
+ def repository_names
74
+ return [] unless multi_repo?
75
+ repositories.map { |r| r["name"] }
76
+ end
77
+
78
+ def global_settings
79
+ @data.slice("passbolt", "compression", "encryption", "auto_init")
80
+ end
81
+
82
+ private
83
+
84
+ def detect_format
85
+ @multi_repo = @data.key?("repositories")
86
+ end
47
87
  end
48
88
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "fileutils"
5
+
6
+ module Ruborg
7
+ # Logging functionality for ruborg
8
+ class RuborgLogger
9
+ attr_reader :logger
10
+
11
+ def initialize(log_file: nil)
12
+ @log_file = log_file || default_log_file
13
+ ensure_log_directory
14
+ @logger = Logger.new(@log_file, "daily")
15
+ @logger.level = Logger::INFO
16
+ @logger.formatter = proc do |severity, datetime, progname, msg|
17
+ "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
18
+ end
19
+ end
20
+
21
+ def info(message)
22
+ @logger.info(message)
23
+ end
24
+
25
+ def error(message)
26
+ @logger.error(message)
27
+ end
28
+
29
+ def warn(message)
30
+ @logger.warn(message)
31
+ end
32
+
33
+ def debug(message)
34
+ @logger.debug(message)
35
+ end
36
+
37
+ private
38
+
39
+ def default_log_file
40
+ File.join(log_directory, "ruborg.log")
41
+ end
42
+
43
+ def log_directory
44
+ dir = File.expand_path("~/.ruborg/logs")
45
+ dir
46
+ end
47
+
48
+ def ensure_log_directory
49
+ FileUtils.mkdir_p(File.dirname(@log_file)) unless File.directory?(File.dirname(@log_file))
50
+ end
51
+ end
52
+ end
@@ -13,10 +13,10 @@ module Ruborg
13
13
  def get_password
14
14
  raise PassboltError, "Resource ID not configured" unless @resource_id
15
15
 
16
- cmd = ["passbolt", "get", @resource_id, "--json"]
17
- output = `#{cmd.join(' ')}`
16
+ cmd = ["passbolt", "get", "resource", @resource_id, "--json"]
17
+ output, status = execute_command(cmd)
18
18
 
19
- raise PassboltError, "Failed to retrieve password from Passbolt" unless $?.success?
19
+ raise PassboltError, "Failed to retrieve password from Passbolt" unless status
20
20
 
21
21
  parse_password(output)
22
22
  end
@@ -29,6 +29,11 @@ module Ruborg
29
29
  end
30
30
  end
31
31
 
32
+ def execute_command(cmd)
33
+ output = `#{cmd.join(' ')}`
34
+ [output, $?.success?]
35
+ end
36
+
32
37
  def parse_password(json_output)
33
38
  data = JSON.parse(json_output)
34
39
  data["password"] || data["secret"]
@@ -40,8 +40,11 @@ module Ruborg
40
40
  def execute_borg_command(cmd)
41
41
  env = {}
42
42
  env["BORG_PASSPHRASE"] = @passphrase if @passphrase
43
+ env["BORG_RELOCATED_REPO_ACCESS_IS_OK"] = "yes"
44
+ env["BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK"] = "yes"
43
45
 
44
- result = system(env, *cmd)
46
+ # Redirect stdin from /dev/null to prevent interactive prompts
47
+ result = system(env, *cmd, in: "/dev/null")
45
48
  raise BorgError, "Borg command failed: #{cmd.join(' ')}" unless result
46
49
 
47
50
  result
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruborg
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/ruborg.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "ruborg/version"
4
+ require_relative "ruborg/logger"
4
5
  require_relative "ruborg/config"
5
6
  require_relative "ruborg/repository"
6
7
  require_relative "ruborg/backup"
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.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michail Pantelelis
@@ -115,10 +115,10 @@ files:
115
115
  - lib/ruborg/backup.rb
116
116
  - lib/ruborg/cli.rb
117
117
  - lib/ruborg/config.rb
118
+ - lib/ruborg/logger.rb
118
119
  - lib/ruborg/passbolt.rb
119
120
  - lib/ruborg/repository.rb
120
121
  - lib/ruborg/version.rb
121
- - ruborg.gemspec
122
122
  - ruborg.yml.example
123
123
  homepage: https://github.com/mpantel/ruborg
124
124
  licenses:
data/ruborg.gemspec DELETED
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/ruborg/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "ruborg"
7
- spec.version = Ruborg::VERSION
8
- spec.authors = ["Michail Pantelelis"]
9
- spec.email = ["mpantel@aegean.gr"]
10
-
11
- spec.summary = "A friendly Ruby frontend for Borg backup"
12
- spec.description = "Ruborg is a Ruby gem that provides a user-friendly interface to Borg backup. It reads YAML configuration files and orchestrates backup operations, supporting repository creation, backup management, and integration with Passbolt for encryption password management."
13
- spec.homepage = "https://github.com/mpantel/ruborg"
14
- spec.license = "MIT"
15
- spec.required_ruby_version = ">= 3.2.0"
16
-
17
- spec.metadata["homepage_uri"] = spec.homepage
18
- spec.metadata["source_code_uri"] = "#{spec.homepage}.git"
19
- spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
-
21
- # Specify which files should be added to the gem when it is released.
22
- spec.files = Dir.chdir(__dir__) do
23
- `git ls-files -z`.split("\x0").reject do |f|
24
- (File.expand_path(f) == __FILE__) ||
25
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
26
- end
27
- end
28
- spec.bindir = "exe"
29
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
- spec.require_paths = ["lib"]
31
-
32
- # Dependencies
33
- spec.add_dependency "thor", "~> 1.3"
34
- spec.add_dependency "psych", "~> 5.0"
35
-
36
- # Development dependencies
37
- spec.add_development_dependency "bundler", "~> 2.0"
38
- spec.add_development_dependency "rake", "~> 13.0"
39
- spec.add_development_dependency "rspec", "~> 3.0"
40
- spec.add_development_dependency "rubocop", "~> 1.0"
41
- end