btrfs-timeline 0.1.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.
Files changed (39) hide show
  1. btrfs_timeline-0.1.0/.gitignore +14 -0
  2. btrfs_timeline-0.1.0/CHANGELOG.md +78 -0
  3. btrfs_timeline-0.1.0/LICENSE +21 -0
  4. btrfs_timeline-0.1.0/PKG-INFO +335 -0
  5. btrfs_timeline-0.1.0/README.ja.md +299 -0
  6. btrfs_timeline-0.1.0/README.md +299 -0
  7. btrfs_timeline-0.1.0/btrfs_timeline/__init__.py +5 -0
  8. btrfs_timeline-0.1.0/btrfs_timeline/__main__.py +10 -0
  9. btrfs_timeline-0.1.0/btrfs_timeline/cli.py +580 -0
  10. btrfs_timeline-0.1.0/btrfs_timeline/core/__init__.py +20 -0
  11. btrfs_timeline-0.1.0/btrfs_timeline/core/browse.py +204 -0
  12. btrfs_timeline-0.1.0/btrfs_timeline/core/diff.py +133 -0
  13. btrfs_timeline-0.1.0/btrfs_timeline/core/history.py +215 -0
  14. btrfs_timeline-0.1.0/btrfs_timeline/core/mounts.py +137 -0
  15. btrfs_timeline-0.1.0/btrfs_timeline/core/restore.py +279 -0
  16. btrfs_timeline-0.1.0/btrfs_timeline/core/snapshots.py +147 -0
  17. btrfs_timeline-0.1.0/btrfs_timeline/i18n.py +160 -0
  18. btrfs_timeline-0.1.0/btrfs_timeline/locales/en.json +139 -0
  19. btrfs_timeline-0.1.0/btrfs_timeline/locales/ja.json +139 -0
  20. btrfs_timeline-0.1.0/btrfs_timeline/web/__init__.py +7 -0
  21. btrfs_timeline-0.1.0/btrfs_timeline/web/auth.py +113 -0
  22. btrfs_timeline-0.1.0/btrfs_timeline/web/server.py +443 -0
  23. btrfs_timeline-0.1.0/btrfs_timeline/web/static/app.js +520 -0
  24. btrfs_timeline-0.1.0/btrfs_timeline/web/static/i18n.js +25 -0
  25. btrfs_timeline-0.1.0/btrfs_timeline/web/static/index.html +91 -0
  26. btrfs_timeline-0.1.0/btrfs_timeline/web/static/style.css +237 -0
  27. btrfs_timeline-0.1.0/btrfs_timeline/web/static/transport.js +57 -0
  28. btrfs_timeline-0.1.0/pyproject.toml +75 -0
  29. btrfs_timeline-0.1.0/tests/js/drive.mjs +126 -0
  30. btrfs_timeline-0.1.0/tests/js/transport.mjs +72 -0
  31. btrfs_timeline-0.1.0/tests/test_browse.py +209 -0
  32. btrfs_timeline-0.1.0/tests/test_diff.py +126 -0
  33. btrfs_timeline-0.1.0/tests/test_history.py +139 -0
  34. btrfs_timeline-0.1.0/tests/test_i18n.py +264 -0
  35. btrfs_timeline-0.1.0/tests/test_mounts.py +60 -0
  36. btrfs_timeline-0.1.0/tests/test_restore.py +307 -0
  37. btrfs_timeline-0.1.0/tests/test_snapshots.py +82 -0
  38. btrfs_timeline-0.1.0/tests/test_web.py +419 -0
  39. btrfs_timeline-0.1.0/tests/test_web_ui.py +184 -0
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
9
+ .coverage
10
+ htmlcov/
11
+ *.log
12
+
13
+ # uv が開発用に生成するロックファイル (ライブラリなので固定しない)
14
+ uv.lock
@@ -0,0 +1,78 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-09-23
9
+
10
+ First public release. Nothing here is a change from an earlier version; this is what the
11
+ release contains.
12
+
13
+ ### Browsing history
14
+
15
+ - Snapshot discovery for the snapper layout (`<mount>/.snapshots/<N>/snapshot` with
16
+ `info.xml`) and for the flat layouts used by btrbk, Timeshift and manual setups. It
17
+ reads `/proc/self/mountinfo` and walks directories rather than calling
18
+ `btrfs subvolume list`, which requires root — so **browsing history does not**.
19
+ - File version history, deduplicated by mtime and size, keeping the periods when a file
20
+ did not exist as entries of their own. Copy-on-write means an unchanged file otherwise
21
+ appears once per snapshot.
22
+ - Directory history, and listing a directory as it stood at a point in time. Entries
23
+ deleted since then show up there, which is the only way to reach them — the current
24
+ filesystem no longer has them — and their own history and restore work as usual from
25
+ that point. Directory versions come from the directory's mtime, so they mark when
26
+ entries came and went, not when a file inside was edited.
27
+ - Diffs between a version and the current file, or between any two versions.
28
+
29
+ ### Restoring
30
+
31
+ - `restore` brings a version back with the `FICLONE` ioctl, sharing extents with the
32
+ snapshot: instant regardless of size, no extra space, falling back to a plain copy
33
+ elsewhere and reporting which happened.
34
+ - Non-destructive by default: the recovered version is written beside the original as
35
+ `notes.<timestamp>.md`. `--in-place` overwrites, and even then the current content is
36
+ kept as a backup unless `--no-backup --force`.
37
+ - Writes go through a temporary file in the destination directory and `rename(2)`, so an
38
+ interrupted restore leaves no partial file. Symlinks are recreated as symlinks; the
39
+ target is resolved with realpath, so restoring a symlinked dotfile in place does not
40
+ replace the link itself.
41
+
42
+ ### Command line
43
+
44
+ - `history`, `restore`, `diff`, `browse`, `snapshots`, `mounts` and `serve`, every one of
45
+ them with `--json`. That output is the contract the other front-ends share: a Cockpit
46
+ module has no server side and can only spawn a process, so a JSON-emitting CLI is the
47
+ one shape a shared core can take.
48
+
49
+ ### Web UI
50
+
51
+ - `serve` runs a standalone web UI with no build step and no JavaScript toolchain: walk
52
+ the filesystem, see a file's versions, preview one, compare it, restore it.
53
+ - One time axis rather than a mode per feature. Picking a directory version re-lists the
54
+ left pane at that moment; the preview pane doubles as the diff view.
55
+ - The screen is rendered in the browser and gets its data through `transport.js`, so the
56
+ Cockpit module can reuse all of it by replacing that one file.
57
+ - Loopback only by default; it refuses any other address without `--auth` (or
58
+ `--allow-no-auth` said explicitly). Restoring is a `POST` and is rejected on a foreign
59
+ `Origin`. `--root` confines reading to a subtree, compared with realpath so a symlink
60
+ inside cannot escape, and `--read-only` disables restoring. The server runs
61
+ unprivileged, as the user.
62
+
63
+ ### Translations
64
+
65
+ - Messages come from JSON catalogs under `btrfs_timeline/locales/`, with English as the
66
+ reference and fallback. English and Japanese ship with the package. The language comes
67
+ from `--lang`, `BTRFS_TIMELINE_LANG` or the usual locale variables, and the web UI has
68
+ a picker that lists each language by the name it gives itself.
69
+ - Adding a language means adding one JSON file and touching no code. Tests check that
70
+ every catalog carries exactly the keys of `en.json` with matching placeholder names,
71
+ and that every key the code looks up exists.
72
+ - JSON rather than gettext, because `.po` needs a compilation step and the web UI and
73
+ Cockpit module render in the browser, where `.mo` is unusable. One catalog format both
74
+ Python and JavaScript can read keeps the translations from being maintained twice.
75
+ - Terminal tables are padded by display width, so East Asian full-width characters line
76
+ up.
77
+
78
+ [0.1.0]: https://github.com/akivajp/btrfs-timeline/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Akiva Miura
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,335 @@
1
+ Metadata-Version: 2.5
2
+ Name: btrfs-timeline
3
+ Version: 0.1.0
4
+ Summary: Browse and restore previous versions of your files from btrfs snapshots, from a browser
5
+ Project-URL: Homepage, https://github.com/akivajp/btrfs-timeline
6
+ Project-URL: Repository, https://github.com/akivajp/btrfs-timeline
7
+ Project-URL: Issues, https://github.com/akivajp/btrfs-timeline/issues
8
+ Author-email: Akiva Miura <akiva.miura@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: backup,btrfs,file-history,self-hosted,snapper,snapshot,web-ui
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: System :: Archiving :: Backup
24
+ Classifier: Topic :: System :: Filesystems
25
+ Classifier: Topic :: System :: Systems Administration
26
+ Requires-Python: >=3.9
27
+ Provides-Extra: dev
28
+ Requires-Dist: bottle>=0.12; extra == 'dev'
29
+ Requires-Dist: pytest>=7.0; extra == 'dev'
30
+ Requires-Dist: webtest>=3.0; extra == 'dev'
31
+ Provides-Extra: server
32
+ Requires-Dist: waitress>=3.0; extra == 'server'
33
+ Provides-Extra: web
34
+ Requires-Dist: bottle>=0.12; extra == 'web'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # btrfs-timeline
38
+
39
+ Browse and restore previous versions of your files from btrfs snapshots — the way
40
+ Time Machine does it, from a browser.
41
+
42
+ [日本語版 README はこちら](README.ja.md)
43
+
44
+ > **Status: early development.** Browsing history, previewing versions and restoring
45
+ > files all work, from the CLI and from the browser. The Cockpit module is next.
46
+ > See [Roadmap](#roadmap).
47
+
48
+ ![The web UI: a file browser on the left, and on the right the versions of the selected file, each with a preview and a restore button](https://raw.githubusercontent.com/akivajp/btrfs-timeline/main/docs/screenshot-en.png)
49
+
50
+ One file, four rows: a stretch when it did not exist yet, two contents it has held since,
51
+ and the file as it stands now. Forty-eight snapshots went into that — the ones that
52
+ changed nothing are not worth a row.
53
+
54
+ ## Why this exists
55
+
56
+ If you run btrfs with automatic snapshots (snapper, btrbk, Timeshift), you already have
57
+ every previous version of every file. Getting one back, however, means knowing where the
58
+ snapshots live, guessing which one is old enough, and comparing files by hand on the
59
+ command line. It works, but it is expert-only — and it is the one thing people actually
60
+ need snapshots for.
61
+
62
+ The tools that exist today each stop somewhere short of this:
63
+
64
+ | Tool | What it does | Why it may not fit |
65
+ | --- | --- | --- |
66
+ | [httm](https://github.com/kimono-koans/httm) | Excellent interactive file history for ZFS/btrfs | CLI/TUI only |
67
+ | Btrfs Assistant, Snapper GUI | Snapshot management | Desktop app; not per-file history |
68
+ | Samba `vfs_shadow_copy2` | "Previous Versions" in Windows Explorer | Requires a Windows client |
69
+ | Rockstor, OpenMediaVault | Full NAS web UI with btrfs support | NAS-only distributions; share-level rollback, not per-file |
70
+ | Cockpit `storaged` | btrfs filesystem/subvolume creation | No snapshot browsing; multi-device btrfs unsupported |
71
+
72
+ Nothing lets you open a browser, point at a path, and walk back through time.
73
+ That is what this is.
74
+
75
+ ## Design
76
+
77
+ The project is meant to ship in three forms, and they share one thing:
78
+
79
+ ```
80
+ btrfs_timeline/core/ Library: snapshot discovery, version history, restore
81
+ btrfs_timeline/cli.py CLI with --json on every subcommand <- the shared contract
82
+ btrfs_timeline/web/static/ The screen: index.html + app.js + style.css
83
+ transport.js <- the only file a front-end replaces
84
+ |- standalone web imports core directly (single process)
85
+ |- Cockpit module cockpit.spawn([... , "--json"], {superuser: "require"})
86
+ `- desktop GUI imports core, or calls the CLI
87
+ ```
88
+
89
+ A Cockpit module has **no server side** — it is static files plus a `manifest.json`, and
90
+ it reaches the system by spawning processes from the browser. So the only shape a shared
91
+ core can take is *a CLI that emits JSON*. Every subcommand here has `--json` for that
92
+ reason; treat its output as a public API.
93
+
94
+ The screen is built in the browser, not on the server. A Cockpit module has no server
95
+ to render with, so anything baked into the HTML could not be reused there. Instead
96
+ `app.js` imports its data from `transport.js`, and that one file is what each front-end
97
+ swaps: `fetch('./api/history?…')` here, `cockpit.spawn` there. The rest of the UI — and
98
+ the translations, which come from the same JSON catalogs the CLI uses — is shared.
99
+
100
+ Two decisions follow from measurements rather than taste:
101
+
102
+ - **No `btrfs subvolume list`.** It requires root (`Operation not permitted` otherwise),
103
+ while the files inside snapshots are readable under ordinary permissions. Discovery is
104
+ done with `/proc/self/mountinfo` plus directory traversal, so **browsing history does
105
+ not need root**.
106
+ - **Deduplication is mandatory.** btrfs is copy-on-write, so an unchanged file appears
107
+ once per snapshot. On the author's machine, 35 snapshots collapse to a single version.
108
+ Versions are merged by mtime and size, and the periods when a file did *not* exist are
109
+ preserved as their own entries — otherwise a deleted-then-recreated file looks
110
+ continuous.
111
+
112
+ ## Installation
113
+
114
+ ```shell
115
+ pipx install btrfs-timeline # CLI only, no dependencies
116
+ pipx install 'btrfs-timeline[web]' # with the standalone web UI
117
+ ```
118
+
119
+ Requires Python 3.9+ and Linux. `btrfs-progs` is *not* required for browsing history.
120
+
121
+ ## Usage
122
+
123
+ ```shell
124
+ # What versions of this file exist?
125
+ btrfs-timeline history ~/notes.md
126
+
127
+ # Machine-readable, for scripts and for the Cockpit module
128
+ btrfs-timeline history ~/notes.md --json
129
+
130
+ # Which snapshots cover this path, and what layout are they in?
131
+ btrfs-timeline snapshots ~/
132
+
133
+ # Which btrfs mounts does this system have?
134
+ btrfs-timeline mounts
135
+
136
+ # What is in this directory? (the web UI uses the same call)
137
+ btrfs-timeline browse ~/Documents --json
138
+
139
+ # What was in it back then — including what has been deleted since
140
+ btrfs-timeline browse ~/Documents --snapshot 1729
141
+
142
+ # What changed between a version and the file as it is now
143
+ btrfs-timeline diff ~/notes.md --from 2
144
+ btrfs-timeline diff ~/notes.md --from 2 --to 3
145
+
146
+ # Open the web UI
147
+ btrfs-timeline serve
148
+ ```
149
+
150
+ Example output:
151
+
152
+ ```
153
+ /home/akiva/.gitconfig
154
+ # FIRST SEEN LAST SEEN SIZE SNAPS STATE
155
+ 1 2024-10-02 02:00:08 2025-01-01 00:00:00 - 3 (does not exist)
156
+ 2 2025-12-01 00:00:08 2026-03-01 00:00:00 268 B 4 ok
157
+ 3 2026-04-01 00:00:00 2026-09-23 01:00:00 297 B 28 ok
158
+ 4 - - 297 B - live
159
+ ```
160
+
161
+ Thirty-five snapshots, three meaningful versions, and the gap before the file was
162
+ created — which is the point.
163
+
164
+ ## Restoring
165
+
166
+ ```shell
167
+ # Bring back the newest version found in snapshots, next to the original file
168
+ btrfs-timeline restore ~/notes.md
169
+
170
+ # Pick a version by the number shown in the "#" column of `history`
171
+ btrfs-timeline restore ~/notes.md --index 2
172
+
173
+ # Or by snapshot id
174
+ btrfs-timeline restore ~/notes.md --snapshot 10129
175
+
176
+ # Show what would happen and change nothing
177
+ btrfs-timeline restore ~/notes.md --dry-run
178
+
179
+ # Write somewhere else, or overwrite the original (a backup is kept)
180
+ btrfs-timeline restore ~/notes.md --to /tmp/notes.old.md
181
+ btrfs-timeline restore ~/notes.md --in-place
182
+ ```
183
+
184
+ Three properties matter here:
185
+
186
+ - **Nothing is overwritten by default.** The restored file is written next to the
187
+ original as `notes.20260401T000008.md`. The worst failure mode for a history tool is
188
+ destroying the current content of a file while trying to get an old one back, so it is
189
+ excluded by default rather than guarded by a prompt. `--in-place` overwrites, and even
190
+ then the current content is kept as `notes.before-restore.<timestamp>.md` unless you
191
+ pass both `--no-backup` and `--force`.
192
+ - **It is a reflink, not a copy.** Restoring uses the `FICLONE` ioctl, so it shares
193
+ extents with the snapshot: instant regardless of file size, and no extra space used.
194
+ If the destination is on another filesystem it falls back to a plain copy, and the
195
+ output says which one happened.
196
+ - **Writes are atomic.** Content goes to a temporary file in the destination directory
197
+ and is moved into place with `rename(2)`, so an interrupted restore never leaves a
198
+ half-written file.
199
+
200
+ ## Web UI
201
+
202
+ ```shell
203
+ pipx install 'btrfs-timeline[web]'
204
+ btrfs-timeline serve # http://127.0.0.1:8088/
205
+ btrfs-timeline serve --root ~/Documents # only allow reading below this directory
206
+ btrfs-timeline serve --read-only # history only, no restoring
207
+ ```
208
+
209
+ Walk the filesystem on the left, click a file, and its versions appear on the right.
210
+ Each version can be previewed before you decide, and restored with one button. Restoring
211
+ always shows a dry run of exactly what it will write — and where the current content will
212
+ be kept — before asking you to confirm.
213
+
214
+ There is **one time axis, not a set of modes**. Everything the screen does is a
215
+ combination of two things: a path, and a point in time.
216
+
217
+ - Click a **directory** and you get its history too. Pick one of its versions and the
218
+ listing on the left becomes what that directory held at that moment.
219
+ - Files that were **deleted since** appear there, struck through. They are invisible in
220
+ the current filesystem, so this is the only way to reach them — and from there their
221
+ history and restore work exactly as they do for any other file.
222
+ - The preview pane doubles as a **diff**: compare a version against the current file, or
223
+ against any other version, with the selector next to the toggle.
224
+
225
+ No tabs were added for any of this. Adding one screen per feature would mean learning the
226
+ same "look at the past" gesture three times over.
227
+
228
+ Two settings sit in the page and are remembered per browser: the **language**, picked
229
+ from the same catalogs the CLI uses, and whether to **show hidden files** — off by
230
+ default, because a home directory is mostly dotfiles and the things you came for get
231
+ buried in them.
232
+
233
+ It listens on loopback only by default, and it deliberately refuses to listen on any
234
+ other address without `--auth USER:PASSWORD` (also read from `BTRFS_TIMELINE_AUTH`),
235
+ because anyone who can reach it can read your files and write over them. `--allow-no-auth`
236
+ overrides that if you really mean it. Restoring is a `POST` and is rejected when the
237
+ request carries a foreign `Origin`: basic auth alone would not stop another site from
238
+ making your browser send the request for you.
239
+
240
+ The server runs as you, so it can only read what you can read. It does not need root —
241
+ that is the point of not using `btrfs subvolume list`.
242
+
243
+ ## Translations
244
+
245
+ Messages are translated at runtime from JSON catalogs in
246
+ [`btrfs_timeline/locales/`](btrfs_timeline/locales/). `en.json` is the reference, and any
247
+ key a catalog is missing falls back to English — so a partial translation is useful from
248
+ its very first line.
249
+
250
+ ```shell
251
+ btrfs-timeline history ~/notes.md --lang ja # this invocation only
252
+ BTRFS_TIMELINE_LANG=ja btrfs-timeline ... # this shell
253
+ ```
254
+
255
+ With neither, the language comes from `LC_ALL`, `LC_MESSAGES` or `LANG`, and falls back
256
+ to English; `LANG=C` means English. Only human-readable output is translated. `--json` is
257
+ byte-identical in every language, because front-ends and scripts consume it.
258
+
259
+ ### Adding a language
260
+
261
+ Copy `en.json` to `<code>.json` — an ISO 639-1 code such as `de`, or a regional variant
262
+ such as `pt_br`, which falls back to `pt` if that catalog exists — and translate the
263
+ values, starting with `language.name`, which is how your language names *itself* (`日本語`,
264
+ not `Japanese`): that string is what the web UI's language picker shows, so someone who
265
+ reads only your language has to be able to find it. **No code changes are needed.** The
266
+ new language is picked up automatically, both in the values `--lang` accepts and in the
267
+ picker.
268
+
269
+ The test suite (`pytest tests/test_i18n.py`) enforces two rules:
270
+
271
+ - Every key from `en.json` is present, and nothing extra is.
272
+ - Placeholder *names* match (`{path}`, `{index}`). Their order within a sentence is yours
273
+ to change — that is exactly why they are named rather than positional.
274
+
275
+ Tables are aligned by terminal display width rather than character count, so East Asian
276
+ full-width characters line up correctly.
277
+
278
+ Why JSON and not gettext: `.po` files need a compilation step to `.mo`, and the web UI
279
+ and Cockpit module render in the browser, where `.mo` is unusable. A single catalog
280
+ format that both Python and JavaScript can read keeps the translations from being
281
+ maintained twice.
282
+
283
+ ## Supported snapshot layouts
284
+
285
+ | Layout | Path shape | Typical source |
286
+ | --- | --- | --- |
287
+ | snapper | `<mount>/.snapshots/<N>/snapshot` + `info.xml` | snapper |
288
+ | flat | `<mount>/.snapshots/<name>` | btrbk, Timeshift, manual |
289
+
290
+ Snapshot timestamps come from snapper's `info.xml` (which records **UTC**, despite
291
+ carrying no timezone marker), from a timestamp embedded in the directory name, or from
292
+ the directory mtime, in that order.
293
+
294
+ ## Roadmap
295
+
296
+ 1. **File history browser, standalone web UI** — done
297
+ 2. **Cockpit module** — the current focus: same CLI, same screen, only `transport.js`
298
+ differs, for people who already run Cockpit
299
+ 3. **Dashboard and device management** — devices, RAID profile, scrub/balance progress
300
+ 4. **Desktop GUI**
301
+
302
+ Device management (3) can destroy a filesystem when it goes wrong, which is a different
303
+ class of risk from reading history. It will stay a separate module with separate
304
+ privileges.
305
+
306
+ ## Notes and limitations
307
+
308
+ - Symlinks are resolved against the *live* filesystem, so history follows the target of a
309
+ link (useful for dotfiles). Links inside snapshots are never followed, so a snapshot
310
+ never reports live content as if it were old.
311
+ - Versions are compared by mtime and size, not by content hash. Hashing would mean
312
+ reading every version of every file.
313
+ - Restoring a whole directory is not supported yet — only individual files. You can see
314
+ what a directory held at any point and restore the files out of it one by one, but
315
+ there is no single "put this directory back". Restoring part of a tree silently would
316
+ be worse than refusing.
317
+ - A directory's history comes from the directory's own mtime, which btrfs preserves. That
318
+ changes when entries are added, removed or renamed — not when a file inside is edited.
319
+ So directory versions mark *what came and went*, which is the granularity you want for
320
+ finding something deleted; use the file's own history for edits.
321
+ - A restored file keeps the original's permissions and mtime, but not its owner:
322
+ `chown` requires root, and this tool is meant to run unprivileged.
323
+ - btrfs RAID 5/6 is still not considered production-ready upstream; this tool does not
324
+ change that.
325
+
326
+ ## Alternatives
327
+
328
+ If you want a terminal tool rather than a browser, use
329
+ [httm](https://github.com/kimono-koans/httm) — it is mature, fast, and covers ZFS too.
330
+ If you want a full NAS appliance, use [Rockstor](https://rockstor.com/) or
331
+ [OpenMediaVault](https://www.openmediavault.org/).
332
+
333
+ ## License
334
+
335
+ MIT — see [LICENSE](LICENSE).