Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
JackGruber committed Feb 22, 2024
2 parents a8f29fd + a9c2d10 commit e7fcf0b
Show file tree
Hide file tree
Showing 13 changed files with 402 additions and 76 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/buildAndTest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: "16"
node-version: "20"
- name: Install dependencies
run: npm install
- name: Build
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## not released

## v1.4.0 (2024-02-22)

- Changes that are required for the Joplin default plugin
- Renamed Plugin from `Simple Backup` to `Backup`
- Add: Allow creating of subfolders for each profile

## v1.3.6 (2024-01-11)

- Add: Screenshots / icon for [https://joplinapp.org/plugins/](https://joplinapp.org/plugins/)
Expand Down
68 changes: 43 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Joplin Backup Plugin <img src=img/icon_32.png>
# Joplin Plugin: Backup <img src=img/icon_32.png>

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

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

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

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

## Installation

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

- Go to `Tools > Options > Plugins`
- Search for `Simple Backup`
- Click Install plugin
- Restart Joplin to enable the plugin
### Replace Joplin built-in plugin via GUI

- Download the latest released JPL package (`io.github.jackgruber.backup.jpl`) from [here](https://github.com/JackGruber/joplin-plugin-backup/releases/latest)
- Go to `Tools > Options > Plugins` in Joplin
- Click on the gear wheel and select `Install from file`
- Select the downloaded JPL file
- Restart Joplin

### Replace Joplin built-in plugin via file system

- Download the latest released JPL package (`io.github.jackgruber.backup.jpl`) from [here](https://github.com/JackGruber/joplin-plugin-backup/releases/latest)
- Close Joplin
- Got to your Joplin profile folder and place the JPL file in the `plugins` folder
- Start Joplin

### Manual

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

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

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

Go to `Tools > Options > Backup`

| Option | Description | Default |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- |
| `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! | |
| `Keep x backups` | How many backups should be kept | `1` |
| `Backups interval in hours` | Create a backup every X hours | `24` |
| `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` |
| `Password protected backups` | Protect the backups via encrypted Zip archive. | `false` |
| `Logfile` | Loglevel for backup.log | `error` |
| `Create zip archive` | Save backup data in a Zip archive | `No` |
| `Zip compression Level` | Compression level for zip archive archive | `Copy (no compression)` |
| `Temporary export path` | The data is first exported into this path before it is copied to the backup `Backup path`. | `` |
| `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}` |
| `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` |
| `Export format` | Selection of the export format of the notes. | `jex` |
| `Command on Backup finish` | Execute command when backup is finished. | |
| `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` |
| `Backup plugins` | Backup the plugin folder from the Joplin profile with all installed plugin jpl files. | `true` |

## Keyboard Shortcuts

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

### Restore a singel note

1. Create a new profile in Joplin via `File > Switch profile > Create new Profile`
2. Joplin switches automatically to the newly created profile
3. Import the Backup via `File > Import > JEX - Joplin Export File`
4. Search for the desired note
5. In the note overview, click on the note on the right and select `Export > JEX - Joplin Export File`
6. Save the file on your computer
7. Switch back to your orginal Joplin profil via `File > Switch profile > Default`
8. Import the exported note via `File > Import > JEX - Joplin Export File` and select the file from step 6

## FAQ

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

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

### Are all Joplin profiles backed up?

No, the backup must be configured for each profile.
Profiles that are not active are not backed up, even if a backup has been configured.

### The Joplin build-in version of the plugin cannot be updated

Yes, the build-in version only gets updates with Joplin updates, but can be replaced as described in the [Installation](#installation) step.

## Changelog

See [CHANGELOG.md](CHANGELOG.md)
Expand Down
141 changes: 139 additions & 2 deletions __test__/backup.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Backup } from "../src/Backup";
import * as fs from "fs-extra";
import * as path from "path";
import * as os from "os";
import { when } from "jest-when";
import { sevenZip } from "../src/sevenZip";
import joplin from "api";
Expand All @@ -27,12 +28,14 @@ let spyOnLogWarn = null;
let spyOnLogError = null;
let spyOnShowError = null;
let spyOnSaveBackupInfo = null;
let spyOnDataGet = null;

const spyOnsSettingsValue = jest.spyOn(joplin.settings, "value");
const spyOnGlobalValue = jest.spyOn(joplin.settings, "globalValue");
const spyOnSettingsSetValue = jest
.spyOn(joplin.settings, "setValue")
.mockImplementation();
const homeDirMock = jest.spyOn(os, "homedir");

async function createTestStructure() {
const test = await getTestPaths();
Expand All @@ -51,7 +54,11 @@ describe("Backup", function () {
when(spyOnsSettingsValue)
.mockImplementation(() => Promise.resolve("no mockImplementation"))
.calledWith("fileLogLevel").mockImplementation(() => Promise.resolve("error"))
.calledWith("path").mockImplementation(() => Promise.resolve(testPath.backupBasePath));
.calledWith("path").mockImplementation(() => Promise.resolve(testPath.backupBasePath))
.calledWith("zipArchive").mockImplementation(() => "no")
.calledWith("execFinishCmd").mockImplementation(() => "")
.calledWith("usePassword").mockImplementation(() => false)
.calledWith("createSubfolderPerProfile").mockImplementation(() => false);

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

spyOnDataGet = jest
.spyOn(joplin.data, "get")
.mockImplementation(async (_path, _query) => ({
items: [],
hasMore: false,
}));

await createTestStructure();
backup = new Backup() as any;
backup.backupStartTime = new Date();
Expand Down Expand Up @@ -93,6 +107,7 @@ describe("Backup", function () {
spyOnShowError.mockReset();
spyOnsSettingsValue.mockReset();
spyOnGlobalValue.mockReset();
spyOnDataGet.mockReset();
spyOnSaveBackupInfo.mockReset();
});

Expand Down Expand Up @@ -168,7 +183,7 @@ describe("Backup", function () {
});

it(`relative paths`, async () => {
const backupPath = "../";
const backupPath = "../foo";
/* prettier-ignore */
when(spyOnsSettingsValue)
.calledWith("path").mockImplementation(() => Promise.resolve(backupPath));
Expand All @@ -180,6 +195,94 @@ describe("Backup", function () {
expect(backup.log.error).toHaveBeenCalledTimes(0);
expect(backup.log.warn).toHaveBeenCalledTimes(0);
});

it.each([
os.homedir(),
path.dirname(os.homedir()),
path.join(os.homedir(), "Desktop"),
path.join(os.homedir(), "Documents"),

// Avoid including system-specific paths here. For example,
// testing with "C:\Windows" fails on POSIX systems because it is interpreted
// as a relative path.
])(
"should not allow backup path (%s) to be an important system directory",
async (path) => {
when(spyOnsSettingsValue)
.calledWith("path")
.mockImplementation(() => Promise.resolve(path));
backup.createSubfolder = false;

await backup.loadBackupPath();

expect(backup.backupBasePath).toBe(null);
}
);
});

describe("backups per profile", function () {
test.each([
{
rootProfileDir: testPath.joplinProfile,
profileDir: testPath.joplinProfile,
joplinEnv: "prod",
expectedProfileName: "default",
},
{
rootProfileDir: testPath.joplinProfile,
profileDir: testPath.joplinProfile,
joplinEnv: "dev",
expectedProfileName: "default-dev",
},
{
rootProfileDir: testPath.joplinProfile,
profileDir: path.join(testPath.joplinProfile, "profile-test"),
joplinEnv: "prod",
expectedProfileName: "profile-test",
},
{
rootProfileDir: testPath.joplinProfile,
profileDir: path.join(testPath.joplinProfile, "profile-idhere"),
joplinEnv: "prod",
expectedProfileName: "profile-idhere",
},
{
rootProfileDir: testPath.joplinProfile,
profileDir: path.join(testPath.joplinProfile, "profile-idhere"),
joplinEnv: "dev",
expectedProfileName: "profile-idhere-dev",
},
])(
"should correctly set backupBasePath based on the current profile name (case %#)",
async ({
profileDir,
rootProfileDir,
joplinEnv,
expectedProfileName,
}) => {
when(spyOnsSettingsValue)
.calledWith("path")
.mockImplementation(async () => testPath.backupBasePath);
when(spyOnGlobalValue)
.calledWith("rootProfileDir")
.mockImplementation(async () => rootProfileDir);
when(spyOnGlobalValue)
.calledWith("profileDir")
.mockImplementation(async () => profileDir);
when(spyOnGlobalValue)
.calledWith("env")
.mockImplementation(async () => joplinEnv);

// Should use the folder named "default" for the default profile
backup.createSubfolderPerProfile = true;
await backup.loadBackupPath();
expect(backup.backupBasePath).toBe(
path.normalize(
path.join(testPath.backupBasePath, expectedProfileName)
)
);
}
);
});

describe("Div", function () {
Expand Down Expand Up @@ -1014,4 +1117,38 @@ describe("Backup", function () {
expect(backup.log.warn).toHaveBeenCalledTimes(0);
});
});

describe("create backup readme", () => {
it.each([
{ backupRetention: 1, createSubfolderPerProfile: false },
{ backupRetention: 2, createSubfolderPerProfile: false },
{ backupRetention: 1, createSubfolderPerProfile: true },
])(
"should create a README.md in the backup directory (case %j)",
async ({ backupRetention, createSubfolderPerProfile }) => {
when(spyOnsSettingsValue)
.calledWith("backupRetention")
.mockImplementation(async () => backupRetention)
.calledWith("backupInfo")
.mockImplementation(() => Promise.resolve("[]"))
.calledWith("createSubfolderPerProfile")
.mockImplementation(() => Promise.resolve(createSubfolderPerProfile));

backup.backupStartTime = null;
await backup.start();

// Should exist and be non-empty
const readmePath = path.join(
testPath.backupBasePath,
"JoplinBackup",
"README.md"
);
expect(await fs.pathExists(readmePath)).toBe(true);
expect(await fs.readFile(readmePath, "utf8")).not.toBe("");

// Prevent "open handle" errors
backup.stopTimer();
}
);
});
});
40 changes: 40 additions & 0 deletions __test__/help.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as path from "path";
import { helper } from "../src/helper";

describe("Test helper", function () {
Expand Down Expand Up @@ -146,4 +147,43 @@ describe("Test helper", function () {
).toBe(testCase.expected);
}
});

test.each([
// Equality
["/tmp/this/is/a/test", "/tmp/this/is/a/test", true],
["/tmp/test", "/tmp/test///", true],

// Subdirectories
["/tmp", "/tmp/test", true],
["/tmp/", "/tmp/test", true],
["/tmp/", "/tmp/..test", true],
["/tmp/test", "/tmp/", false],

// Different directories
["/tmp/", "/tmp/../test", false],
["/tmp/te", "/tmp/test", false],
["a", "/a", false],
["/a/b", "/b/c", false],
])(
"isSubdirectoryOrEqual with POSIX paths (is %s the parent of %s?)",
(path1, path2, expected) => {
expect(helper.isSubdirectoryOrEqual(path1, path2, path.posix)).toBe(
expected
);
}
);

test.each([
["C:\\Users\\User\\", "C:\\Users\\User\\", true],
["D:\\Users\\User\\", "C:\\Users\\User\\", false],
["C:\\Users\\Userr\\", "C:\\Users\\User\\", false],
["C:\\Users\\User\\", "C:\\Users\\User\\.config\\joplin-desktop", true],
])(
"isSubdirectoryOrEqual with Windows paths (is %s the parent of %s?)",
(path1, path2, expected) => {
expect(helper.isSubdirectoryOrEqual(path1, path2, path.win32)).toBe(
expected
);
}
);
});
Loading

0 comments on commit e7fcf0b

Please sign in to comment.