easy_sync 0.0.5 → 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.
- checksums.yaml +5 -5
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +85 -0
- data/CLAUDE.md +133 -0
- data/Gemfile +2 -0
- data/README.md +607 -21
- data/Rakefile +6 -8
- data/bin/easy_sync +8 -1
- data/docs/fine-placement.md +174 -0
- data/docs/hardware-notes.md +71 -0
- data/docs/integrity-scan.md +435 -0
- data/docs/parallel-scrub.md +280 -0
- data/docs/performance.md +146 -0
- data/docs/tripwire.md +232 -0
- data/easy_sync.gemspec +25 -16
- data/lib/easy_sync/cli.rb +1246 -0
- data/lib/easy_sync/config.rb +202 -0
- data/lib/easy_sync/error.rb +5 -0
- data/lib/easy_sync/jbod/backblaze.rb +113 -0
- data/lib/easy_sync/jbod/benchmarker.rb +149 -0
- data/lib/easy_sync/jbod/cleaner.rb +93 -0
- data/lib/easy_sync/jbod/dashboard.rb +611 -0
- data/lib/easy_sync/jbod/keep_awake.rb +33 -0
- data/lib/easy_sync/jbod/manifest.rb +1068 -0
- data/lib/easy_sync/jbod/mirror.rb +204 -0
- data/lib/easy_sync/jbod/models.rb +165 -0
- data/lib/easy_sync/jbod/page_cache.rb +56 -0
- data/lib/easy_sync/jbod/placement.rb +71 -0
- data/lib/easy_sync/jbod/planner.rb +76 -0
- data/lib/easy_sync/jbod/purger.rb +161 -0
- data/lib/easy_sync/jbod/restorer.rb +142 -0
- data/lib/easy_sync/jbod/run_lock.rb +98 -0
- data/lib/easy_sync/jbod/run_log.rb +57 -0
- data/lib/easy_sync/jbod/runner.rb +615 -0
- data/lib/easy_sync/jbod/scrub_pool.rb +136 -0
- data/lib/easy_sync/jbod/scrubber.rb +321 -0
- data/lib/easy_sync/jbod/share_scan.rb +26 -0
- data/lib/easy_sync/jbod/sync_eta.rb +68 -0
- data/lib/easy_sync/jbod/tripwire.rb +80 -0
- data/lib/easy_sync/jbod/volume_info.rb +283 -0
- data/lib/easy_sync/jbod.rb +35 -0
- data/lib/easy_sync/shell.rb +62 -0
- data/lib/easy_sync/templates/dashboard.html.erb +419 -0
- data/lib/easy_sync/version.rb +3 -1
- data/lib/easy_sync.rb +7 -3
- metadata +84 -45
- data/Gemfile.lock +0 -55
- data/Guardfile +0 -28
- data/lib/easy_sync/rsync.rb +0 -60
- data/lib/easy_sync/sync_logger.rb +0 -20
- data/lib/easy_sync/sync_runner.rb +0 -44
- data/spec/easy_sync/rsync_spec.rb +0 -25
- data/spec/easy_sync/sync_runner_spec.rb +0 -54
- data/spec/spec_helper.rb +0 -33
- data/spec/support.rb +0 -89
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7b15e5fef6a770d80d2737ccc438d50e5df901703cbf4346b60dcfa2461f2f79
|
|
4
|
+
data.tar.gz: a9162791c01351dcb3121a324cf9b43d3eb2bca6b0cf723945004f44d027d424
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e289ada3ee924c73d9df8f850ba6085d935453238c8e074cf4ee39790915183e684420b38b13749f76a54648be23272ad6fda4654d59224e843f7232a053f856
|
|
7
|
+
data.tar.gz: '011189cabcb91bd44513da0ffaa2e45c0a7b16bed2455fd112a6db8e96cec3e81d9a2a6aba5ece7593777b33fd74cc0e37d2c9f7389a28c451727cfcb56f0a49'
|
data/.gitignore
CHANGED
data/.rspec
ADDED
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.
|