joplin-plugin-incremental-backup 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Raymond Tang
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,125 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Incremental Backup for Joplin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Joplin desktop plugin that creates **deduplicated incremental snapshots** of your notebooks using Joplin's native **Raw export format** — the same format produced by `File > Export > RAW - Joplin Export Directory`.
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
> **Status:** in development. The plugin requires Joplin 3.7+ and desktop only — Joplin's mobile apps do not support plugins.
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
- **Automatic snapshots** — backs up all notebooks on a configurable interval (default: every 24 hours), or on demand from the Tools menu.
|
|
12
|
+
- **Deduplication** — files that are byte-identical to the previous snapshot are stored as hardlinks instead of copies. Changed and new files are stored normally, so every snapshot remains complete on its own.
|
|
13
|
+
- **Content-addressed matching** — files are matched by SHA-256 hash, so even renamed or moved notes don't consume extra space.
|
|
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
|
+
- **Skip unchanged backups** — if no note has been modified since the last snapshot, 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.
|
|
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
|
+
- **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
|
+
- **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
|
+
- **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.
|
|
21
|
+
- **Copy-mode warning** — if a previous snapshot exists but the filesystem doesn't support hardlinks (so every file had to be copied), a warning is shown so you know each snapshot consumes the full vault size.
|
|
22
|
+
- **Durable metadata** — `manifest.json` and `latest.txt` are fsynced before and after the atomic rename, and a best-effort directory fsync is attempted so a power cut can't corrupt the snapshot index. Exported files themselves are left to OS writeback; fsyncing 20 k files per backup would be prohibitive.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
All core features are implemented:
|
|
27
|
+
|
|
28
|
+
| Feature | Status |
|
|
29
|
+
| -------------------------------------------------------------- | ------ |
|
|
30
|
+
| Settings UI (path, interval, retention, skip-if-unchanged) | Done |
|
|
31
|
+
| Scheduled auto-backup (`Tools > Options > Incremental Backup`) | Done |
|
|
32
|
+
| `Create incremental backup` command (Tools menu) | Done |
|
|
33
|
+
| `Verify incremental backup` command (Tools menu) | Done |
|
|
34
|
+
| Deduplicated snapshots with hardlinks + copy fallback | Done |
|
|
35
|
+
| SHA-256 manifest and integrity verification | Done |
|
|
36
|
+
| Retention-based pruning | Done |
|
|
37
|
+
| Multi-profile support (optional subfolder per profile) | Done |
|
|
38
|
+
|
|
39
|
+
## How to use
|
|
40
|
+
|
|
41
|
+
1. Install the plugin and open **Tools > Options > Incremental Backup**.
|
|
42
|
+
2. Set **Backup location** to an absolute path on a local drive (a dedicated external drive or a second partition is ideal — hardlinks don't work across filesystems or on most network shares).
|
|
43
|
+
3. Optionally adjust:
|
|
44
|
+
- **Backup interval (hours)** — `0` disables automatic backups.
|
|
45
|
+
- **Snapshots to keep** — oldest snapshots beyond this count are pruned.
|
|
46
|
+
- **Skip if nothing changed** — on by default.
|
|
47
|
+
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**.
|
|
48
|
+
|
|
49
|
+
### Backup layout
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
<backup path>/
|
|
53
|
+
├── 20261020T143022/ ← snapshot (UTC timestamp)
|
|
54
|
+
│ ├── manifest.json ← file list with SHA-256 hashes
|
|
55
|
+
│ ├── <note-id>.md ← one file per note (Raw export format)
|
|
56
|
+
│ ├── resources/
|
|
57
|
+
│ │ └── <resource-id> ← one file per attached resource
|
|
58
|
+
│ └── ... ← plus folders/tags/note-tags as <id>.md
|
|
59
|
+
├── 20261021T090000/ ← next snapshot (unchanged files hardlinked)
|
|
60
|
+
├── _joplin_active_20261021T090000_12345_abcdef/ ← staging dir (only while running)
|
|
61
|
+
├── latest.txt ← name of the newest snapshot
|
|
62
|
+
└── .backup.lock ← present only while a backup is running
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
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/`.
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
### Scheduler catch-up behaviour
|
|
70
|
+
|
|
71
|
+
When Joplin starts, the plugin checks when the last successful backup finished:
|
|
72
|
+
|
|
73
|
+
- **Never backed up** → runs a backup shortly after startup.
|
|
74
|
+
- **Last backup ≥ 2× the interval ago** (e.g. 48 h for a 24 h interval) → runs a catch-up backup shortly after startup.
|
|
75
|
+
- **Last backup between 1× and 2× the interval ago** → does **not** run at startup; schedules the next run at the next interval boundary (so cadence stays anchored to the last backup). This means if Joplin is closed for most of the day, the effective cadence may stretch from 24 h to 48 h instead of never running at all.
|
|
76
|
+
- **Last backup < 1× the interval ago** → schedules the next run when the interval elapses.
|
|
77
|
+
|
|
78
|
+
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`).
|
|
79
|
+
|
|
80
|
+
After a successful backup, the next one is scheduled exactly one interval later. After a failed backup, the next attempt is scheduled after 5 minutes rather than waiting a full interval.
|
|
81
|
+
|
|
82
|
+
### Encrypted items
|
|
83
|
+
|
|
84
|
+
The Raw exporter **skips items that are still encrypted** and reports them as warnings rather than failing. If your profile is encrypted, unlock it (and let all items finish downloading/decrypting) before running a backup, otherwise the snapshot will be missing those items. When this happens the plugin logs the warnings and shows them in the backup dialog, so an incomplete snapshot is never silently reported as a full success.
|
|
85
|
+
|
|
86
|
+
### Durability trade-off
|
|
87
|
+
|
|
88
|
+
`manifest.json` and `latest.txt` are fsynced so they survive a power cut. The exported note and resource files themselves are **not** fsynced individually — fsyncing 10–20 k files per backup would add significant latency and I/O, especially on USB drives. This means a power loss during a backup could leave some exported files partially written, but:
|
|
89
|
+
|
|
90
|
+
- The lock prevents another run from starting until the previous one finishes or is recovered.
|
|
91
|
+
- On the next run, the staging directory is cleaned up and the export is retried from scratch.
|
|
92
|
+
- `verifyBackup` will detect any corrupted files in existing snapshots.
|
|
93
|
+
|
|
94
|
+
If you need full file-level durability, use a filesystem with journaling (ext4, NTFS, APFS) on a UPS-backed drive.
|
|
95
|
+
|
|
96
|
+
### Restoring a backup
|
|
97
|
+
|
|
98
|
+
Snapshots are plain Joplin Raw exports, so restoring doesn't require this plugin:
|
|
99
|
+
|
|
100
|
+
1. Open Joplin on the machine you want to restore to.
|
|
101
|
+
2. Choose **File > Import > RAW - Joplin Export Directory**.
|
|
102
|
+
3. Select any snapshot folder (e.g. `20261020T143022/`) — its contents are imported as notebooks, notes, tags, and resources.
|
|
103
|
+
|
|
104
|
+
This makes the backups a safe, vendor-independent archive: even without the plugin (or without Joplin itself), your data is readable.
|
|
105
|
+
|
|
106
|
+
## Building from source
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm install
|
|
110
|
+
npm run dist # builds the plugin into publish/
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Other scripts:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npm run format # format the codebase with Prettier
|
|
117
|
+
npm run format:check # check formatting (CI)
|
|
118
|
+
npm run updateVersion # sync version from package.json into the manifest
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
For information on publishing the plugin, see [GENERATOR_DOC.md](./GENERATOR_DOC.md).
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "joplin-plugin-incremental-backup",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive",
|
|
6
|
+
"prepare": "npm run dist",
|
|
7
|
+
"test": "vitest run",
|
|
8
|
+
"test:coverage": "vitest run --coverage",
|
|
9
|
+
"lint": "oxlint src tests",
|
|
10
|
+
"format": "prettier --write .",
|
|
11
|
+
"format:check": "prettier --check .",
|
|
12
|
+
"updateVersion": "webpack --env joplin-plugin-config=updateVersion",
|
|
13
|
+
"update": "npm install -g generator-joplin && yo joplin --node-package-manager npm --update --force"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"joplin-plugin", "backup"
|
|
18
|
+
],
|
|
19
|
+
"files": [
|
|
20
|
+
"publish"
|
|
21
|
+
],
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.20.5",
|
|
24
|
+
"@vitest/coverage-v8": "^5.0.5",
|
|
25
|
+
"chalk": "^4.1.0",
|
|
26
|
+
"copy-webpack-plugin": "^11.0.0",
|
|
27
|
+
"fs-extra": "^10.1.0",
|
|
28
|
+
"glob": "^8.0.3",
|
|
29
|
+
"memfs": "^4.80.0",
|
|
30
|
+
"oxlint": "^1.89.0",
|
|
31
|
+
"prettier": "3.9.10",
|
|
32
|
+
"tar": "^6.1.11",
|
|
33
|
+
"ts-loader": "^9.3.1",
|
|
34
|
+
"typescript": "^4.8.2",
|
|
35
|
+
"vitest": "^5.0.5",
|
|
36
|
+
"webpack": "^5.74.0",
|
|
37
|
+
"webpack-cli": "^4.10.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 1,
|
|
3
|
+
"id": "io.github.raytray.joplin-incremental-backup",
|
|
4
|
+
"app_min_version": "3.7",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"name": "Incremental Backup",
|
|
7
|
+
"description": "Deduplicated incremental snapshots of your Joplin data using the native Raw export format.",
|
|
8
|
+
"author": "raytray",
|
|
9
|
+
"homepage_url": "https://github.com/Raytray/joplin-incremental-backup",
|
|
10
|
+
"repository_url": "https://github.com/Raytray/joplin-incremental-backup",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"backup"
|
|
13
|
+
],
|
|
14
|
+
"categories": [],
|
|
15
|
+
"screenshots": [],
|
|
16
|
+
"icons": {},
|
|
17
|
+
"promo_tile": {},
|
|
18
|
+
"_publish_hash": "sha256:96142d4d2a97ab03564411c06d4627fdc9645e81baaf354389d7669d1093f1cb",
|
|
19
|
+
"_publish_commit": "main:f9b72bd8e168b971d0764df62f2edc8cd20767a4"
|
|
20
|
+
}
|