ruborg 0.8.1 → 0.9.3

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: 74bbfee5ede7b87c5a99ddec057c90430ee66a5667e6a933455baf4478f569d2
4
- data.tar.gz: f7ddb9aa319573d39888fad0933de0ce99fcc08069eae459550605eba9ff1082
3
+ metadata.gz: 8f3c9f239a72ec2f321419eb6f4349c25fe4cec4ca5174c156e2f1d3fe84ec68
4
+ data.tar.gz: 4930dbbc8dc2d4e2040628a8c57e62d7bf3ac20ca58a0e7e363a649f6f8d6546
5
5
  SHA512:
6
- metadata.gz: 285d04844f53fc87e5af8b28d24fc9dfe410e1610e2c79845ea0b1a5ea8a7d60ed76c5df2f667068b0c2b55269870d9af2c30f64ed9f8efc63312d1f99acd60a
7
- data.tar.gz: e72be5e589955c1e3832a5f38236d485d61db23a8f60ece3a147a6ecf21eb1b66fd9e1eddbc77c4720c4dc69f675491eb4c7eb5dc22b93b2a1856757af4e7a96
6
+ metadata.gz: ba9bd867f83d24c88abf435f4b64368b9c414080290747f107a6b5e94db6e82ddba0a59b0f700a4625e689b54724aefceecccc56f5e51f3fc8271be74601bc5e
7
+ data.tar.gz: e95dd232d73c7c63968c0105170928d0ab28d689342ad875a81fbc30ac56d508179a1866b476b79125fe43a85fc69cc68baef14a45cd220019be5612ce458bea
data/CHANGELOG.md CHANGED
@@ -7,6 +7,91 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.3] - 2026-05-09
11
+
12
+ ### Added
13
+ - **`ruborg lock` command**: Check for and optionally break stale Borg repository locks
14
+ - `ruborg lock --repository NAME` — exits 0 if no lock, exits 1 if lock detected
15
+ - `ruborg lock --repository NAME --break --yes` — breaks the lock via `borg break-lock`
16
+ - `ruborg lock --repository NAME --force --yes` — force-removes lock files directly without invoking Borg (useful when Borg itself can't run)
17
+ - `--break` and `--force` are mutually exclusive; both require `--yes` as a safety guard
18
+ - `Repository#locked?` — pure filesystem check on `lock.exclusive` / `lock.roster`, no Borg invocation or passphrase required
19
+ - `Repository#break_lock` — delegates to `borg break-lock`; requires Borg >= 1.4.0
20
+ - `Repository#force_break_lock` — direct filesystem removal of lock files/dirs; no Borg needed
21
+ - Status output (lock present/absent) goes to stdout for scriptability; warning messages go to `$stderr`
22
+ - **Pre-flight lock detection during backup**: If a repository is locked when `backup` starts, ruborg waits and retries instead of failing immediately
23
+ - Polls every 5 seconds, prints elapsed time via the spinner
24
+ - Aborts with a clear error message after `lock_wait` seconds (default 300), suggesting `ruborg lock` to inspect or clear
25
+ - **`lock_wait` config key**: Optional integer (seconds). When set, also passed as `--lock-wait` to all Borg commands so Borg itself waits for mid-operation locks. Omitting the key leaves Borg at its own default (1 second)
26
+ - **Minimum Borg version**: Raised to 1.4.0; `break_lock` verifies this before invoking `borg break-lock`
27
+ - **`CLI::DEFAULT_LOCK_WAIT = 300`**: Named constant for the pre-flight wait timeout
28
+ - Fixes [#8](https://github.com/mpantel/ruborg/issues/8)
29
+
30
+ ## [0.9.2] - 2026-05-09
31
+
32
+ ### Added
33
+ - **CLI progress display**: Real-time feedback during backup operations
34
+ - Named stages printed to `$stderr`: `[1/3] Verifying repository`, `[2/3] Backing up files`, `[3/3] Pruning`
35
+ - Animated spinner for indeterminate operations (cache loading, `borg create`, pruning)
36
+ - Inline progress bar for per-file backup mode: `[=========> ] 42/120 filename.jpg`
37
+ - Stage count adapts to the operation: 2 stages without pruning, 3 with
38
+ - Degrades gracefully to plain text lines when output is piped or redirected (non-TTY)
39
+ - All progress output goes to `$stderr` — `--json` stdout and piped output remain clean
40
+ - No external gem dependencies — pure Ruby with ANSI `\r` rewrite
41
+ - Fixes [#6](https://github.com/mpantel/ruborg/issues/6)
42
+
43
+ ## [0.9.1] - 2026-05-09
44
+
45
+ ### Added
46
+ - **Archive metadata cache**: New `ArchiveCache` class eliminates N+1 `borg info` calls during per-file backup runs
47
+ - Metadata cached in `<repo_path>.ruborg_cache.json` — sibling file to the repository
48
+ - Cache is shared across machines: any host with access to the repo path shares the same cache
49
+ - Local repos use `File::LOCK_EX` for safe concurrent reads/writes with merge-on-conflict
50
+ - SSH repos (`user@host:/path`, `ssh://user@host/path`) fetch/push the cache via `scp` with optimistic locking (fetch-fresh → merge → push), avoiding deadlocks on process crashes
51
+ - Only archives not yet in cache trigger a `borg info` call; warm runs reduce subprocess overhead from O(n) to O(new archives)
52
+ - Fixes [#4](https://github.com/mpantel/ruborg/issues/4)
53
+ - **Catalog command**: New `ruborg catalog` command for fast, offline browsing of backed-up files
54
+ - Reads the local cache file — no `borg` subprocess calls needed
55
+ - `--search PATTERN` — filter entries by file path using a regex
56
+ - `--stats` — show aggregate statistics (total archives, unique files, total size, source dirs)
57
+ - `--json` — machine-readable JSON output; default is a human-friendly text table
58
+ - Works per-repository like all other commands (`--repository`)
59
+ - Supports SSH repos transparently via the same `scp`-based cache fetch
60
+ - **Bug fix**: `ArchiveCache` now normalises all loaded metadata to symbol keys, ensuring cache hits and cache misses return identical key types
61
+
62
+ ## [0.9.0] - 2025-10-14
63
+
64
+ ### Changed
65
+ - **Command Consolidation**: Unified validation commands for consistency
66
+ - `ruborg validate` renamed to `ruborg validate config` (validates YAML configuration)
67
+ - `ruborg check` renamed to `ruborg validate repo` (validates repository compatibility and integrity)
68
+ - Both commands now use consistent `validate` terminology
69
+ - `--verify-data` option remains available for `validate repo` to run full integrity checks
70
+ - Eliminates confusion between `validate` (config) and `check` (repository)
71
+ - Updated README with new command syntax and examples
72
+ - Updated all tests to use new command format
73
+
74
+ ### Added
75
+ - **Skip Hash Check Option**: New `skip_hash_check` configuration option for faster per-file backups
76
+ - Skips expensive SHA256 content hash calculation when file already exists
77
+ - Trusts file path, size, and modification time matching for duplicate detection
78
+ - Significantly speeds up backups with many unchanged files
79
+ - Configurable globally or per-repository
80
+ - Default: `false` (paranoid mode - always verify content hash)
81
+ - Use case: Large directories where mtime changes are reliable (most filesystems)
82
+ - Example: `skip_hash_check: true` in YAML configuration
83
+ - **Migration Help**: `ruborg check` now displays a helpful deprecation notice
84
+ - Shows clear message explaining the command has been renamed
85
+ - Provides examples of the new `ruborg validate repo` syntax
86
+ - Exits with error to prevent confusion
87
+ - Logs deprecation warning for audit trail
88
+ - **Enhanced Version Command**: `ruborg version` now shows both Ruborg and Borg versions with path
89
+ - Displays Ruborg version (gem version)
90
+ - Displays installed Borg version and executable path
91
+ - Example output: `borg 1.2.8 (/usr/local/bin/borg)`
92
+ - Gracefully handles missing Borg installation
93
+ - Helps users verify both tool versions and location at a glance
94
+
10
95
  ## [0.8.1] - 2025-10-09
11
96
 
12
97
  ### Added
data/README.md CHANGED
@@ -25,13 +25,15 @@ A friendly Ruby frontend for [Borg Backup](https://www.borgbackup.org/). Ruborg
25
25
  - 📈 **Summary View** - Quick overview of all repositories and their configurations
26
26
  - 🔧 **Custom Borg Path** - Support for custom Borg executable paths per repository
27
27
  - 🏠 **Hostname Validation** - NEW! Restrict backups to specific hosts (global or per-repository)
28
- - **Well-tested** - Comprehensive test suite with RSpec (294 examples, 0 failures)
28
+ - 🔒 **Lock Management** - Detect and break stale Borg repository locks with `ruborg lock`
29
+ - ⏳ **Lock-aware Backups** - Pre-flight lock detection with configurable wait timeout before backup
30
+ - ✅ **Well-tested** - Comprehensive test suite with RSpec (412 examples, 0 failures)
29
31
  - 🔒 **Security-focused** - Path validation, safe YAML loading, command injection protection
30
32
 
31
33
  ## Prerequisites
32
34
 
33
35
  - Ruby >= 3.2.0
34
- - [Borg Backup](https://www.borgbackup.org/) installed and available in PATH
36
+ - [Borg Backup](https://www.borgbackup.org/) >= 1.4.0 installed and available in PATH
35
37
  - [Passbolt CLI](https://github.com/passbolt/go-passbolt-cli) (optional, for password management)
36
38
 
37
39
  ### Installing Borg Backup
@@ -163,13 +165,15 @@ repositories:
163
165
 
164
166
  **Configuration Features:**
165
167
  - **Automatic Type Validation**: Configuration is validated on startup to catch type errors early
166
- - **Validation Command**: Run `ruborg validate` to check configuration files for errors
168
+ - **Validation Command**: Run `ruborg validate config` to check configuration files for errors
167
169
  - **Descriptions**: Add `description` field to document each repository's purpose
168
170
  - **Hostname Validation**: Optional `hostname` field to restrict backups to specific hosts (global or per-repository)
169
171
  - **Source Deletion Safety**: `allow_remove_source` flag to explicitly enable `--remove-source` option (default: disabled)
172
+ - **Skip Hash Check**: Optional `skip_hash_check` flag to skip content hash verification for faster backups (per-file mode only)
170
173
  - **Type-Safe Booleans**: Strict boolean validation prevents configuration errors (must use `true`/`false`, not strings)
171
- - **Global Settings**: Hostname, compression, encryption, auto_init, allow_remove_source, log_file, borg_path, borg_options, and retention apply to all repositories
172
- - **Per-Repository Overrides**: Any global setting can be overridden at the repository level (including hostname, allow_remove_source, and custom borg_path)
174
+ - **Lock Wait Timeout**: Optional `lock_wait` (integer, seconds) how long ruborg waits for a locked repository before aborting. Also passed as `--lock-wait` to Borg when set. Default: 300 seconds (pre-flight), Borg default: 1 second (when not configured)
175
+ - **Global Settings**: Hostname, compression, encryption, auto_init, allow_remove_source, skip_hash_check, lock_wait, log_file, borg_path, borg_options, and retention apply to all repositories
176
+ - **Per-Repository Overrides**: Any global setting can be overridden at the repository level (including hostname, allow_remove_source, skip_hash_check, lock_wait, and custom borg_path)
173
177
  - **Custom Borg Path**: Specify a custom Borg executable path if borg is not in PATH or to use a specific version
174
178
  - **Retention Policies**: Define how many backups to keep (hourly, daily, weekly, monthly, yearly)
175
179
  - **Multiple Sources**: Each repository can have multiple backup sources with their own exclude patterns
@@ -184,7 +188,7 @@ Ruborg automatically validates your configuration on startup. All commands check
184
188
  Check your configuration file for errors:
185
189
 
186
190
  ```bash
187
- ruborg validate --config ruborg.yml
191
+ ruborg validate config --config ruborg.yml
188
192
  ```
189
193
 
190
194
  **Validation checks:**
@@ -471,20 +475,52 @@ Group: postgres
471
475
  Type: regular file
472
476
  ```
473
477
 
474
- ### Check Repository Compatibility
478
+ ### Manage Repository Locks
479
+
480
+ Borg uses lock files to prevent concurrent access. If a backup crashes, stale locks can block all subsequent operations. Use `ruborg lock` to inspect and clear them.
481
+
482
+ ```bash
483
+ # Check if a repository is locked (exits 0 = no lock, 1 = locked)
484
+ ruborg lock --repository documents
485
+
486
+ # Break the lock via borg break-lock (requires Borg >= 1.4.0)
487
+ ruborg lock --repository documents --break --yes
488
+
489
+ # Force-remove lock files directly (no Borg required, last resort)
490
+ ruborg lock --repository documents --force --yes
491
+ ```
492
+
493
+ **Lock-aware backups:** When `ruborg backup` starts and detects a lock, it waits up to `lock_wait` seconds (default 300) for the lock to clear before aborting:
494
+
495
+ ```
496
+ [1/2] Verifying repository: documents
497
+ Repository locked — waiting for lock to clear (5s / 300s)…
498
+ Repository locked — waiting for lock to clear (10s / 300s)…
499
+ ✓ Lock cleared
500
+ [2/2] Creating archive
501
+ ```
502
+
503
+ Configure the timeout in `ruborg.yml` (also passed as `--lock-wait` to Borg when set):
504
+
505
+ ```yaml
506
+ # Wait up to 60s for a lock before aborting; also passes --lock-wait 60 to borg commands
507
+ lock_wait: 60
508
+ ```
509
+
510
+ ### Validate Repository Compatibility
475
511
 
476
512
  ```bash
477
513
  # Check specific repository compatibility with installed Borg version
478
- ruborg check --repository documents
514
+ ruborg validate repo --repository documents
479
515
 
480
516
  # Check all repositories
481
- ruborg check --all
517
+ ruborg validate repo --all
482
518
 
483
519
  # Check with data integrity verification (slower)
484
- ruborg check --repository documents --verify-data
520
+ ruborg validate repo --repository documents --verify-data
485
521
  ```
486
522
 
487
- The `check` command verifies:
523
+ The `validate repo` command verifies:
488
524
  - Installed Borg version
489
525
  - Repository format version
490
526
  - Compatibility between Borg and repository versions
@@ -494,11 +530,11 @@ The `check` command verifies:
494
530
  ```
495
531
  Borg version: 1.2.8
496
532
 
497
- --- Checking repository: documents ---
533
+ --- Validating repository: documents ---
498
534
  Repository version: 1
499
535
  ✓ Compatible with Borg 1.2.8
500
536
 
501
- --- Checking repository: databases ---
537
+ --- Validating repository: databases ---
502
538
  Repository version: 2
503
539
  ✗ INCOMPATIBLE with Borg 1.2.8
504
540
  Repository version 2 cannot be read by Borg 1.2.8
@@ -652,26 +688,27 @@ See [SECURITY.md](SECURITY.md) for detailed security information and best practi
652
688
  | Command | Description | Options |
653
689
  |---------|-------------|---------|
654
690
  | `init REPOSITORY` | Initialize a new Borg repository | `--passphrase`, `--passbolt-id`, `--log` |
655
- | `validate` | Validate configuration file for type errors | `--config`, `--log` |
691
+ | `validate config` | Validate configuration file for type errors | `--config`, `--log` |
692
+ | `validate repo` | Validate repository compatibility and integrity | `--config`, `--repository`, `--all`, `--verify-data`, `--log` |
656
693
  | `backup` | Create a backup using config file | `--config`, `--repository`, `--all`, `--name`, `--remove-source`, `--log` |
657
694
  | `list` | List archives or files in repository | `--config`, `--repository`, `--archive`, `--log` |
658
695
  | `restore ARCHIVE` | Restore files from archive | `--config`, `--repository`, `--destination`, `--path`, `--log` |
659
696
  | `metadata ARCHIVE` | Get file metadata from archive | `--config`, `--repository`, `--file`, `--log` |
697
+ | `lock` | Check for and optionally break a repository lock | `--config`, `--repository`, `--break`, `--force`, `--yes`, `--log` |
660
698
  | `info` | Show repository information | `--config`, `--repository`, `--log` |
661
- | `check` | Check repository integrity and compatibility | `--config`, `--repository`, `--all`, `--verify-data`, `--log` |
662
699
  | `version` | Show ruborg version | None |
663
700
 
664
701
  ### Options
665
702
 
666
703
  - `--config`: Path to configuration file (default: `ruborg.yml`)
667
704
  - `--log`: Path to log file (overrides config, default: `~/.ruborg/logs/ruborg.log`)
668
- - `--repository` / `-r`: Repository name (optional for info, required for backup/list/restore/check unless --all)
669
- - `--all`: Process all repositories (backup and check commands)
705
+ - `--repository` / `-r`: Repository name (optional for info, required for backup/list/restore/validate repo unless --all)
706
+ - `--all`: Process all repositories (backup and validate repo commands)
670
707
  - `--name`: Custom archive name (backup command only)
671
708
  - `--remove-source`: Remove source files after successful backup (backup command only)
672
709
  - `--destination`: Destination directory for restore (restore command only)
673
710
  - `--path`: Specific file or directory to restore (restore command only)
674
- - `--verify-data`: Run full data integrity check (check command only, slower)
711
+ - `--verify-data`: Run full data integrity check (validate repo command only, slower)
675
712
 
676
713
  ## Retention Policies
677
714
 
@@ -822,6 +859,60 @@ repositories:
822
859
 
823
860
  **Backup vs Retention:** The per-file `retention_mode` only affects how archives are created and pruned. Traditional backup commands still work normally - you can list, restore, and check per-file archives just like standard archives.
824
861
 
862
+ ### Skip Hash Check for Faster Backups
863
+
864
+ **NEW:** In per-file backup mode, you can optionally skip content hash verification for faster duplicate detection:
865
+
866
+ ```yaml
867
+ repositories:
868
+ - name: project-files
869
+ path: /mnt/backup/project-files
870
+ retention_mode: per_file
871
+ skip_hash_check: true # Skip SHA256 content hash verification
872
+ sources:
873
+ - name: projects
874
+ paths:
875
+ - /home/user/projects
876
+ ```
877
+
878
+ **How it works:**
879
+ - **Default (paranoid mode)**: Ruborg calculates SHA256 hash of file content to verify files haven't changed (even when size and mtime are identical)
880
+ - **With skip_hash_check: true**: Ruborg trusts file path, size, and modification time for duplicate detection (skips hash calculation)
881
+
882
+ **When to use:**
883
+ - ✅ **Large directories** with thousands of files where hash calculation is slow
884
+ - ✅ **Reliable filesystems** where modification time changes are trustworthy
885
+ - ✅ **Regular backups** where files are unlikely to be manually modified with `touch -t`
886
+
887
+ **When NOT to use:**
888
+ - ❌ **Security-critical data** where you want maximum verification
889
+ - ❌ **Untrusted sources** where files might be tampered with
890
+ - ❌ **Systems with unreliable mtime** (rare, but some network filesystems)
891
+
892
+ **Performance impact:**
893
+ ```yaml
894
+ # Example: 10,000 unchanged files, average 50KB each
895
+ # With skip_hash_check: false (default) - ~30 seconds (read + hash all files)
896
+ # With skip_hash_check: true - ~3 seconds (read metadata only)
897
+ ```
898
+
899
+ **Console output:**
900
+ ```
901
+ # With skip_hash_check: true
902
+ [1/10000] Backing up: /home/user/file1.txt - Archive already exists (skipped hash check)
903
+ [2/10000] Backing up: /home/user/file2.txt - Archive already exists (skipped hash check)
904
+ ...
905
+ ✓ Per-file backup completed: 50 file(s) backed up, 9950 skipped (hash check skipped)
906
+
907
+ # With skip_hash_check: false (default)
908
+ [1/10000] Backing up: /home/user/file1.txt - Archive already exists (file unchanged)
909
+ [2/10000] Backing up: /home/user/file2.txt - Archive already exists (file unchanged)
910
+ ...
911
+ ✓ Per-file backup completed: 50 file(s) backed up, 9950 skipped (unchanged)
912
+ ```
913
+
914
+ **Security note:** Even with `skip_hash_check: true`, files are still verified by path, size, and mtime. The only difference is skipping the SHA256 content hash verification, which catches rare edge cases like manual file tampering with preserved timestamps.
915
+
825
916
  ### Automatic Pruning
826
917
 
827
918
  Enable **automatic pruning** to remove old backups after each backup operation:
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+ require "tempfile"
6
+
7
+ module Ruborg
8
+ # Persistent cache of per-archive metadata, stored as a JSON file sibling to the
9
+ # Borg repository. Eliminates repeated `borg info` calls across runs.
10
+ #
11
+ # Supports local paths (File::LOCK_EX) and SSH paths (optimistic merge via scp).
12
+ # All metadata is stored and returned with symbol keys (:path, :size, :hash, :source_dir).
13
+ class ArchiveCache
14
+ SSH_PATTERN = %r{\A(?:ssh://|[^\s/]+@[^\s:]+:)}
15
+
16
+ def initialize(repo_path)
17
+ @repo_path = repo_path
18
+ @data = {}
19
+ @snapshot = {}
20
+ @loaded = false
21
+ end
22
+
23
+ def fetch
24
+ return self if @loaded
25
+
26
+ if ssh?
27
+ load_remote
28
+ else
29
+ load_local
30
+ end
31
+
32
+ @snapshot = snapshot(@data)
33
+ @loaded = true
34
+ self
35
+ end
36
+
37
+ def [](archive_name)
38
+ @data[archive_name]
39
+ end
40
+
41
+ def store(archive_name, metadata)
42
+ @data[archive_name] = symbolize(metadata)
43
+ end
44
+
45
+ # Returns all cached entries as an array of hashes, each including :archive_name.
46
+ def entries
47
+ @data.map { |archive_name, metadata| metadata.merge(archive_name: archive_name) }
48
+ end
49
+
50
+ def save_if_changed
51
+ return unless dirty?
52
+
53
+ if ssh?
54
+ save_remote
55
+ else
56
+ save_local
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def dirty?
63
+ @data != @snapshot
64
+ end
65
+
66
+ def snapshot(hash)
67
+ hash.transform_values(&:dup)
68
+ end
69
+
70
+ def ssh?
71
+ SSH_PATTERN.match?(@repo_path)
72
+ end
73
+
74
+ def cache_path_for(path)
75
+ "#{path}.ruborg_cache.json"
76
+ end
77
+
78
+ def symbolize(metadata)
79
+ metadata.transform_keys(&:to_sym)
80
+ end
81
+
82
+ def normalize_archives(raw)
83
+ (raw || {}).transform_values { |v| symbolize(v) }
84
+ end
85
+
86
+ def load_local
87
+ path = cache_path_for(@repo_path)
88
+ return unless File.exist?(path)
89
+
90
+ File.open(path, "r") do |f|
91
+ f.flock(File::LOCK_SH)
92
+ parsed = JSON.parse(f.read)
93
+ @data = normalize_archives(parsed["archives"])
94
+ end
95
+ rescue JSON::ParserError
96
+ @data = {}
97
+ end
98
+
99
+ def save_local
100
+ path = cache_path_for(@repo_path)
101
+ File.open(path, File::RDWR | File::CREAT, 0o600) do |f|
102
+ f.flock(File::LOCK_EX)
103
+ existing = read_existing_local(f)
104
+ merged = existing.merge(@data)
105
+ f.rewind
106
+ f.write(JSON.generate({ "version" => 1, "archives" => stringify_for_storage(merged) }))
107
+ f.truncate(f.pos)
108
+ end
109
+ end
110
+
111
+ def read_existing_local(file)
112
+ content = file.read
113
+ return {} if content.empty?
114
+
115
+ normalize_archives(JSON.parse(content)["archives"])
116
+ rescue JSON::ParserError
117
+ {}
118
+ end
119
+
120
+ # JSON requires string keys; convert symbol keys back before writing.
121
+ def stringify_for_storage(data)
122
+ data.transform_values { |v| v.transform_keys(&:to_s) }
123
+ end
124
+
125
+ def parse_ssh
126
+ if @repo_path.start_with?("ssh://")
127
+ require "uri"
128
+ uri = URI.parse(@repo_path)
129
+ host = uri.user ? "#{uri.user}@#{uri.host}" : uri.host
130
+ host = "#{host}:#{uri.port}" if uri.port && uri.port != 22
131
+ [host, uri.path]
132
+ else
133
+ match = @repo_path.match(%r{\A([^\s/]+@[^\s:]+):(.+)\z})
134
+ return [nil, nil] unless match
135
+
136
+ [match[1], match[2]]
137
+ end
138
+ end
139
+
140
+ def load_remote
141
+ host, path = parse_ssh
142
+ return unless host
143
+
144
+ remote = "#{host}:#{cache_path_for(path)}"
145
+ loaded = nil
146
+ Tempfile.create(["ruborg_cache", ".json"]) do |tmp|
147
+ _, status = Open3.capture2e("scp", "-q", "-B", remote, tmp.path)
148
+ next unless status.success?
149
+
150
+ begin
151
+ loaded = normalize_archives(JSON.parse(File.read(tmp.path))["archives"])
152
+ rescue JSON::ParserError
153
+ loaded = {}
154
+ end
155
+ end
156
+ @data = loaded if loaded
157
+ end
158
+
159
+ def save_remote
160
+ host, path = parse_ssh
161
+ return unless host
162
+
163
+ remote = "#{host}:#{cache_path_for(path)}"
164
+ fresh = fetch_remote_fresh(remote)
165
+ merged = fresh.merge(@data)
166
+
167
+ Tempfile.create(["ruborg_cache_upload", ".json"]) do |tmp|
168
+ tmp.write(JSON.generate({ "version" => 1, "archives" => stringify_for_storage(merged) }))
169
+ tmp.flush
170
+ Open3.capture2e("scp", "-q", "-B", tmp.path, remote)
171
+ end
172
+ end
173
+
174
+ def fetch_remote_fresh(remote)
175
+ result = {}
176
+ Tempfile.create(["ruborg_cache_fresh", ".json"]) do |tmp|
177
+ _, status = Open3.capture2e("scp", "-q", "-B", remote, tmp.path)
178
+ next unless status.success?
179
+
180
+ begin
181
+ result = normalize_archives(JSON.parse(File.read(tmp.path))["archives"])
182
+ rescue JSON::ParserError
183
+ result = {}
184
+ end
185
+ end
186
+ result
187
+ end
188
+ end
189
+ end