hbkit 0.4.7__tar.gz → 0.5.0__tar.gz
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.
- hbkit-0.5.0/CHANGELOG.md +46 -0
- {hbkit-0.4.7 → hbkit-0.5.0}/FORMAT.md +69 -1
- {hbkit-0.4.7 → hbkit-0.5.0}/PKG-INFO +90 -6
- {hbkit-0.4.7 → hbkit-0.5.0}/README.md +89 -5
- {hbkit-0.4.7 → hbkit-0.5.0}/pyproject.toml +2 -2
- {hbkit-0.4.7 → hbkit-0.5.0}/src/hbkit/__init__.py +1 -1
- {hbkit-0.4.7 → hbkit-0.5.0}/src/hbkit/archive.py +201 -6
- hbkit-0.5.0/src/hbkit/cli.py +621 -0
- {hbkit-0.4.7 → hbkit-0.5.0}/src/hbkit/doctor.py +40 -10
- {hbkit-0.4.7 → hbkit-0.5.0}/src/hbkit/index.py +55 -8
- {hbkit-0.4.7 → hbkit-0.5.0}/src/hbkit/manifest.py +28 -4
- {hbkit-0.4.7 → hbkit-0.5.0}/src/hbkit/runner.py +4 -2
- {hbkit-0.4.7 → hbkit-0.5.0}/src/hbkit/tui.py +30 -5
- hbkit-0.5.0/tests/synth.py +202 -0
- hbkit-0.5.0/tests/test_hbkit.py +1563 -0
- hbkit-0.4.7/src/hbkit/cli.py +0 -345
- hbkit-0.4.7/tests/test_hbkit.py +0 -629
- {hbkit-0.4.7 → hbkit-0.5.0}/.gitignore +0 -0
- {hbkit-0.4.7 → hbkit-0.5.0}/LICENSE +0 -0
- {hbkit-0.4.7 → hbkit-0.5.0}/contrib/art/README.md +0 -0
- {hbkit-0.4.7 → hbkit-0.5.0}/packaging/README.md +0 -0
- {hbkit-0.4.7 → hbkit-0.5.0}/src/hbkit/crypto.py +0 -0
- {hbkit-0.4.7 → hbkit-0.5.0}/src/hbkit/mount.py +0 -0
hbkit-0.5.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### New
|
|
6
|
+
|
|
7
|
+
- **Backup versions.** `list`, `get`, `verify`, `info`, `doctor` and `tui` read any
|
|
8
|
+
version: `--version N`, `--version @YYYY-MM-DD[ HH:MM[:SS]]` / `--before '…'` (newest
|
|
9
|
+
complete version that started no later than that local time), `--allow-incomplete`.
|
|
10
|
+
The default is still the newest version, but now the newest *complete* one, read for
|
|
11
|
+
every share alike.
|
|
12
|
+
- **`hbk <archive> versions [--porcelain]`** lists versions with start time and status.
|
|
13
|
+
It needs no password.
|
|
14
|
+
- `list` accepts the same glob as `get` when the pattern contains `*`, `?` or `[`.
|
|
15
|
+
- `--` ends the options.
|
|
16
|
+
- Each backup version gets its own cached index; superseded ones are deleted.
|
|
17
|
+
|
|
18
|
+
### Changed — check your scripts
|
|
19
|
+
|
|
20
|
+
Exit codes are now distinct and documented (README, "Exit codes"):
|
|
21
|
+
|
|
22
|
+
| situation | 0.4.8 | 0.5.0 |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| password missing (non-interactive) | 1 | 3 |
|
|
25
|
+
| wrong password | 1 or 2 | 3 |
|
|
26
|
+
| glob/pattern matched nothing (`get`, `verify`) | 1 | 5 |
|
|
27
|
+
| pattern matched nothing (`list`) | 0 | 5 |
|
|
28
|
+
| not enough disk space | 1 | 6 |
|
|
29
|
+
| usage error / usage text shown | 1 | 2 |
|
|
30
|
+
| interrupted `get`/`verify` | 0 or 1 | 130 |
|
|
31
|
+
| unexpected internal error | 1 | 2 |
|
|
32
|
+
| requested version missing / not complete | — | 4 |
|
|
33
|
+
| output pipe closed (`… \| head`), `list`/`versions`/`info` | 1 (traceback) | 0 |
|
|
34
|
+
| output pipe closed, `get`/`verify` | 1 (traceback) | 141 |
|
|
35
|
+
|
|
36
|
+
Arguments and options a command used to ignore are refused with code 2: a stray word
|
|
37
|
+
after the destination (an unquoted path with a space), `--strict` outside `get`, `-j` on
|
|
38
|
+
`list`/`info`/`doctor`, a repeated option, `--porcelain` outside `versions`.
|
|
39
|
+
|
|
40
|
+
`hbk --version` alone still prints hbkit's version; `hbk --version 3 <archive> …` now
|
|
41
|
+
selects backup version 3.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- Resuming into a directory that holds another version's restore no longer keeps a file
|
|
46
|
+
just because its size matches; the restore record now names each file's source.
|
|
@@ -231,7 +231,75 @@ Reconstruct paths by joining `pname_id_v2 -> name_id_v2`. Roots are rows whose p
|
|
|
231
231
|
absent from the table or which are self-parented. `mode` is a POSIX mode word — use
|
|
232
232
|
`S_ISDIR` to separate directories from files.
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
### 5.1 Backup versions [V for the columns below, I for their meaning]
|
|
235
|
+
|
|
236
|
+
Each backup run is a **version** with an integer id (1, 2, 3, …). A version writes one
|
|
237
|
+
`Config/@Share/<share>/<id>.db` per share it covered. Everything else — `virtual_file`,
|
|
238
|
+
`file_chunk`, `chunk_index`, the buckets — is shared by all versions, so the same file in
|
|
239
|
+
two versions differs only in its `version_list` row (`size`, `mtime_sec`,
|
|
240
|
+
`off_virtual_file`). Reading an older version means reading an older `<id>.db`; nothing
|
|
241
|
+
below the share databases changes.
|
|
242
|
+
|
|
243
|
+
`Config/version_info.db`, observed on Hyper Backup 4.2.2-4262 (DSM 7, two versions):
|
|
244
|
+
|
|
245
|
+
```sql
|
|
246
|
+
CREATE TABLE version_info (
|
|
247
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, -- the version id, as in <id>.db
|
|
248
|
+
timestamp INTEGER, -- unix seconds, when the version started
|
|
249
|
+
name TEXT, source TEXT, -- source is encrypted on encrypted archives
|
|
250
|
+
share TEXT, -- "share1,share2," - shares in this version
|
|
251
|
+
status TEXT, -- 'Complete' for a finished version
|
|
252
|
+
statistics TEXT, -- JSON: sizes and per-kind counts
|
|
253
|
+
diff_size INTEGER, locked INTEGER NOT NULL default 0, share_info BLOB,
|
|
254
|
+
missing_src TEXT, tag_db_magic BLOB, tag_db_file_size_thr INTEGER,
|
|
255
|
+
has_suspend_dup INTEGER DEFAULT 0, bad_share TEXT DEFAULT '',
|
|
256
|
+
internal_share TEXT DEFAULT '',
|
|
257
|
+
suspend_history TEXT DEFAULT '', -- JSON events: create, complete, ...
|
|
258
|
+
enc_cksum BLOB DEFAULT NULL, depose_time INTEGER DEFAULT -1);
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Only `id`, `timestamp` and `status` are relied on. `'Complete'` and `'Failure'` are the
|
|
262
|
+
status values seen so far; hbkit treats **any value other than `'Complete'` as not
|
|
263
|
+
complete** rather than guessing what it means. `timestamp` matched the `create` event in
|
|
264
|
+
`suspend_history`. [V]
|
|
265
|
+
|
|
266
|
+
A second archive (Hyper Backup 4.2.2-4262, encrypted) was read at 8 and again at 11
|
|
267
|
+
versions. Version 8 is a `Failure`: a run that stopped on an error. Its `suspend_history`
|
|
268
|
+
ends in event 5 `fail` where finished versions end in event 6 `complete`, and in the
|
|
269
|
+
8-version copy its `<id>.db` file lists existed, holding what was backed up before the
|
|
270
|
+
error. Reading it would silently give an incomplete restore, which is why it is never the
|
|
271
|
+
default. The next successful run became version 9 and deleted version 8's `<id>.db` files
|
|
272
|
+
(`Config/version_delete.prog` was written then, with `is_rotate=false`), but left its
|
|
273
|
+
`version_info` row, now naming shares that have no file list for it. `id` is
|
|
274
|
+
`AUTOINCREMENT`, so ids are never reused. [V for the values]
|
|
275
|
+
|
|
276
|
+
Version 11 is a run Hyper Backup reported as **partially completed**, because a source
|
|
277
|
+
folder of the task had been renamed away. It is recorded as `'Complete'`, with event 6
|
|
278
|
+
`complete` in `suspend_history`, like any finished version. Only `missing_src` differs: it
|
|
279
|
+
holds the missing source in the same `/*eNC00*...*/` form as `source` (`NULL` on every
|
|
280
|
+
other version). Its file lists omit that folder and are otherwise whole, so hbkit reads it
|
|
281
|
+
by default. Whether other causes of "partially completed" (unreadable files, say) are also
|
|
282
|
+
recorded as `'Complete'` is not known. hbkit cannot decrypt `missing_src` yet: it is not
|
|
283
|
+
encoded like file names. [V for the values, ? for other causes and the encoding]
|
|
284
|
+
|
|
285
|
+
`Config/@Share/<share>/complete_list.db` holds `complete_list(version_id INTEGER PRIMARY
|
|
286
|
+
KEY)`. It is **not** a reliable completion record: on the same archive the `@AppConfig`
|
|
287
|
+
share's list was empty although both versions were `Complete` in `version_info`, while
|
|
288
|
+
the data share listed both. On the second archive (11 versions) the data share listed
|
|
289
|
+
every `Complete` version but not the `Failure` one, and `@AppConfig` was again empty. hbkit
|
|
290
|
+
does not use it. [V for the observation, ? for meaning]
|
|
291
|
+
|
|
292
|
+
On that archive version 1 had `depose_time` set to the start time of version 2, while
|
|
293
|
+
version 2 kept the default `-1`. On the second archive each `Complete` version had the
|
|
294
|
+
start time of the next **`Complete`** version (version 7 holds version 9's, skipping the
|
|
295
|
+
failed 8), the newest `Complete` version had `0` and the `Failure` version `-1`: it looks
|
|
296
|
+
like "superseded as the newest good version at". The meaning is not known and hbkit
|
|
297
|
+
ignores it, as it does `bad_share` and `locked`.
|
|
298
|
+
[V for the values, ? for meaning]
|
|
299
|
+
|
|
300
|
+
The same archive also showed `Pool/vkey.db` with one row per version (`version_id` 1, 2),
|
|
301
|
+
and index shards with a `_dup` sibling (`0.idx_dup`) instead of a generation suffix; the
|
|
302
|
+
`_dup` copies are ignored.
|
|
235
303
|
|
|
236
304
|
Entries named `@eaDir`, or ending `@SynoEAStream` / `@SynoResource`, are Synology
|
|
237
305
|
metadata — thumbnails, extended attributes, resource forks — not user data. In the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hbkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Recover files from Synology Hyper Backup (.hbk) archives without Synology software
|
|
5
5
|
Project-URL: Homepage, https://github.com/YordiLorenzo/hbkit
|
|
6
6
|
Project-URL: Source, https://github.com/YordiLorenzo/hbkit
|
|
@@ -97,8 +97,9 @@ live progress bar, throughput, ETA and a failure log.
|
|
|
97
97
|
|
|
98
98
|
```sh
|
|
99
99
|
hbk <archive> doctor # probe an archive, prove it's readable
|
|
100
|
-
hbk <archive> info # task name, codec, shares,
|
|
101
|
-
hbk <archive>
|
|
100
|
+
hbk <archive> info # task name, codec, shares, versions
|
|
101
|
+
hbk <archive> versions [--porcelain] # backup versions, start times, status
|
|
102
|
+
hbk <archive> list [pattern|glob] # search the file index
|
|
102
103
|
hbk <archive> get <glob> <dest> [-j N] # extract, preserving tree and mtimes
|
|
103
104
|
hbk <archive> get <glob> <dest> --strict # ...re-verifying checksums on resume
|
|
104
105
|
hbk <archive> verify <glob> [-j N] # integrity-check, write nothing
|
|
@@ -114,7 +115,81 @@ For encrypted archives pass `-p/--password`, set `HBK_PASSWORD`, or let it promp
|
|
|
114
115
|
shows a password field when it detects encryption.
|
|
115
116
|
|
|
116
117
|
`<archive>` is a `.hbk` directory, or any drive or folder containing one — it will find it.
|
|
117
|
-
Globs match the full archive path, which begins with the share name
|
|
118
|
+
Globs match the full archive path, which begins with a slash and the share name
|
|
119
|
+
(`"/Photos/*"`, not `"Photos/*"`). `list` takes either plain text (case-insensitive
|
|
120
|
+
substring) or, if it contains `*`, `?` or `[`, the same glob `get` would use.
|
|
121
|
+
|
|
122
|
+
### Backup versions
|
|
123
|
+
|
|
124
|
+
Hyper Backup keeps many versions of the same data. `list`, `get`, `verify`, `info`,
|
|
125
|
+
`doctor` and `tui` read the **newest complete** version unless told otherwise:
|
|
126
|
+
|
|
127
|
+
| option | reads |
|
|
128
|
+
|---|---|
|
|
129
|
+
| *(none)* | the newest complete version |
|
|
130
|
+
| `--version N` | exactly version `N` (the numbers `versions` prints) |
|
|
131
|
+
| `--version @2026-10-15` | the newest complete version that started **by the end of** that day |
|
|
132
|
+
| `--version '@2026-10-15 03:30'` | … that started no later than 03:30:59 that day |
|
|
133
|
+
| `--before '2026-10-15 23:59'` | the same as `--version '@2026-10-15 23:59'` |
|
|
134
|
+
| `--allow-incomplete` | also accept a version whose backup did not finish |
|
|
135
|
+
|
|
136
|
+
Dates are in the local time of the machine running `hbk` (set `TZ` to change it); a bare
|
|
137
|
+
date runs to local midnight (also on daylight-saving days), a minute to :59, a time with
|
|
138
|
+
seconds is exact. A clock time that does not exist on a spring-forward night is read as
|
|
139
|
+
the hour after; one that occurs twice in autumn means its first occurrence. A version
|
|
140
|
+
counts as complete only if Hyper Backup recorded its status
|
|
141
|
+
as `Complete`, which it also does for a "partially completed" backup, at least one that
|
|
142
|
+
skipped a missing source folder; an unfinished or failed one is refused unless you pass `--allow-incomplete`, which
|
|
143
|
+
on its own reads the newest version whether or not it finished. If no version is
|
|
144
|
+
complete, commands exit with code 4 rather than showing an empty archive.
|
|
145
|
+
|
|
146
|
+
Every share is read from the **same** version, so the default and `--version N` for the
|
|
147
|
+
newest complete `N` give identical results; a share that was not part of that version is
|
|
148
|
+
absent rather than silently taken from an older one. Archives without
|
|
149
|
+
`Config/version_info.db` fall back to the newest database of each share. If the file is
|
|
150
|
+
there but cannot be read (damaged, an unknown layout, caught mid-write, or with no
|
|
151
|
+
versions in it), hbkit cannot tell finished versions from unfinished ones, so it refuses
|
|
152
|
+
(code 2) unless you pass
|
|
153
|
+
`--allow-incomplete`, which then reads the newest file list of each share and says that
|
|
154
|
+
completion was not checked. Whenever there is a choice, the version being read is
|
|
155
|
+
printed on stderr.
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
hbk "$HBK_DIR" versions # human-readable, marks the default
|
|
159
|
+
hbk "$HBK_DIR" versions --porcelain # id<TAB>ISO time<TAB>unix time<TAB>status
|
|
160
|
+
hbk "$HBK_DIR" list "/db_staging/firebird/*" --version @2026-10-15
|
|
161
|
+
hbk "$HBK_DIR" get "/db_staging/firebird/*.gbk" "$TMP" --version @2026-10-15 -j 8
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`--porcelain` prints one line per version, oldest first, for example
|
|
165
|
+
`12 2026-10-15T03:00:04+02:00 1792026004 Complete`. `versions` needs no password: the
|
|
166
|
+
version list is stored unencrypted. Each version gets its own index in the cache, so
|
|
167
|
+
switching between them only builds each index once. A rebuilt index replaces the stale
|
|
168
|
+
one for the same archive and version.
|
|
169
|
+
|
|
170
|
+
### Exit codes
|
|
171
|
+
|
|
172
|
+
Scripts can rely on these:
|
|
173
|
+
|
|
174
|
+
| code | meaning |
|
|
175
|
+
|---|---|
|
|
176
|
+
| 0 | success |
|
|
177
|
+
| 1 | one or more files failed to extract or verify — a checksum mismatch or an unreadable chunk; the failures are listed on stdout |
|
|
178
|
+
| 2 | usage error (including a malformed `--version`), the archive is missing, unreadable or of an unsupported layout, or an unexpected internal error |
|
|
179
|
+
| 3 | the archive is encrypted and no password was given, or it is wrong |
|
|
180
|
+
| 4 | the requested version does not exist, is not complete, or no version matches the date |
|
|
181
|
+
| 5 | the glob (`get`, `verify`) or pattern (`list`) matched no file |
|
|
182
|
+
| 6 | the destination does not have enough free space (`get`) |
|
|
183
|
+
| 130 | interrupted (Ctrl-C) |
|
|
184
|
+
| 141 | `get`, `verify` or another command that acts rather than only prints stopped because its output pipe closed (`… \| head`) before it finished; the outcome is unknown. `list`, `versions` and `info` exit 0 then; a pipe that closes after a command finished keeps that command's code (Linux and macOS; on Windows a closed pipe is a non-zero I/O error for every command) |
|
|
185
|
+
|
|
186
|
+
`doctor` has its own simple contract: 0 when the archive is recoverable, 1 otherwise, with
|
|
187
|
+
the reason in its report. Any argument or option a command would ignore — a second
|
|
188
|
+
`--version`, `--strict` outside `get`, a stray word after the destination (an unquoted
|
|
189
|
+
path with a space) — is refused with code 2 rather than silently dropped. `--version`
|
|
190
|
+
on its own prints hbkit's version; followed by anything it selects a backup version. To
|
|
191
|
+
search for a name that starts with `-`, end the options with `--`:
|
|
192
|
+
`hbk <archive> list -- -draft`.
|
|
118
193
|
|
|
119
194
|
**Start with `doctor`.** It reports the layout it found and then *proves* the archive is
|
|
120
195
|
readable by rebuilding a random sample of real files with full checksum verification:
|
|
@@ -140,7 +215,12 @@ readable by rebuilding a random sample of real files with full checksum verifica
|
|
|
140
215
|
|
|
141
216
|
## Behaviour worth knowing
|
|
142
217
|
|
|
143
|
-
- **Resumable.** Correctly-sized files are skipped, so re-running a big job is cheap.
|
|
218
|
+
- **Resumable.** Correctly-sized files are skipped, so re-running a big job is cheap. A
|
|
219
|
+
file recorded as restored from a *different* backup version is extracted again even if
|
|
220
|
+
its size matches, so restoring another version into the same directory is safe. A file
|
|
221
|
+
whose record does not name its source — restored by hbkit before 0.5 — is extracted
|
|
222
|
+
again once too, since nothing says which version it came from. (A file with no record
|
|
223
|
+
at all, e.g. copied in by hand, is still judged by size alone; `--strict` refuses it.)
|
|
144
224
|
- **Crash-safe.** Files are written to `.part` and atomically renamed, so an interrupted
|
|
145
225
|
run never leaves a truncated file that a later resume would trust.
|
|
146
226
|
- **Layout preserved.** Output goes to `<dest>/<share>/<original path>` with original mtimes.
|
|
@@ -148,7 +228,7 @@ readable by rebuilding a random sample of real files with full checksum verifica
|
|
|
148
228
|
- **Sidecars skipped.** `@eaDir`, `@SynoEAStream` and `@SynoResource` are Synology
|
|
149
229
|
metadata — thumbnails and xattr streams, not your data. In one real archive they were
|
|
150
230
|
half of all entries but under 1% of the bytes.
|
|
151
|
-
- **Index cached** per archive in `~/.cache/hbkit` (override with `HBK_CACHE`), rebuilt
|
|
231
|
+
- **Index cached** per archive and version in `~/.cache/hbkit` (override with `HBK_CACHE`), rebuilt
|
|
152
232
|
automatically when the archive changes. Browsing 1.1M files is instant after the first
|
|
153
233
|
open; building it takes ~10s locally, ~48s over a network mount.
|
|
154
234
|
|
|
@@ -271,6 +351,10 @@ Read this before trusting it with the only copy of anything.
|
|
|
271
351
|
archive (unencrypted, LZ4, single version, single pool) and a small encrypted one from
|
|
272
352
|
the same DSM version, both verified byte-exact, plus a 7-version encrypted archive from
|
|
273
353
|
Hyper Backup 4.2.2 reported in [#4](https://github.com/YordiLorenzo/hbkit/issues/4).
|
|
354
|
+
Version selection was checked byte-exact on a small two-version encrypted Hyper Backup
|
|
355
|
+
4.2.2 archive (each version's copy of a changed file, by number and by date), and on
|
|
356
|
+
synthetic archives; the version list comes from `Config/version_info.db`, whose layout
|
|
357
|
+
is checked before use.
|
|
274
358
|
Older record layouts (16-byte `chunk_index`, 28-byte bucket records) and zlib chunks
|
|
275
359
|
are implemented from disassembly but have not met a real archive of that kind. `doctor`
|
|
276
360
|
exists precisely so you can find out in seconds rather than mid-restore.
|
|
@@ -68,8 +68,9 @@ live progress bar, throughput, ETA and a failure log.
|
|
|
68
68
|
|
|
69
69
|
```sh
|
|
70
70
|
hbk <archive> doctor # probe an archive, prove it's readable
|
|
71
|
-
hbk <archive> info # task name, codec, shares,
|
|
72
|
-
hbk <archive>
|
|
71
|
+
hbk <archive> info # task name, codec, shares, versions
|
|
72
|
+
hbk <archive> versions [--porcelain] # backup versions, start times, status
|
|
73
|
+
hbk <archive> list [pattern|glob] # search the file index
|
|
73
74
|
hbk <archive> get <glob> <dest> [-j N] # extract, preserving tree and mtimes
|
|
74
75
|
hbk <archive> get <glob> <dest> --strict # ...re-verifying checksums on resume
|
|
75
76
|
hbk <archive> verify <glob> [-j N] # integrity-check, write nothing
|
|
@@ -85,7 +86,81 @@ For encrypted archives pass `-p/--password`, set `HBK_PASSWORD`, or let it promp
|
|
|
85
86
|
shows a password field when it detects encryption.
|
|
86
87
|
|
|
87
88
|
`<archive>` is a `.hbk` directory, or any drive or folder containing one — it will find it.
|
|
88
|
-
Globs match the full archive path, which begins with the share name
|
|
89
|
+
Globs match the full archive path, which begins with a slash and the share name
|
|
90
|
+
(`"/Photos/*"`, not `"Photos/*"`). `list` takes either plain text (case-insensitive
|
|
91
|
+
substring) or, if it contains `*`, `?` or `[`, the same glob `get` would use.
|
|
92
|
+
|
|
93
|
+
### Backup versions
|
|
94
|
+
|
|
95
|
+
Hyper Backup keeps many versions of the same data. `list`, `get`, `verify`, `info`,
|
|
96
|
+
`doctor` and `tui` read the **newest complete** version unless told otherwise:
|
|
97
|
+
|
|
98
|
+
| option | reads |
|
|
99
|
+
|---|---|
|
|
100
|
+
| *(none)* | the newest complete version |
|
|
101
|
+
| `--version N` | exactly version `N` (the numbers `versions` prints) |
|
|
102
|
+
| `--version @2026-10-15` | the newest complete version that started **by the end of** that day |
|
|
103
|
+
| `--version '@2026-10-15 03:30'` | … that started no later than 03:30:59 that day |
|
|
104
|
+
| `--before '2026-10-15 23:59'` | the same as `--version '@2026-10-15 23:59'` |
|
|
105
|
+
| `--allow-incomplete` | also accept a version whose backup did not finish |
|
|
106
|
+
|
|
107
|
+
Dates are in the local time of the machine running `hbk` (set `TZ` to change it); a bare
|
|
108
|
+
date runs to local midnight (also on daylight-saving days), a minute to :59, a time with
|
|
109
|
+
seconds is exact. A clock time that does not exist on a spring-forward night is read as
|
|
110
|
+
the hour after; one that occurs twice in autumn means its first occurrence. A version
|
|
111
|
+
counts as complete only if Hyper Backup recorded its status
|
|
112
|
+
as `Complete`, which it also does for a "partially completed" backup, at least one that
|
|
113
|
+
skipped a missing source folder; an unfinished or failed one is refused unless you pass `--allow-incomplete`, which
|
|
114
|
+
on its own reads the newest version whether or not it finished. If no version is
|
|
115
|
+
complete, commands exit with code 4 rather than showing an empty archive.
|
|
116
|
+
|
|
117
|
+
Every share is read from the **same** version, so the default and `--version N` for the
|
|
118
|
+
newest complete `N` give identical results; a share that was not part of that version is
|
|
119
|
+
absent rather than silently taken from an older one. Archives without
|
|
120
|
+
`Config/version_info.db` fall back to the newest database of each share. If the file is
|
|
121
|
+
there but cannot be read (damaged, an unknown layout, caught mid-write, or with no
|
|
122
|
+
versions in it), hbkit cannot tell finished versions from unfinished ones, so it refuses
|
|
123
|
+
(code 2) unless you pass
|
|
124
|
+
`--allow-incomplete`, which then reads the newest file list of each share and says that
|
|
125
|
+
completion was not checked. Whenever there is a choice, the version being read is
|
|
126
|
+
printed on stderr.
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
hbk "$HBK_DIR" versions # human-readable, marks the default
|
|
130
|
+
hbk "$HBK_DIR" versions --porcelain # id<TAB>ISO time<TAB>unix time<TAB>status
|
|
131
|
+
hbk "$HBK_DIR" list "/db_staging/firebird/*" --version @2026-10-15
|
|
132
|
+
hbk "$HBK_DIR" get "/db_staging/firebird/*.gbk" "$TMP" --version @2026-10-15 -j 8
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`--porcelain` prints one line per version, oldest first, for example
|
|
136
|
+
`12 2026-10-15T03:00:04+02:00 1792026004 Complete`. `versions` needs no password: the
|
|
137
|
+
version list is stored unencrypted. Each version gets its own index in the cache, so
|
|
138
|
+
switching between them only builds each index once. A rebuilt index replaces the stale
|
|
139
|
+
one for the same archive and version.
|
|
140
|
+
|
|
141
|
+
### Exit codes
|
|
142
|
+
|
|
143
|
+
Scripts can rely on these:
|
|
144
|
+
|
|
145
|
+
| code | meaning |
|
|
146
|
+
|---|---|
|
|
147
|
+
| 0 | success |
|
|
148
|
+
| 1 | one or more files failed to extract or verify — a checksum mismatch or an unreadable chunk; the failures are listed on stdout |
|
|
149
|
+
| 2 | usage error (including a malformed `--version`), the archive is missing, unreadable or of an unsupported layout, or an unexpected internal error |
|
|
150
|
+
| 3 | the archive is encrypted and no password was given, or it is wrong |
|
|
151
|
+
| 4 | the requested version does not exist, is not complete, or no version matches the date |
|
|
152
|
+
| 5 | the glob (`get`, `verify`) or pattern (`list`) matched no file |
|
|
153
|
+
| 6 | the destination does not have enough free space (`get`) |
|
|
154
|
+
| 130 | interrupted (Ctrl-C) |
|
|
155
|
+
| 141 | `get`, `verify` or another command that acts rather than only prints stopped because its output pipe closed (`… \| head`) before it finished; the outcome is unknown. `list`, `versions` and `info` exit 0 then; a pipe that closes after a command finished keeps that command's code (Linux and macOS; on Windows a closed pipe is a non-zero I/O error for every command) |
|
|
156
|
+
|
|
157
|
+
`doctor` has its own simple contract: 0 when the archive is recoverable, 1 otherwise, with
|
|
158
|
+
the reason in its report. Any argument or option a command would ignore — a second
|
|
159
|
+
`--version`, `--strict` outside `get`, a stray word after the destination (an unquoted
|
|
160
|
+
path with a space) — is refused with code 2 rather than silently dropped. `--version`
|
|
161
|
+
on its own prints hbkit's version; followed by anything it selects a backup version. To
|
|
162
|
+
search for a name that starts with `-`, end the options with `--`:
|
|
163
|
+
`hbk <archive> list -- -draft`.
|
|
89
164
|
|
|
90
165
|
**Start with `doctor`.** It reports the layout it found and then *proves* the archive is
|
|
91
166
|
readable by rebuilding a random sample of real files with full checksum verification:
|
|
@@ -111,7 +186,12 @@ readable by rebuilding a random sample of real files with full checksum verifica
|
|
|
111
186
|
|
|
112
187
|
## Behaviour worth knowing
|
|
113
188
|
|
|
114
|
-
- **Resumable.** Correctly-sized files are skipped, so re-running a big job is cheap.
|
|
189
|
+
- **Resumable.** Correctly-sized files are skipped, so re-running a big job is cheap. A
|
|
190
|
+
file recorded as restored from a *different* backup version is extracted again even if
|
|
191
|
+
its size matches, so restoring another version into the same directory is safe. A file
|
|
192
|
+
whose record does not name its source — restored by hbkit before 0.5 — is extracted
|
|
193
|
+
again once too, since nothing says which version it came from. (A file with no record
|
|
194
|
+
at all, e.g. copied in by hand, is still judged by size alone; `--strict` refuses it.)
|
|
115
195
|
- **Crash-safe.** Files are written to `.part` and atomically renamed, so an interrupted
|
|
116
196
|
run never leaves a truncated file that a later resume would trust.
|
|
117
197
|
- **Layout preserved.** Output goes to `<dest>/<share>/<original path>` with original mtimes.
|
|
@@ -119,7 +199,7 @@ readable by rebuilding a random sample of real files with full checksum verifica
|
|
|
119
199
|
- **Sidecars skipped.** `@eaDir`, `@SynoEAStream` and `@SynoResource` are Synology
|
|
120
200
|
metadata — thumbnails and xattr streams, not your data. In one real archive they were
|
|
121
201
|
half of all entries but under 1% of the bytes.
|
|
122
|
-
- **Index cached** per archive in `~/.cache/hbkit` (override with `HBK_CACHE`), rebuilt
|
|
202
|
+
- **Index cached** per archive and version in `~/.cache/hbkit` (override with `HBK_CACHE`), rebuilt
|
|
123
203
|
automatically when the archive changes. Browsing 1.1M files is instant after the first
|
|
124
204
|
open; building it takes ~10s locally, ~48s over a network mount.
|
|
125
205
|
|
|
@@ -242,6 +322,10 @@ Read this before trusting it with the only copy of anything.
|
|
|
242
322
|
archive (unencrypted, LZ4, single version, single pool) and a small encrypted one from
|
|
243
323
|
the same DSM version, both verified byte-exact, plus a 7-version encrypted archive from
|
|
244
324
|
Hyper Backup 4.2.2 reported in [#4](https://github.com/YordiLorenzo/hbkit/issues/4).
|
|
325
|
+
Version selection was checked byte-exact on a small two-version encrypted Hyper Backup
|
|
326
|
+
4.2.2 archive (each version's copy of a changed file, by number and by date), and on
|
|
327
|
+
synthetic archives; the version list comes from `Config/version_info.db`, whose layout
|
|
328
|
+
is checked before use.
|
|
245
329
|
Older record layouts (16-byte `chunk_index`, 28-byte bucket records) and zlib chunks
|
|
246
330
|
are implemented from disassembly but have not met a real archive of that kind. `doctor`
|
|
247
331
|
exists precisely so you can find out in seconds rather than mid-restore.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hbkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Recover files from Synology Hyper Backup (.hbk) archives without Synology software"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -43,4 +43,4 @@ hbk-tui = "hbkit.tui:main"
|
|
|
43
43
|
packages = ["src/hbkit"]
|
|
44
44
|
|
|
45
45
|
[tool.hatch.build.targets.sdist]
|
|
46
|
-
include = ["src/hbkit", "tests", "README.md", "FORMAT.md", "LICENSE"]
|
|
46
|
+
include = ["src/hbkit", "tests", "README.md", "FORMAT.md", "CHANGELOG.md", "LICENSE"]
|