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.
- btrfs_timeline-0.1.0/.gitignore +14 -0
- btrfs_timeline-0.1.0/CHANGELOG.md +78 -0
- btrfs_timeline-0.1.0/LICENSE +21 -0
- btrfs_timeline-0.1.0/PKG-INFO +335 -0
- btrfs_timeline-0.1.0/README.ja.md +299 -0
- btrfs_timeline-0.1.0/README.md +299 -0
- btrfs_timeline-0.1.0/btrfs_timeline/__init__.py +5 -0
- btrfs_timeline-0.1.0/btrfs_timeline/__main__.py +10 -0
- btrfs_timeline-0.1.0/btrfs_timeline/cli.py +580 -0
- btrfs_timeline-0.1.0/btrfs_timeline/core/__init__.py +20 -0
- btrfs_timeline-0.1.0/btrfs_timeline/core/browse.py +204 -0
- btrfs_timeline-0.1.0/btrfs_timeline/core/diff.py +133 -0
- btrfs_timeline-0.1.0/btrfs_timeline/core/history.py +215 -0
- btrfs_timeline-0.1.0/btrfs_timeline/core/mounts.py +137 -0
- btrfs_timeline-0.1.0/btrfs_timeline/core/restore.py +279 -0
- btrfs_timeline-0.1.0/btrfs_timeline/core/snapshots.py +147 -0
- btrfs_timeline-0.1.0/btrfs_timeline/i18n.py +160 -0
- btrfs_timeline-0.1.0/btrfs_timeline/locales/en.json +139 -0
- btrfs_timeline-0.1.0/btrfs_timeline/locales/ja.json +139 -0
- btrfs_timeline-0.1.0/btrfs_timeline/web/__init__.py +7 -0
- btrfs_timeline-0.1.0/btrfs_timeline/web/auth.py +113 -0
- btrfs_timeline-0.1.0/btrfs_timeline/web/server.py +443 -0
- btrfs_timeline-0.1.0/btrfs_timeline/web/static/app.js +520 -0
- btrfs_timeline-0.1.0/btrfs_timeline/web/static/i18n.js +25 -0
- btrfs_timeline-0.1.0/btrfs_timeline/web/static/index.html +91 -0
- btrfs_timeline-0.1.0/btrfs_timeline/web/static/style.css +237 -0
- btrfs_timeline-0.1.0/btrfs_timeline/web/static/transport.js +57 -0
- btrfs_timeline-0.1.0/pyproject.toml +75 -0
- btrfs_timeline-0.1.0/tests/js/drive.mjs +126 -0
- btrfs_timeline-0.1.0/tests/js/transport.mjs +72 -0
- btrfs_timeline-0.1.0/tests/test_browse.py +209 -0
- btrfs_timeline-0.1.0/tests/test_diff.py +126 -0
- btrfs_timeline-0.1.0/tests/test_history.py +139 -0
- btrfs_timeline-0.1.0/tests/test_i18n.py +264 -0
- btrfs_timeline-0.1.0/tests/test_mounts.py +60 -0
- btrfs_timeline-0.1.0/tests/test_restore.py +307 -0
- btrfs_timeline-0.1.0/tests/test_snapshots.py +82 -0
- btrfs_timeline-0.1.0/tests/test_web.py +419 -0
- btrfs_timeline-0.1.0/tests/test_web_ui.py +184 -0
|
@@ -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
|
+

|
|
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).
|