Skip to content

Commit e7fcf0b

Browse files
committed
Merge branch 'develop'
2 parents a8f29fd + a9c2d10 commit e7fcf0b

13 files changed

+402
-76
lines changed

.github/workflows/buildAndTest.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
- uses: actions/checkout@v2
88
- uses: actions/setup-node@v2
99
with:
10-
node-version: "16"
10+
node-version: "20"
1111
- name: Install dependencies
1212
run: npm install
1313
- name: Build

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## not released
44

5+
## v1.4.0 (2024-02-22)
6+
7+
- Changes that are required for the Joplin default plugin
8+
- Renamed Plugin from `Simple Backup` to `Backup`
9+
- Add: Allow creating of subfolders for each profile
10+
511
## v1.3.6 (2024-01-11)
612

713
- Add: Screenshots / icon for [https://joplinapp.org/plugins/](https://joplinapp.org/plugins/)

README.md

+43-25
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Joplin Backup Plugin <img src=img/icon_32.png>
1+
# Joplin Plugin: Backup <img src=img/icon_32.png>
22

33
A plugin to extend Joplin with a manual and automatic backup function.
44

@@ -12,7 +12,8 @@ A plugin to extend Joplin with a manual and automatic backup function.
1212
<!-- TOC depthfrom:2 orderedlist:false -->
1313

1414
- [Installation](#installation)
15-
- [Automatic](#automatic)
15+
- [Replace Joplin built-in plugin via GUI](#replace-joplin-built-in-plugin-via-gui)
16+
- [Replace Joplin built-in plugin via file system](#replace-joplin-built-in-plugin-via-file-system)
1617
- [Manual](#manual)
1718
- [Usage](#usage)
1819
- [Options](#options)
@@ -21,11 +22,14 @@ A plugin to extend Joplin with a manual and automatic backup function.
2122
- [Restore](#restore)
2223
- [Settings](#settings)
2324
- [Notes](#notes)
25+
- [Restore a singel note](#restore-a-singel-note)
2426
- [FAQ](#faq)
2527
- [Internal Joplin links betwen notes are lost](#internal-joplin-links-betwen-notes-are-lost)
2628
- [Combine multiple JEX Files to one](#combine-multiple-jex-files-to-one)
2729
- [Open a JEX Backup file](#open-a-jex-backup-file)
2830
- [Are Note History Revisions backed up?](#are-note-history-revisions-backed-up)
31+
- [Are all Joplin profiles backed up?](#are-all-joplin-profiles-backed-up)
32+
- [The Joplin build-in version of the plugin cannot be updated](#the-joplin-build-in-version-of-the-plugin-cannot-be-updated)
2933
- [Changelog](#changelog)
3034
- [Links](#links)
3135

@@ -34,12 +38,23 @@ A plugin to extend Joplin with a manual and automatic backup function.
3438

3539
## Installation
3640

37-
### Automatic
41+
The plugin is installed as built-in plugin in Joplin version `2.14.6` and newer.
42+
The built-in plugin cannot be updated via GUI, to update to a other version replace the built-in version.
3843

39-
- Go to `Tools > Options > Plugins`
40-
- Search for `Simple Backup`
41-
- Click Install plugin
42-
- Restart Joplin to enable the plugin
44+
### Replace Joplin built-in plugin via GUI
45+
46+
- Download the latest released JPL package (`io.github.jackgruber.backup.jpl`) from [here](https://github.com/JackGruber/joplin-plugin-backup/releases/latest)
47+
- Go to `Tools > Options > Plugins` in Joplin
48+
- Click on the gear wheel and select `Install from file`
49+
- Select the downloaded JPL file
50+
- Restart Joplin
51+
52+
### Replace Joplin built-in plugin via file system
53+
54+
- Download the latest released JPL package (`io.github.jackgruber.backup.jpl`) from [here](https://github.com/JackGruber/joplin-plugin-backup/releases/latest)
55+
- Close Joplin
56+
- Got to your Joplin profile folder and place the JPL file in the `plugins` folder
57+
- Start Joplin
4358

4459
### Manual
4560

@@ -51,6 +66,7 @@ A plugin to extend Joplin with a manual and automatic backup function.
5166
## Usage
5267

5368
First configure the Plugin under `Tools > Options > Backup`!
69+
The plugin must be configured separately for each Joplin profile.
5470

5571
Backups can be created manually with the command `Tools > Create backup` or are created automatically based on the configured interval.
5672
The backup started manually by `Create backup` respects all the settings except for the `Backups interval in hours`.
@@ -59,24 +75,6 @@ The backup started manually by `Create backup` respects all the settings except
5975

6076
Go to `Tools > Options > Backup`
6177

62-
| Option | Description | Default |
63-
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- |
64-
| `Backup path` | Where to save the backups to. <br>This path is exclusive for the Joplin backups, there should be no other data in it when you disable the `Create Subfolder` settings! | |
65-
| `Keep x backups` | How many backups should be kept | `1` |
66-
| `Backups interval in hours` | Create a backup every X hours | `24` |
67-
| `Only on change` | Creates a backup at the specified backup interval only if there was a change to a `note`, `tag`, `resource` or `notebook` | `false` |
68-
| `Password protected backups` | Protect the backups via encrypted Zip archive. | `false` |
69-
| `Logfile` | Loglevel for backup.log | `error` |
70-
| `Create zip archive` | Save backup data in a Zip archive | `No` |
71-
| `Zip compression Level` | Compression level for zip archive archive | `Copy (no compression)` |
72-
| `Temporary export path` | The data is first exported into this path before it is copied to the backup `Backup path`. | `` |
73-
| `Backup set name` | Name of the backup set if multiple backups are to be keep. [Available moment tokens](https://momentjs.com/docs/#/displaying/format/), which can be used with `{<TOKEN>}` | `{YYYYMMDDHHmm}` |
74-
| `Single JEX` | Create only one JEX file for all, this option is recommended to prevent the loss of internal note links or folder structure during a restore! | `true` |
75-
| `Export format` | Selection of the export format of the notes. | `jex` |
76-
| `Command on Backup finish` | Execute command when backup is finished. | |
77-
| `Create Subfolder` | Create a sub folder `JoplinBackup` in the configured `Backup path`. Deactivate only if there is no other data in the `Backup path`! | `true` |
78-
| `Backup plugins` | Backup the plugin folder from the Joplin profile with all installed plugin jpl files. | `true` |
79-
8078
## Keyboard Shortcuts
8179

8280
Under `Options > Keyboard Shortcuts` you can assign a keyboard shortcut for the following commands:
@@ -111,6 +109,17 @@ The notes are imported via `File > Import > JEX - Joplin Export File`.
111109
The notes are imported additionally, no check for duplicates is performed.
112110
If the notebook in which the note was located already exists in your Joplin, then a "(1)" will be appended to the folder name.
113111

112+
### Restore a singel note
113+
114+
1. Create a new profile in Joplin via `File > Switch profile > Create new Profile`
115+
2. Joplin switches automatically to the newly created profile
116+
3. Import the Backup via `File > Import > JEX - Joplin Export File`
117+
4. Search for the desired note
118+
5. In the note overview, click on the note on the right and select `Export > JEX - Joplin Export File`
119+
6. Save the file on your computer
120+
7. Switch back to your orginal Joplin profil via `File > Switch profile > Default`
121+
8. Import the exported note via `File > Import > JEX - Joplin Export File` and select the file from step 6
122+
114123
## FAQ
115124

116125
### Internal Joplin links betwen notes are lost
@@ -136,6 +145,15 @@ The file names in the archive correspond to the Joplin internal IDs.
136145

137146
The note history and file versions (revisions) are not included in the backup.
138147

148+
### Are all Joplin profiles backed up?
149+
150+
No, the backup must be configured for each profile.
151+
Profiles that are not active are not backed up, even if a backup has been configured.
152+
153+
### The Joplin build-in version of the plugin cannot be updated
154+
155+
Yes, the build-in version only gets updates with Joplin updates, but can be replaced as described in the [Installation](#installation) step.
156+
139157
## Changelog
140158

141159
See [CHANGELOG.md](CHANGELOG.md)

__test__/backup.test.ts

+139-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Backup } from "../src/Backup";
22
import * as fs from "fs-extra";
33
import * as path from "path";
4+
import * as os from "os";
45
import { when } from "jest-when";
56
import { sevenZip } from "../src/sevenZip";
67
import joplin from "api";
@@ -27,12 +28,14 @@ let spyOnLogWarn = null;
2728
let spyOnLogError = null;
2829
let spyOnShowError = null;
2930
let spyOnSaveBackupInfo = null;
31+
let spyOnDataGet = null;
3032

3133
const spyOnsSettingsValue = jest.spyOn(joplin.settings, "value");
3234
const spyOnGlobalValue = jest.spyOn(joplin.settings, "globalValue");
3335
const spyOnSettingsSetValue = jest
3436
.spyOn(joplin.settings, "setValue")
3537
.mockImplementation();
38+
const homeDirMock = jest.spyOn(os, "homedir");
3639

3740
async function createTestStructure() {
3841
const test = await getTestPaths();
@@ -51,7 +54,11 @@ describe("Backup", function () {
5154
when(spyOnsSettingsValue)
5255
.mockImplementation(() => Promise.resolve("no mockImplementation"))
5356
.calledWith("fileLogLevel").mockImplementation(() => Promise.resolve("error"))
54-
.calledWith("path").mockImplementation(() => Promise.resolve(testPath.backupBasePath));
57+
.calledWith("path").mockImplementation(() => Promise.resolve(testPath.backupBasePath))
58+
.calledWith("zipArchive").mockImplementation(() => "no")
59+
.calledWith("execFinishCmd").mockImplementation(() => "")
60+
.calledWith("usePassword").mockImplementation(() => false)
61+
.calledWith("createSubfolderPerProfile").mockImplementation(() => false);
5562

5663
/* prettier-ignore */
5764
when(spyOnGlobalValue)
@@ -60,6 +67,13 @@ describe("Backup", function () {
6067
.calledWith("locale").mockImplementation(() => Promise.resolve("en_US"))
6168
.calledWith("templateDir").mockImplementation(() => Promise.resolve(testPath.templates));
6269

70+
spyOnDataGet = jest
71+
.spyOn(joplin.data, "get")
72+
.mockImplementation(async (_path, _query) => ({
73+
items: [],
74+
hasMore: false,
75+
}));
76+
6377
await createTestStructure();
6478
backup = new Backup() as any;
6579
backup.backupStartTime = new Date();
@@ -93,6 +107,7 @@ describe("Backup", function () {
93107
spyOnShowError.mockReset();
94108
spyOnsSettingsValue.mockReset();
95109
spyOnGlobalValue.mockReset();
110+
spyOnDataGet.mockReset();
96111
spyOnSaveBackupInfo.mockReset();
97112
});
98113

@@ -168,7 +183,7 @@ describe("Backup", function () {
168183
});
169184

170185
it(`relative paths`, async () => {
171-
const backupPath = "../";
186+
const backupPath = "../foo";
172187
/* prettier-ignore */
173188
when(spyOnsSettingsValue)
174189
.calledWith("path").mockImplementation(() => Promise.resolve(backupPath));
@@ -180,6 +195,94 @@ describe("Backup", function () {
180195
expect(backup.log.error).toHaveBeenCalledTimes(0);
181196
expect(backup.log.warn).toHaveBeenCalledTimes(0);
182197
});
198+
199+
it.each([
200+
os.homedir(),
201+
path.dirname(os.homedir()),
202+
path.join(os.homedir(), "Desktop"),
203+
path.join(os.homedir(), "Documents"),
204+
205+
// Avoid including system-specific paths here. For example,
206+
// testing with "C:\Windows" fails on POSIX systems because it is interpreted
207+
// as a relative path.
208+
])(
209+
"should not allow backup path (%s) to be an important system directory",
210+
async (path) => {
211+
when(spyOnsSettingsValue)
212+
.calledWith("path")
213+
.mockImplementation(() => Promise.resolve(path));
214+
backup.createSubfolder = false;
215+
216+
await backup.loadBackupPath();
217+
218+
expect(backup.backupBasePath).toBe(null);
219+
}
220+
);
221+
});
222+
223+
describe("backups per profile", function () {
224+
test.each([
225+
{
226+
rootProfileDir: testPath.joplinProfile,
227+
profileDir: testPath.joplinProfile,
228+
joplinEnv: "prod",
229+
expectedProfileName: "default",
230+
},
231+
{
232+
rootProfileDir: testPath.joplinProfile,
233+
profileDir: testPath.joplinProfile,
234+
joplinEnv: "dev",
235+
expectedProfileName: "default-dev",
236+
},
237+
{
238+
rootProfileDir: testPath.joplinProfile,
239+
profileDir: path.join(testPath.joplinProfile, "profile-test"),
240+
joplinEnv: "prod",
241+
expectedProfileName: "profile-test",
242+
},
243+
{
244+
rootProfileDir: testPath.joplinProfile,
245+
profileDir: path.join(testPath.joplinProfile, "profile-idhere"),
246+
joplinEnv: "prod",
247+
expectedProfileName: "profile-idhere",
248+
},
249+
{
250+
rootProfileDir: testPath.joplinProfile,
251+
profileDir: path.join(testPath.joplinProfile, "profile-idhere"),
252+
joplinEnv: "dev",
253+
expectedProfileName: "profile-idhere-dev",
254+
},
255+
])(
256+
"should correctly set backupBasePath based on the current profile name (case %#)",
257+
async ({
258+
profileDir,
259+
rootProfileDir,
260+
joplinEnv,
261+
expectedProfileName,
262+
}) => {
263+
when(spyOnsSettingsValue)
264+
.calledWith("path")
265+
.mockImplementation(async () => testPath.backupBasePath);
266+
when(spyOnGlobalValue)
267+
.calledWith("rootProfileDir")
268+
.mockImplementation(async () => rootProfileDir);
269+
when(spyOnGlobalValue)
270+
.calledWith("profileDir")
271+
.mockImplementation(async () => profileDir);
272+
when(spyOnGlobalValue)
273+
.calledWith("env")
274+
.mockImplementation(async () => joplinEnv);
275+
276+
// Should use the folder named "default" for the default profile
277+
backup.createSubfolderPerProfile = true;
278+
await backup.loadBackupPath();
279+
expect(backup.backupBasePath).toBe(
280+
path.normalize(
281+
path.join(testPath.backupBasePath, expectedProfileName)
282+
)
283+
);
284+
}
285+
);
183286
});
184287

185288
describe("Div", function () {
@@ -1014,4 +1117,38 @@ describe("Backup", function () {
10141117
expect(backup.log.warn).toHaveBeenCalledTimes(0);
10151118
});
10161119
});
1120+
1121+
describe("create backup readme", () => {
1122+
it.each([
1123+
{ backupRetention: 1, createSubfolderPerProfile: false },
1124+
{ backupRetention: 2, createSubfolderPerProfile: false },
1125+
{ backupRetention: 1, createSubfolderPerProfile: true },
1126+
])(
1127+
"should create a README.md in the backup directory (case %j)",
1128+
async ({ backupRetention, createSubfolderPerProfile }) => {
1129+
when(spyOnsSettingsValue)
1130+
.calledWith("backupRetention")
1131+
.mockImplementation(async () => backupRetention)
1132+
.calledWith("backupInfo")
1133+
.mockImplementation(() => Promise.resolve("[]"))
1134+
.calledWith("createSubfolderPerProfile")
1135+
.mockImplementation(() => Promise.resolve(createSubfolderPerProfile));
1136+
1137+
backup.backupStartTime = null;
1138+
await backup.start();
1139+
1140+
// Should exist and be non-empty
1141+
const readmePath = path.join(
1142+
testPath.backupBasePath,
1143+
"JoplinBackup",
1144+
"README.md"
1145+
);
1146+
expect(await fs.pathExists(readmePath)).toBe(true);
1147+
expect(await fs.readFile(readmePath, "utf8")).not.toBe("");
1148+
1149+
// Prevent "open handle" errors
1150+
backup.stopTimer();
1151+
}
1152+
);
1153+
});
10171154
});

__test__/help.test.ts

+40
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as path from "path";
12
import { helper } from "../src/helper";
23

34
describe("Test helper", function () {
@@ -146,4 +147,43 @@ describe("Test helper", function () {
146147
).toBe(testCase.expected);
147148
}
148149
});
150+
151+
test.each([
152+
// Equality
153+
["/tmp/this/is/a/test", "/tmp/this/is/a/test", true],
154+
["/tmp/test", "/tmp/test///", true],
155+
156+
// Subdirectories
157+
["/tmp", "/tmp/test", true],
158+
["/tmp/", "/tmp/test", true],
159+
["/tmp/", "/tmp/..test", true],
160+
["/tmp/test", "/tmp/", false],
161+
162+
// Different directories
163+
["/tmp/", "/tmp/../test", false],
164+
["/tmp/te", "/tmp/test", false],
165+
["a", "/a", false],
166+
["/a/b", "/b/c", false],
167+
])(
168+
"isSubdirectoryOrEqual with POSIX paths (is %s the parent of %s?)",
169+
(path1, path2, expected) => {
170+
expect(helper.isSubdirectoryOrEqual(path1, path2, path.posix)).toBe(
171+
expected
172+
);
173+
}
174+
);
175+
176+
test.each([
177+
["C:\\Users\\User\\", "C:\\Users\\User\\", true],
178+
["D:\\Users\\User\\", "C:\\Users\\User\\", false],
179+
["C:\\Users\\Userr\\", "C:\\Users\\User\\", false],
180+
["C:\\Users\\User\\", "C:\\Users\\User\\.config\\joplin-desktop", true],
181+
])(
182+
"isSubdirectoryOrEqual with Windows paths (is %s the parent of %s?)",
183+
(path1, path2, expected) => {
184+
expect(helper.isSubdirectoryOrEqual(path1, path2, path.win32)).toBe(
185+
expected
186+
);
187+
}
188+
);
149189
});

0 commit comments

Comments
 (0)