easy_sync 0.0.1 → 2.0.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.
Files changed (57) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +8 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +85 -0
  6. data/CLAUDE.md +133 -0
  7. data/Gemfile +2 -0
  8. data/README.md +607 -21
  9. data/Rakefile +6 -8
  10. data/bin/easy_sync +8 -1
  11. data/docs/fine-placement.md +174 -0
  12. data/docs/hardware-notes.md +71 -0
  13. data/docs/integrity-scan.md +435 -0
  14. data/docs/parallel-scrub.md +280 -0
  15. data/docs/performance.md +146 -0
  16. data/docs/tripwire.md +232 -0
  17. data/easy_sync.gemspec +25 -16
  18. data/lib/easy_sync/cli.rb +1246 -0
  19. data/lib/easy_sync/config.rb +202 -0
  20. data/lib/easy_sync/error.rb +5 -0
  21. data/lib/easy_sync/jbod/backblaze.rb +113 -0
  22. data/lib/easy_sync/jbod/benchmarker.rb +149 -0
  23. data/lib/easy_sync/jbod/cleaner.rb +93 -0
  24. data/lib/easy_sync/jbod/dashboard.rb +611 -0
  25. data/lib/easy_sync/jbod/keep_awake.rb +33 -0
  26. data/lib/easy_sync/jbod/manifest.rb +1068 -0
  27. data/lib/easy_sync/jbod/mirror.rb +204 -0
  28. data/lib/easy_sync/jbod/models.rb +165 -0
  29. data/lib/easy_sync/jbod/page_cache.rb +56 -0
  30. data/lib/easy_sync/jbod/placement.rb +71 -0
  31. data/lib/easy_sync/jbod/planner.rb +76 -0
  32. data/lib/easy_sync/jbod/purger.rb +161 -0
  33. data/lib/easy_sync/jbod/restorer.rb +142 -0
  34. data/lib/easy_sync/jbod/run_lock.rb +98 -0
  35. data/lib/easy_sync/jbod/run_log.rb +57 -0
  36. data/lib/easy_sync/jbod/runner.rb +615 -0
  37. data/lib/easy_sync/jbod/scrub_pool.rb +136 -0
  38. data/lib/easy_sync/jbod/scrubber.rb +321 -0
  39. data/lib/easy_sync/jbod/share_scan.rb +26 -0
  40. data/lib/easy_sync/jbod/sync_eta.rb +68 -0
  41. data/lib/easy_sync/jbod/tripwire.rb +80 -0
  42. data/lib/easy_sync/jbod/volume_info.rb +283 -0
  43. data/lib/easy_sync/jbod.rb +35 -0
  44. data/lib/easy_sync/shell.rb +62 -0
  45. data/lib/easy_sync/templates/dashboard.html.erb +419 -0
  46. data/lib/easy_sync/version.rb +3 -1
  47. data/lib/easy_sync.rb +7 -3
  48. metadata +84 -45
  49. data/Gemfile.lock +0 -56
  50. data/Guardfile +0 -28
  51. data/lib/easy_sync/rsync.rb +0 -46
  52. data/lib/easy_sync/sync_logger.rb +0 -20
  53. data/lib/easy_sync/sync_runner.rb +0 -44
  54. data/spec/easy_sync/rsync_spec.rb +0 -25
  55. data/spec/easy_sync/sync_runner_spec.rb +0 -54
  56. data/spec/spec_helper.rb +0 -33
  57. data/spec/support.rb +0 -89
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: cea7195f628ae1bb09b15c6fc14d8a97217d20f8
4
- data.tar.gz: 6bdd4d0fb5a0ad7149196b817aadcc5d367d2a7d
2
+ SHA256:
3
+ metadata.gz: 7b15e5fef6a770d80d2737ccc438d50e5df901703cbf4346b60dcfa2461f2f79
4
+ data.tar.gz: a9162791c01351dcb3121a324cf9b43d3eb2bca6b0cf723945004f44d027d424
5
5
  SHA512:
