joplin-plugin-incremental-backup 1.0.3 → 1.0.5
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.
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A Joplin desktop plugin that creates **deduplicated incremental snapshots** of y
|
|
|
4
4
|
|
|
5
5
|
Each snapshot is a complete, self-contained copy of your data. Consecutive snapshots share unchanged files via hardlinks (rsnapshot-style), so storing dozens of snapshots costs only a fraction of the space of full copies. If hardlinks aren't supported on your backup drive (e.g. some network shares or FAT/exFAT drives), the plugin transparently falls back to plain copies.
|
|
6
6
|
|
|
7
|
-
> **Status:** in development. The plugin requires Joplin 3.
|
|
7
|
+
> **Status:** in development. The plugin requires Joplin 3.2.11+ and desktop only — Joplin's mobile apps do not support plugins.
|
|
8
8
|
|
|
9
9
|
## What it does
|
|
10
10
|
|
|
@@ -13,8 +13,9 @@ Each snapshot is a complete, self-contained copy of your data. Consecutive snaps
|
|
|
13
13
|
- **Content-addressed matching** — files are matched by SHA-256 hash, so even renamed or moved notes don't consume extra space.
|
|
14
14
|
- **Retention pruning** — keeps a configurable number of snapshots (default: 5) and automatically deletes the oldest ones. Deleting an old snapshot never damages newer ones, because hardlinked files are only removed when no snapshot references them.
|
|
15
15
|
- **Skip unchanged backups** — if nothing has changed since the last snapshot (notes, attachments, notebooks, tags, or which notes carry which tags), the backup is skipped entirely (can be disabled).
|
|
16
|
-
- **Integrity verification** — every snapshot includes a `manifest.json` with the size and SHA-256 hash of every file. A verify command re-checks the whole snapshot and reports any missing or corrupted files.
|
|
16
|
+
- **Integrity verification** — every snapshot includes a `manifest.json` with the size and SHA-256 hash of every file. A verify command re-checks the whole snapshot and reports any missing or corrupted files, and warns about files the manifest doesn't list. While a verification runs, backups can still run, but retention pruning skips the snapshot being verified (it is pruned by a later run instead). A verification started while a backup is running says so and asks to be run again once it finishes.
|
|
17
17
|
- **Crash-safe** — each backup run stages its files in a unique temporary directory (e.g. `_joplin_active_20261021T090000_12345_abcdef`) that is atomically renamed into place when complete. An exclusive lock prevents overlapping runs; the lock's mtime is refreshed periodically so a long export is never mistaken for an abandoned one. Stale locks are recovered atomically (rename-steal), and leftover staging data is cleaned up on startup unless another backup holds a fresh lock.
|
|
18
|
+
- **Optional local staging** — with **Stage the export on this computer** on (under Advanced), the export goes to a temporary folder in the Joplin profile instead of the backup drive. Unchanged files are then hardlinked from the previous snapshot and only new or changed files are copied to the drive, so a daily backup of a large vault writes only what changed. See [Local staging](#local-staging).
|
|
18
19
|
- **Disk-space pre-flight** — checks available space before starting a backup and aborts the run if the backup drive is below the minimum free-space threshold, so a full disk can't produce a truncated snapshot. (If free space can't be measured on a platform, the backup proceeds with a warning.)
|
|
19
20
|
- **Export warnings surfaced** — if Joplin's exporter skips any item (e.g. still-encrypted notes), the warnings are logged and shown in the backup dialog so an incomplete snapshot isn't mistaken for a complete one.
|
|
20
21
|
- **Export completeness check** — after each export, the plugin compares the note/folder/resource counts from Joplin's database against the exported files. Missing items (often due to still-encrypted content) are reported as warnings.
|
|
@@ -29,6 +30,7 @@ Each snapshot is a complete, self-contained copy of your data. Consecutive snaps
|
|
|
29
30
|
- **Backup interval (hours)** — `0` disables automatic backups.
|
|
30
31
|
- **Snapshots to keep** — oldest snapshots beyond this count are pruned.
|
|
31
32
|
- **Skip if nothing changed** — on by default.
|
|
33
|
+
- **Stage the export on this computer** (Advanced) — off by default. See [Local staging](#local-staging).
|
|
32
34
|
4. Backups now run automatically on the interval. You can also trigger one any time via **Tools > Create incremental backup**, and check snapshot health via **Tools > Verify incremental backup**.
|
|
33
35
|
|
|
34
36
|
### Backup layout
|
|
@@ -44,13 +46,35 @@ Each snapshot is a complete, self-contained copy of your data. Consecutive snaps
|
|
|
44
46
|
├── 20261021T090000/ ← next snapshot (unchanged files hardlinked)
|
|
45
47
|
├── _joplin_active_20261021T090000_12345_abcdef/ ← staging dir (only while running)
|
|
46
48
|
├── latest.txt ← name of the newest snapshot
|
|
47
|
-
|
|
49
|
+
├── .joplin-incremental-backup ← marker: this is a backup directory
|
|
50
|
+
├── .backup.lock ← present only while a backup is running
|
|
51
|
+
└── .verifying.<snapshot>.<id> ← present only while a snapshot is being verified
|
|
48
52
|
```
|
|
49
53
|
|
|
50
54
|
Notes, notebooks, tags and note-tags are each written as a single `<id>.md` file directly in the snapshot root (this mirrors Joplin's Raw export exactly); only attachments live under `resources/`.
|
|
51
55
|
|
|
52
56
|
The staging directory is unique per run (embedding the start time, process id, and a random suffix) so two concurrent runs can never write into the same directory. On startup, any leftover staging directories older than the stale-lock threshold are cleaned up — unless another Joplin instance holds a fresh lock, in which case they are left untouched.
|
|
53
57
|
|
|
58
|
+
### Disconnected backup drives
|
|
59
|
+
|
|
60
|
+
The first backup to a directory creates it and writes a marker file, `.joplin-incremental-backup`. After that, the plugin refuses to back up to that directory if the marker is missing, instead of recreating the directory. This protects against an unmounted drive: without the check, a path like `/mnt/backup/joplin` would be recreated on the internal disk, the snapshots would go there, and they would be hidden under the mount point once the drive came back.
|
|
61
|
+
|
|
62
|
+
If you moved or deleted the backups on purpose, create an empty `.joplin-incremental-backup` file in the directory, or choose a different directory.
|
|
63
|
+
|
|
64
|
+
When upgrading from a version without the marker, the first backup adopts your existing backup directory. If that directory does not exist at that point but backups have been made before, the plugin assumes the drive is not mounted and refuses rather than creating it. If it really is a new location, create the directory yourself and the next backup will use it.
|
|
65
|
+
|
|
66
|
+
### Local staging
|
|
67
|
+
|
|
68
|
+
By default each run exports the whole vault into the staging directory **on the backup drive**, hashes it there, and then replaces unchanged files with hardlinks. That saves space but not writes: a 5 GB vault backed up daily writes about 5 GB to the drive every day, even if one note changed. On a USB stick or a slow drive that also makes every run slow.
|
|
69
|
+
|
|
70
|
+
With **Stage the export on this computer** on, a run instead:
|
|
71
|
+
|
|
72
|
+
1. Exports into `plugin-data/<plugin id>/staging/_joplin_active_…` in the Joplin profile, and hashes the files there.
|
|
73
|
+
2. Builds the staging directory on the backup drive by hardlinking files that are identical to the previous snapshot and copying only new or changed files.
|
|
74
|
+
3. Renames the staging directory into place and deletes the local export, as before.
|
|
75
|
+
|
|
76
|
+
The trade-off is temporary local disk space about the size of the vault. Before exporting, the plugin checks that the local disk has room for the previous snapshot plus the usual minimum free space. If it doesn't, the folder can't be created, or the local export fails (for example because the disk fills up on a first backup), the run stages on the backup drive as before and reports a warning. If the backup directory is on the same disk as the Joplin profile, staging locally would only add writes, so the run stages on the backup drive without a warning. Local staging left behind by a crash is removed the next time Joplin starts.
|
|
77
|
+
|
|
54
78
|
### Scheduler catch-up behaviour
|
|
55
79
|
|
|
56
80
|
When Joplin starts, the plugin checks when the last successful backup finished:
|
|
@@ -62,7 +86,7 @@ When Joplin starts, the plugin checks when the last successful backup finished:
|
|
|
62
86
|
|
|
63
87
|
If strict 24 h spacing matters more than avoiding an export at launch, this is a one-line change in `src/scheduler.ts` (replace the "next interval boundary" branch with `now + intervalMs`).
|
|
64
88
|
|
|
65
|
-
After a successful backup, the next one is scheduled exactly one interval later. After a failed backup, the next attempt is scheduled
|
|
89
|
+
After a successful backup, the next one is scheduled exactly one interval later. After a failed backup, the next attempt is scheduled sooner than a full interval, backing off while failures repeat: 5 minutes, then 15 minutes, then 1 hour, then the regular interval (never longer than the interval). Retries after a failure run quietly: only the first failure in a row shows toasts, and later ones are logged and recorded in **Show last backup result**. A successful backup (including a manual one) or a change to the backup directory or interval starts over. If another backup is already running, the scheduled one is retried after 5 minutes and does not count as a failure.
|
|
66
90
|
|
|
67
91
|
### Encrypted items
|
|
68
92
|
|
|
@@ -90,11 +114,15 @@ This makes the backups a safe, vendor-independent archive: even without the plug
|
|
|
90
114
|
|
|
91
115
|
## Building from source
|
|
92
116
|
|
|
117
|
+
Needs Node.js 22.12 or later.
|
|
118
|
+
|
|
93
119
|
```bash
|
|
94
120
|
npm install
|
|
95
121
|
npm run dist # builds the plugin into publish/
|
|
96
122
|
```
|
|
97
123
|
|
|
124
|
+
The project started from the `generator-joplin` scaffold, but don't refresh it with the generator: its template pins older dev dependencies and would overwrite `tsconfig.json`. Update dependencies directly with npm.
|
|
125
|
+
|
|
98
126
|
Other scripts:
|
|
99
127
|
|
|
100
128
|
```bash
|
|
@@ -103,6 +131,22 @@ npm run format:check # check formatting (CI)
|
|
|
103
131
|
npm run updateVersion # sync version from package.json into the manifest
|
|
104
132
|
```
|
|
105
133
|
|
|
134
|
+
### End-to-end tests
|
|
135
|
+
|
|
136
|
+
`e2e/` runs the built plugin inside a real Joplin desktop app with a throwaway profile. The tests add notes through Joplin's Data API, let the scheduler take the backups, and check the snapshots on disk (Raw layout, manifest hashes, hardlinks between snapshots, change summary, skipped runs). The scenario runs twice: once staging on the backup drive, and once with **Stage the export on this computer** on. For the second run the backups go on another filesystem (`/dev/shm` by default, override with `JOPLIN_E2E_OTHER_DEVICE`), since local staging is skipped when both are on the same disk.
|
|
137
|
+
|
|
138
|
+
A second scenario (`e2e/startup.e2e.test.ts`) covers what happens at startup: automatic backups paused without a usable directory, the upgrade guard for a missing drive, cleanup after a crashed run (stale lock and staging directories), backing off from another instance's lock, **Only back up when something changed** turned off, and the schedule not running a backup when it is turned off or not yet due. It also checks how moved notes, renamed notebooks, trashed notes and new attachments show up in a snapshot.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npm run fetch-joplin # download + verify + extract Joplin (default 3.7.22) into .cache/joplin/
|
|
142
|
+
npm run test:e2e # builds the plugin, then runs e2e/ in a visible Joplin window
|
|
143
|
+
npm run test:e2e:headless # same, on a virtual display (as in CI); needs xvfb
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
For headless runs, install Xvfb first (`sudo apt-get install xvfb`). The harness unsets `WAYLAND_DISPLAY` for Joplin, so on WSLg or a Wayland desktop no window appears.
|
|
147
|
+
|
|
148
|
+
CI runs the suite on the oldest supported Joplin (3.2.11) and on 3.7.22. To test another version locally, run `npm run fetch-joplin -- 3.2.11`; it becomes the default for `npm run test:e2e`. Set `JOPLIN_BIN` to use a different Joplin executable, `JOPLIN_E2E_WORKDIR` to choose where the profile, logs and backups go, and `JOPLIN_E2E_KEEP=1` to keep them after a passing run. They are always kept when a test fails. The tests never touch your own Joplin profile, and they run alongside a Joplin you already have open.
|
|
149
|
+
|
|
106
150
|
For information on publishing the plugin, see [GENERATOR_DOC.md](./GENERATOR_DOC.md).
|
|
107
151
|
|
|
108
152
|
## License
|
package/package.json
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "joplin-plugin-incremental-backup",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive",
|
|
6
6
|
"prepare": "npm run dist",
|
|
7
7
|
"test": "vitest run",
|
|
8
8
|
"test:coverage": "vitest run --coverage",
|
|
9
|
-
"
|
|
9
|
+
"pretest:e2e": "npm run dist",
|
|
10
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
11
|
+
"test:e2e:headless": "xvfb-run -a npm run test:e2e",
|
|
12
|
+
"fetch-joplin": "scripts/fetch-joplin.sh",
|
|
13
|
+
"lint": "oxlint src tests e2e",
|
|
10
14
|
"format": "prettier --write .",
|
|
11
15
|
"format:check": "prettier --check .",
|
|
12
|
-
"updateVersion": "webpack --env joplin-plugin-config=updateVersion"
|
|
13
|
-
|
|
16
|
+
"updateVersion": "webpack --env joplin-plugin-config=updateVersion"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22.12"
|
|
14
20
|
},
|
|
15
21
|
"license": "MIT",
|
|
16
22
|
"keywords": [
|
|
@@ -24,17 +30,17 @@
|
|
|
24
30
|
"@types/node": "^22.20.5",
|
|
25
31
|
"@vitest/coverage-v8": "^5.0.5",
|
|
26
32
|
"chalk": "^4.1.0",
|
|
27
|
-
"copy-webpack-plugin": "^
|
|
28
|
-
"fs-extra": "^
|
|
29
|
-
"glob": "^
|
|
33
|
+
"copy-webpack-plugin": "^14.0.0",
|
|
34
|
+
"fs-extra": "^11.4.1",
|
|
35
|
+
"glob": "^14.0.4",
|
|
30
36
|
"memfs": "^4.80.0",
|
|
31
37
|
"oxlint": "^1.89.0",
|
|
32
38
|
"prettier": "3.9.10",
|
|
33
|
-
"tar": "^
|
|
34
|
-
"ts-loader": "^9.
|
|
35
|
-
"typescript": "^
|
|
39
|
+
"tar": "^7.5.22",
|
|
40
|
+
"ts-loader": "^9.6.2",
|
|
41
|
+
"typescript": "^6.0.3",
|
|
36
42
|
"vitest": "^5.0.5",
|
|
37
|
-
"webpack": "^5.
|
|
38
|
-
"webpack-cli": "^
|
|
43
|
+
"webpack": "^5.112.2",
|
|
44
|
+
"webpack-cli": "^7.3.1"
|
|
39
45
|
}
|
|
40
46
|
}
|
|
Binary file
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 1,
|
|
3
3
|
"id": "io.github.raytray.joplin-incremental-backup",
|
|
4
|
-
"app_min_version": "3.
|
|
5
|
-
"version": "1.0.
|
|
4
|
+
"app_min_version": "3.2.11",
|
|
5
|
+
"version": "1.0.5",
|
|
6
6
|
"name": "Incremental Backup",
|
|
7
7
|
"description": "Deduplicated incremental snapshots of your Joplin data using the native Raw export format.",
|
|
8
8
|
"author": "raytray",
|
|
@@ -20,6 +20,6 @@
|
|
|
20
20
|
"screenshots": [],
|
|
21
21
|
"icons": {},
|
|
22
22
|
"promo_tile": {},
|
|
23
|
-
"_publish_hash": "sha256:
|
|
24
|
-
"_publish_commit": "main:
|
|
23
|
+
"_publish_hash": "sha256:85d623a1ab68d6c28d340e1b7319e547f6ac7517083102c447c0eab44fd815a0",
|
|
24
|
+
"_publish_commit": "main:7330557655bf68105b530da5d1829d75e77c2c83"
|
|
25
25
|
}
|