6
- metadata.gz: 9977a20cc1c688e8e5d1767c4b8533643da3b071f990d59ba6b037bb95f76d3f70844b741ad28aea86316fef7899ac3441390eed7377a7173be95384135143f2
7
- data.tar.gz: 22bac584e4ba65fad6d642aaf758b401864b6d615697af1b28b25c00bde5705bea7ef8c2ad7d6ec99723e39b6bfdfe2cde36be9d0d2744b839f44e0564636b45
6
+ metadata.gz: e289ada3ee924c73d9df8f850ba6085d935453238c8e074cf4ee39790915183e684420b38b13749f76a54648be23272ad6fda4654d59224e843f7232a053f856
7
+ data.tar.gz: '011189cabcb91bd44513da0ffaa2e45c0a7b16bed2455fd112a6db8e96cec3e81d9a2a6aba5ece7593777b33fd74cc0e37d2c9f7389a28c451727cfcb56f0a49'
data/.gitignore CHANGED
@@ -15,3 +15,11 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .rspec_status
19
+ *.sqlite3
20
+ *.db
21
+ spec/test_data*
22
+ *.swp
23
+ *.swo
24
+ .DS_Store
25
+ CLAUDE.local.md
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --color
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 4.0.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,85 @@
1
+ # Changelog
2
+
3
+ Major features only, per released version. Commit history has the rest.
4
+
5
+ ## 2.0.0 (2026-09-26)
6
+
7
+ A rewrite. easy_sync no longer takes snapshots of one folder: it mirrors NAS
8
+ shares onto a set of independent drives (JBOD) and tracks where every folder
9
+ lives. Nothing from 0.0.x carries over; see "Breaking changes".
10
+
11
+ ### Backup
12
+
13
+ - **Folder-level placement across drives.** Each top-level folder of a share
14
+ goes to one drive, plus one unit for the share's loose top-level files. A
15
+ share stays on one drive while it fits and spreads out only when it must.
16
+ Placed folders never move on their own.
17
+ - **SQLite manifest** (`~/.easy_sync/manifest.sqlite3`) of drives, folders,
18
+ sync runs and placement history, with a copy on every drive.
19
+ - **Drives matched by serial number**, never by mount path. Unknown or retired
20
+ volumes are never written to.
21
+ - **Deletions with a grace period.** rsync never deletes; a file gone from the
22
+ NAS is removed from its drive only after `grace_days` and `grace_runs`.
23
+ - **Ransomware tripwire** that notices a sync about to replace or remove far
24
+ more existing files than normal. It only reports for now; enforcing it is
25
+ opt-in (`tripwire_enforce`).
26
+ - **"Not backed up" inventory**: every folder on the NAS is shown as placed,
27
+ not backed up (no drive has room) or empty, even when a copy is interrupted.
28
+ - **`restore`** copies folders back onto the NAS from their drives, never
29
+ deleting anything.
30
+
31
+ ### Drive health
32
+
33
+ - **SMART health** via `smartctl` (falling back to `diskutil`), with
34
+ reallocated-sector trends so old, stable wear isn't reported as failure.
35
+ `verify-drive` records a clean full-surface scan as a new baseline.
36
+ - **`scrub`**: bit-rot detection that reads files back off the platter and
37
+ checks them against stored checksums, several drives at once
38
+ (`--jobs`), within a time budget (`--for 8h`). The next sync re-copies
39
+ flagged files.
40
+ - **`benchmark`**: each drive's write and read speed over time, flagging a
41
+ drive that has slowed down.
42
+
43
+ ### Managing drives and shares
44
+
45
+ - `add-source`, `remove-source` and `sources`: the tool writes the config.
46
+ - `register-drive`, `rename-drive`, `replace-drive`, `forget-drive`.
47
+ - `reassign` moves a folder or a whole share to another drive (`--copy`
48
+ copies it drive-to-drive).
49
+ - `plan` measures shares and checks every folder fits a drive.
50
+ - `eject` ejects every connected drive so the enclosure can be powered off
51
+ between syncs, and says when to connect them again for Backblaze. It asks
52
+ before ejecting a drive Backblaze hasn't finished uploading.
53
+ - `clean` removes excluded junk (`.DS_Store`, `#recycle`, ...) from the drives.
54
+
55
+ ### Reporting
56
+
57
+ - **HTML dashboard**: a one-line verdict, drive tiles coloured by SMART health
58
+ (never by fullness), sync history, pending deletions, scrub findings and an
59
+ activity feed.
60
+ - **`status`**: drives, health and folders in the terminal, including a
61
+ running sync's progress and time remaining.
62
+ - **Backblaze upload status** per drive in `status` and on the dashboard, read
63
+ from Backblaze Personal's local state: shows whether each drive is safe to
64
+ power off.
65
+ - A log per sync and scrub under `~/.easy_sync/logs/`.
66
+
67
+ ### Safety
68
+
69
+ - `--dry-run` writes nothing, anywhere.
70
+ - One long run at a time: sync, scrub, restore, clean, benchmark and
71
+ `reassign --copy` share a lock.
72
+ - Ctrl-C stops the running rsync cleanly, and an interrupted copy picks up
73
+ where it left off on the next run instead of starting the file over.
74
+ - The Mac is kept awake during long runs (`caffeinate`).
75
+
76
+ ### Breaking changes
77
+
78
+ - macOS only (10.13 or later); Ruby 3.3 or later; rsync 3.0 or later.
79
+ - Snapshot mode is gone.
80
+ - The config moved to `~/.easy_sync/config.yml`, and its format changed.
81
+ `~/.easy_syncrc.yml` is not migrated.
82
+
83
+ ## 0.0.5 (2014-12-28)
84
+
85
+ - Incremental rsync snapshots of a folder, keeping the last 5.
data/CLAUDE.md ADDED
@@ -0,0 +1,133 @@
1
+ # easy_sync: notes for whoever works on this next
2
+
3
+ macOS-only Ruby gem (2.0.0) that mirrors NAS shares onto independent drives.
4
+ README.md is the user-facing truth; this file is for working on the code.
5
+ Feature designs, real-hardware results and measurements live in `docs/`
6
+ (map at the bottom).
7
+
8
+ ## Run the suite before every push
9
+
10
+ bundle exec rake # a few seconds, 500+ examples, no drives or rsync needed
11
+
12
+ - Every external call (rsync, df, du, diskutil, smartctl, caffeinate) goes through
13
+ `EasySync::Shell`; specs inject `FakeShell` (spec/support/fake_shell.rb) and
14
+ register responses with `fake_shell.on(...)`. Never let a spec shell out for real.
15
+ - spec_helper redirects the home directory (`Config::HOME_DIR`) and the
16
+ Backblaze install (`Backblaze::DATA_DIR`) into the temp dir. Keep it that
17
+ way: an earlier test found the developer's real config and moved it into a
18
+ temp dir that was then deleted. Any new default path must be derived at call
19
+ time (`Config.defaults`), never at load time.
20
+ - Specs use paths under `temp_dir`, never literal `/Volumes/...` (that broke on a
21
+ real Mac where `/Volumes` is not writable).
22
+
23
+ ## Before touching rsync flags, diskutil/smartctl parsing, `Shell`, the schema, scrub's reads or Backblaze
24
+
25
+ Read docs/hardware-notes.md. Each entry there was learned on real drives or
26
+ tools and would be expensive to re-derive (rsync 3.5's `--exclude=/*/` and
27
+ `--max-delete` behaviour, smartctl exit codes, the manifest's stale
28
+ `user_version` 5, `F_NOCACHE` vs. the page cache, signalling a process
29
+ group, `diskutil eject` output, Backblaze's state files).
30
+
31
+ ## Invariants (never break these)
32
+
33
+ - rsync never deletes. Only `Purger` deletes, only after `grace_days` AND
34
+ `grace_runs`, only inside the folder's own destination on a mounted drive.
35
+ It never `rm_rf`s a whole folder that overlaps a live folder on the same
36
+ drive (one below it, or a `tree` folder above it), and a `root` folder's
37
+ whole-folder purge removes only its top-level files.
38
+ `sync`'s refetch overwrites flagged files via `rsync -I`; `scrub` is
39
+ read-only on the drive.
40
+ - A placed folder never moves automatically. No rebalancing (Backblaze backs
41
+ up from the drives, so a moved folder is uploaded again). A folder changes
42
+ drive only through `reassign` (`--copy` copies drive-to-drive).
43
+ - Placement units: one per top-level folder of a share, plus one `root` unit
44
+ (keyed by the share name) for its loose top-level files. A new unit goes to
45
+ a drive already holding part of its share if it fits there. Nothing splits
46
+ a single top-level folder further: one bigger than every drive stays
47
+ unplaced ("not backed up"). See docs/fine-placement.md.
48
+ - Drives are matched by the serial in `<drive>/.easy_sync/drive.json`, never by
49
+ mount path. Unknown or retired volumes are never written to.
50
+ - A missing or empty share is skipped, never mirrored.
51
+ - A folder with no real file on the NAS (only hidden/excluded names) is never
52
+ placed (`Runner#empty_source?`); it shows in the report as `empty`.
53
+ - Placement always leaves `reserve` bytes free on a drive (config `:reserve:`,
54
+ default 2gb) for APFS metadata, the `.easy_sync/` state copies, and rsync's
55
+ temp file mid-copy.
56
+ - **`--dry-run` writes nothing, anywhere**: no folder assignment, no
57
+ `sync_runs`/`pending_deletions`/`source_inventory` row, no drive usage or
58
+ health update, no dashboard, no `.easy_sync/` copy to any drive. It was
59
+ violated once and found only by reading the dashboard after a real dry run.
60
+ If you touch `Runner#run`, grep it for `unless @dry_run` and guard every new
61
+ write site; specs must assert the manifest is unchanged.
62
+ - A `sync` decides every placement first (phase 1: measure, place, record the
63
+ full `source_inventory` for every folder seen on the NAS), *then* copies
64
+ (phase 2). That is why an interrupted 3-day copy still leaves an accurate
65
+ "what's backed up vs not" picture. Don't collapse the phases into one loop.
66
+ - Tile colour on the dashboard means SMART health, never fullness. A JBOD
67
+ drive at 97% used is working as intended.
68
+ - The dashboard's "not backed up" count (from `source_inventory`) is the
69
+ number the user acts on: it says whether to buy another drive. It once
70
+ silently disappeared (the dashboard only knew *placed* folders); don't let a
71
+ change hide it again.
72
+ - Anything that shells out is injectable and faked in specs.
73
+ - Sync is sequential on purpose: one rsync, one folder at a time. The source is
74
+ one NAS behind one link, so parallel rsyncs would contend, not add
75
+ throughput. `sync` must keep its aggregate at 50 MB/s or more
76
+ (docs/performance.md).
77
+ - `restore` never passes `--delete`: it only adds/updates files on the NAS. It
78
+ resolves a folder's NAS destination from the *current* `:sources:` by share
79
+ name, so a folder whose share was `remove-source`d is skipped with an error,
80
+ never guessed at.
81
+ - Backblaze is optional: everything about it (`status`/dashboard upload state,
82
+ the 30-day disconnect countdown, `eject`'s prompt) appears only when
83
+ `Jbod::Backblaze.read` finds an install.
84
+
85
+ ## Verify live when you touch the sync path (or anything in hardware-notes)
86
+
87
+ Two 250 GB USB drives (`jbod-test-1`, `jbod-test-2`, APFS encrypted; the
88
+ passphrase is in the gitignored `CLAUDE.local.md`) exist for this. Use a
89
+ scratch config via `--config` so the real `~/.easy_sync/` is never touched,
90
+ and a scratch source tree under the scratchpad. Several bugs here were only
91
+ visible on real hardware. Record what you learn in docs/hardware-notes.md.
92
+
93
+ ## Repo / process notes
94
+
95
+ - Public repo, solo maintainer. `main` blocks force-push and deletion; no
96
+ required reviews (they would only gate the owner). Work on a branch and open
97
+ a PR; the owner squash-merges.
98
+ - The repo is public: never commit real drive serials, volume UUIDs, folder
99
+ names from the library, or passphrases, not even in a comment, a spec or a
100
+ doc. Use made-up ones (`SN-backup-04-8tb`, `movies/Metropolis (1927)`).
101
+ Drive names (`backup-0N-Xtb`), share names, models and fleet-wide counts
102
+ are fine. Older commits still hold five serials and the test drives'
103
+ passphrase; history was deliberately not rewritten.
104
+ - The README's dashboard screenshots come from
105
+ `bundle exec ruby script/dashboard_screenshot.rb` (a made-up manifest and
106
+ Backblaze install). Re-run it and commit the PNGs when the dashboard's look
107
+ changes.
108
+ - A slow `commit && push` can be auto-backgrounded and look finished when it
109
+ isn't: `git log origin/<branch>..HEAD` must be empty before calling it done.
110
+ - 2.0.0 dropped all 1.x compatibility on purpose (snapshot mode, nested
111
+ config, `~/.easy_syncrc.yml` migration, `easy_sync jbod` alias, old markers,
112
+ pre-2.0 schema upgrades). Don't re-add any of it unless asked.
113
+ - The tool writes its own config (`Config#save`): `add-source` and
114
+ `remove-source` rewrite `~/.easy_sync/config.yml`, preserving comments and
115
+ order. A user never has to hand-edit YAML.
116
+ - Release: `bundle exec rake release` builds, tags `v2.0.0`, pushes the tag
117
+ and publishes. Until it has run, `gem install easy_sync` gets 0.0.5
118
+ (`git tag -l v2.0.0` tells you which). Date the CHANGELOG entry first.
119
+
120
+ ## Where things are documented
121
+
122
+ - docs/hardware-notes.md: traps found on real drives, rsync and macOS tools.
123
+ - docs/fine-placement.md: folder-by-folder placement, `reassign --copy`, the
124
+ Purger overlap guard, and its real-hardware results.
125
+ - docs/tripwire.md: the ransomware tripwire, report-only until
126
+ `tripwire_enforce`, and what it needs measured before enforcing.
127
+ - docs/integrity-scan.md: `scrub` (bit-rot detection), its real-hardware
128
+ results, the fleet's first full pass, and the Backblaze disconnect warning.
129
+ - docs/parallel-scrub.md: `scrub --jobs N`.
130
+ - docs/performance.md: sync throughput, drive, enclosure and hash speeds,
131
+ `benchmark` results. The NAS limits sync speed, never the drives; Time
132
+ Machine on the same Synology is the first suspect when a sync is slow.
133
+ - CHANGELOG.md: major features per release.
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gemspec