From 42ac15d2ac9007a1a952fdf20921a00761e181f3 Mon Sep 17 00:00:00 2001 From: Henry H Date: Tue, 6 Feb 2024 09:13:59 -0800 Subject: [PATCH 01/30] Refuse to overwrite the home directory --- __test__/help.test.ts | 10 ++++++++++ src/Backup.ts | 14 ++++++++++---- src/helper.ts | 7 +++++++ src/locales/en_US.json | 1 + 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/__test__/help.test.ts b/__test__/help.test.ts index 8d364a0..ecbce06 100644 --- a/__test__/help.test.ts +++ b/__test__/help.test.ts @@ -146,4 +146,14 @@ describe("Test helper", function () { ).toBe(testCase.expected); } }); + + test.each([ + [ "/tmp/this/is/a/test", "/tmp/this/is/a/test", true ], + [ "/tmp/test", "/tmp/test///", true ], + [ "/tmp/te", "/tmp/test", false ], + [ "a", "/a", false ], + [ "/a/b", "/b/c", false ], + ])("pathsEquivalent (%s ?= %s)", (path1, path2, expected) => { + expect(helper.pathsEquivalent(path1, path2)).toBe(expected); + }); }); diff --git a/src/Backup.ts b/src/Backup.ts index 2c0c174..6e19282 100644 --- a/src/Backup.ts +++ b/src/Backup.ts @@ -4,6 +4,7 @@ import joplin from "api"; import * as path from "path"; import backupLogging from "electron-log"; import * as fs from "fs-extra"; +import * as os from "os"; import { sevenZip } from "./sevenZip"; import * as moment from "moment"; import { helper } from "./helper"; @@ -288,11 +289,16 @@ class Backup { } } - if (path.normalize(profileDir) === this.backupBasePath) { + const handleInvalidPath = async (errorId: string) => { + const invalidBackupPath = this.backupBasePath; this.backupBasePath = null; - await this.showError( - i18n.__("msg.error.backupPathJoplinDir", path.normalize(profileDir)) - ); + await this.showError(i18n.__(errorId, invalidBackupPath)); + }; + + if (helper.pathsEquivalent(profileDir, this.backupBasePath)) { + await handleInvalidPath("msg.error.backupPathJoplinDir"); + } else if (helper.pathsEquivalent(os.homedir(), this.backupBasePath)) { + await handleInvalidPath("msg.error.backupPathHomeDir"); } } diff --git a/src/helper.ts b/src/helper.ts index 3726fc2..cf2ea6f 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,4 +1,5 @@ import joplin from "api"; +import * as path from "path"; export namespace helper { export async function validFileName(fileName: string) { @@ -65,4 +66,10 @@ export namespace helper { return result; } + + export function pathsEquivalent(path1: string, path2: string) { + // We use `resolve` and not `normalize` because `resolve` removes trailing + // slashes, while `normalize` does not. + return path.resolve(path1) === path.resolve(path2); + } } diff --git a/src/locales/en_US.json b/src/locales/en_US.json index 79b6d55..aadf2aa 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -14,6 +14,7 @@ "fileCopy": "Error on file/folder copy in %s: %s", "deleteFile": "Error on file/folder delete in %s: %s", "backupPathJoplinDir": "The backup path is the Joplin profile directory (%s) without subfolders, this is not allowed!", + "backupPathHomeDir": "The backup path is the home directory (%s). Either enable \"createSubfolders\" or choose a different backup directory.", "BackupSetNotSupportedChars": "Backup set name does contain not allowed characters ( %s )!", "passwordDoubleQuotes": "Password contains \" (double quotes), these are not allowed because of a bug. Password protection for the backup is deactivated!" } From 2f8ccdab529566988b1a840b0ae98c60bc79133c Mon Sep 17 00:00:00 2001 From: Henry H Date: Tue, 6 Feb 2024 10:18:24 -0800 Subject: [PATCH 02/30] Don't allow overwriting a parent of the profile directory --- __test__/backup.test.ts | 2 +- __test__/help.test.ts | 29 +++++++++++++++++++++-------- src/Backup.ts | 8 ++++---- src/helper.ts | 20 ++++++++++++++++---- src/locales/de_DE.json | 2 +- src/locales/en_US.json | 4 ++-- 6 files changed, 45 insertions(+), 20 deletions(-) diff --git a/__test__/backup.test.ts b/__test__/backup.test.ts index c557dde..5e96ec9 100644 --- a/__test__/backup.test.ts +++ b/__test__/backup.test.ts @@ -168,7 +168,7 @@ describe("Backup", function () { }); it(`relative paths`, async () => { - const backupPath = "../"; + const backupPath = "../foo"; /* prettier-ignore */ when(spyOnsSettingsValue) .calledWith("path").mockImplementation(() => Promise.resolve(backupPath)); diff --git a/__test__/help.test.ts b/__test__/help.test.ts index ecbce06..a8966df 100644 --- a/__test__/help.test.ts +++ b/__test__/help.test.ts @@ -148,12 +148,25 @@ describe("Test helper", function () { }); test.each([ - [ "/tmp/this/is/a/test", "/tmp/this/is/a/test", true ], - [ "/tmp/test", "/tmp/test///", true ], - [ "/tmp/te", "/tmp/test", false ], - [ "a", "/a", false ], - [ "/a/b", "/b/c", false ], - ])("pathsEquivalent (%s ?= %s)", (path1, path2, expected) => { - expect(helper.pathsEquivalent(path1, path2)).toBe(expected); - }); + // 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 (is %s the parent of %s?)", + (path1, path2, expected) => { + expect(helper.isSubdirectoryOrEqual(path1, path2)).toBe(expected); + } + ); }); diff --git a/src/Backup.ts b/src/Backup.ts index 6e19282..1d817e2 100644 --- a/src/Backup.ts +++ b/src/Backup.ts @@ -295,10 +295,10 @@ class Backup { await this.showError(i18n.__(errorId, invalidBackupPath)); }; - if (helper.pathsEquivalent(profileDir, this.backupBasePath)) { - await handleInvalidPath("msg.error.backupPathJoplinDir"); - } else if (helper.pathsEquivalent(os.homedir(), this.backupBasePath)) { - await handleInvalidPath("msg.error.backupPathHomeDir"); + if (helper.isSubdirectoryOrEqual(this.backupBasePath, os.homedir())) { + await handleInvalidPath("msg.error.backupPathContainsHomeDir"); + } else if (helper.isSubdirectoryOrEqual(this.backupBasePath, profileDir)) { + await handleInvalidPath("msg.error.backupPathContainsJoplinDir"); } } diff --git a/src/helper.ts b/src/helper.ts index cf2ea6f..971a49e 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -67,9 +67,21 @@ export namespace helper { return result; } - export function pathsEquivalent(path1: string, path2: string) { - // We use `resolve` and not `normalize` because `resolve` removes trailing - // slashes, while `normalize` does not. - return path.resolve(path1) === path.resolve(path2); + // Doesn't resolve simlinks + // See https://stackoverflow.com/questions/44892672/how-to-check-if-two-paths-are-the-same-in-npm + // for possible alternative implementations. + export function isSubdirectoryOrEqual(parent: string, possibleChild: string) { + // Appending path.sep to handle this case: + // parent: /a/b/test + // possibleChild: /a/b/test2 + // "/a/b/test2".startsWith("/a/b/test") -> true, but + // "/a/b/test2/".startsWith("/a/b/test/") -> false + // + // Note that .resolve removes trailing slashes. + // + const normalizedParent = path.resolve(parent) + path.sep; + const normalizedChild = path.resolve(possibleChild) + path.sep; + + return normalizedChild.startsWith(normalizedParent); } } diff --git a/src/locales/de_DE.json b/src/locales/de_DE.json index 9749df5..1f6b902 100644 --- a/src/locales/de_DE.json +++ b/src/locales/de_DE.json @@ -13,7 +13,7 @@ "Backup": "Backup Fehler für %s: %s", "fileCopy": "Fehler beim kopieren von Datei/Ordner in %s: %s", "deleteFile": "Fehler beim löschen von Datei/Ordner in %s: %s", - "backupPathJoplinDir": "Als Sicherungs Pfad wurde das Joplin profile Verzeichniss (%s) ohne Unterordner ausgewählt, dies ist nicht erlaubt!", + "backupPathContainsJoplinDir": "Als Sicherungs Pfad wurde das Joplin profile Verzeichniss (%s) ohne Unterordner ausgewählt, dies ist nicht erlaubt!", "BackupSetNotSupportedChars": "Der Name des Backup-Sets enthält nicht zulässige Zeichen ( %s )!", "passwordDoubleQuotes": "Das Passwort enthält \" (Doppelte Anführungszeichen), diese sind wegen eines Bugs nicht erlaubt. Der Passwortschutz für die Backups wurde deaktivert!" } diff --git a/src/locales/en_US.json b/src/locales/en_US.json index aadf2aa..50ad38e 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -13,8 +13,8 @@ "Backup": "Backup error for %s: %s", "fileCopy": "Error on file/folder copy in %s: %s", "deleteFile": "Error on file/folder delete in %s: %s", - "backupPathJoplinDir": "The backup path is the Joplin profile directory (%s) without subfolders, this is not allowed!", - "backupPathHomeDir": "The backup path is the home directory (%s). Either enable \"createSubfolders\" or choose a different backup directory.", + "backupPathContainsJoplinDir": "The backup path is or contains the Joplin profile directory (%s) without subfolders, this is not allowed!", + "backupPathContainsHomeDir": "The backup path is or contains the home directory (%s). Without enabling the subfolder setting, this is not allowed!", "BackupSetNotSupportedChars": "Backup set name does contain not allowed characters ( %s )!", "passwordDoubleQuotes": "Password contains \" (double quotes), these are not allowed because of a bug. Password protection for the backup is deactivated!" } From 09f6cef9e7e4132c0a646623fb2dad971da973c0 Mon Sep 17 00:00:00 2001 From: Henry H Date: Tue, 6 Feb 2024 10:45:31 -0800 Subject: [PATCH 03/30] More tests --- __test__/help.test.ts | 21 +++++++++++++++++++-- src/helper.ts | 12 +++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/__test__/help.test.ts b/__test__/help.test.ts index a8966df..9057163 100644 --- a/__test__/help.test.ts +++ b/__test__/help.test.ts @@ -1,3 +1,4 @@ +import * as path from "path"; import { helper } from "../src/helper"; describe("Test helper", function () { @@ -164,9 +165,25 @@ describe("Test helper", function () { ["a", "/a", false], ["/a/b", "/b/c", false], ])( - "isSubdirectoryOrEqual (is %s the parent of %s?)", + "isSubdirectoryOrEqual with POSIX paths(is %s the parent of %s?)", (path1, path2, expected) => { - expect(helper.isSubdirectoryOrEqual(path1, path2)).toBe(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 + ); } ); }); diff --git a/src/helper.ts b/src/helper.ts index 971a49e..45eba0c 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -70,7 +70,13 @@ export namespace helper { // Doesn't resolve simlinks // See https://stackoverflow.com/questions/44892672/how-to-check-if-two-paths-are-the-same-in-npm // for possible alternative implementations. - export function isSubdirectoryOrEqual(parent: string, possibleChild: string) { + export function isSubdirectoryOrEqual( + parent: string, + possibleChild: string, + + // Testing only + pathModule: typeof path = path + ) { // Appending path.sep to handle this case: // parent: /a/b/test // possibleChild: /a/b/test2 @@ -79,8 +85,8 @@ export namespace helper { // // Note that .resolve removes trailing slashes. // - const normalizedParent = path.resolve(parent) + path.sep; - const normalizedChild = path.resolve(possibleChild) + path.sep; + const normalizedParent = pathModule.resolve(parent) + pathModule.sep; + const normalizedChild = pathModule.resolve(possibleChild) + pathModule.sep; return normalizedChild.startsWith(normalizedParent); } From a4fd675653677729984bcad869d53929bf700a5d Mon Sep 17 00:00:00 2001 From: Henry H Date: Tue, 6 Feb 2024 11:32:07 -0800 Subject: [PATCH 04/30] Add missing space --- __test__/help.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__test__/help.test.ts b/__test__/help.test.ts index 9057163..69a9ce2 100644 --- a/__test__/help.test.ts +++ b/__test__/help.test.ts @@ -165,7 +165,7 @@ describe("Test helper", function () { ["a", "/a", false], ["/a/b", "/b/c", false], ])( - "isSubdirectoryOrEqual with POSIX paths(is %s the parent of %s?)", + "isSubdirectoryOrEqual with POSIX paths (is %s the parent of %s?)", (path1, path2, expected) => { expect(helper.isSubdirectoryOrEqual(path1, path2, path.posix)).toBe( expected From 13c7a73f66067b273c598fd1d7ce567c19e6310c Mon Sep 17 00:00:00 2001 From: Henry H Date: Wed, 7 Feb 2024 09:52:02 -0800 Subject: [PATCH 05/30] Allow creating subfolders for each profile --- __test__/backup.test.ts | 42 +++++++++++++++++++++++++++++++++++++++++ src/Backup.ts | 41 +++++++++++++++++++++++++++++++++++----- src/locales/en_US.json | 4 ++++ src/settings.ts | 9 +++++++++ 4 files changed, 91 insertions(+), 5 deletions(-) diff --git a/__test__/backup.test.ts b/__test__/backup.test.ts index c557dde..b6005c8 100644 --- a/__test__/backup.test.ts +++ b/__test__/backup.test.ts @@ -182,6 +182,48 @@ describe("Backup", function () { }); }); + describe("backups per profile", function () { + test.each([ + { + rootProfileDir: testPath.joplinProfile, + profileDir: testPath.joplinProfile, + expectedProfileName: "default", + }, + { + rootProfileDir: testPath.joplinProfile, + profileDir: path.join(testPath.joplinProfile, "profile-test"), + expectedProfileName: "profile-test", + }, + { + rootProfileDir: testPath.joplinProfile, + profileDir: path.join(testPath.joplinProfile, "profile-idhere"), + expectedProfileName: "profile-idhere", + }, + ])( + "should correctly set backupBasePath based on the current profile name (case %#)", + async ({ profileDir, rootProfileDir, expectedProfileName }) => { + when(spyOnsSettingsValue) + .calledWith("path") + .mockImplementation(async () => testPath.backupBasePath); + when(spyOnGlobalValue) + .calledWith("rootProfileDir") + .mockImplementation(async () => rootProfileDir); + when(spyOnGlobalValue) + .calledWith("profileDir") + .mockImplementation(async () => profileDir); + + // 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 () { it(`Create empty folder`, async () => { const folder = await backup.createEmptyFolder( diff --git a/src/Backup.ts b/src/Backup.ts index 2c0c174..bf2a415 100644 --- a/src/Backup.ts +++ b/src/Backup.ts @@ -28,6 +28,7 @@ class Backup { private compressionLevel: number; private singleJex: boolean; private createSubfolder: boolean; + private createSubfolderPerProfile: boolean; private backupSetName: string; private exportFormat: string; private execFinishCmd: string; @@ -272,12 +273,10 @@ class Backup { ); } - if (this.createSubfolder) { - this.log.verbose("append subFolder"); - const orgBackupBasePath = this.backupBasePath; - this.backupBasePath = path.join(this.backupBasePath, "JoplinBackup"); + const origBackupBasePath = this.backupBasePath; + const handleSubfolderCreation = async () => { if ( - fs.existsSync(orgBackupBasePath) && + fs.existsSync(origBackupBasePath) && !fs.existsSync(this.backupBasePath) ) { try { @@ -286,6 +285,35 @@ class Backup { await this.showError(i18n.__("msg.error.folderCreation", e.message)); } } + }; + + if (this.createSubfolder) { + this.log.verbose("append subFolder"); + this.backupBasePath = path.join(this.backupBasePath, "JoplinBackup"); + await handleSubfolderCreation(); + } + + if (this.createSubfolderPerProfile) { + this.log.verbose("append profile subfolder"); + // We assume that Joplin's profile structure is the following + // rootProfileDir/ + // | profileDir/ + // | | [[profile content]] + // or, if using the default, + // rootProfileDir/ + // | [[profile content]] + const profileRootDir = await joplin.settings.globalValue( + "rootProfileDir" + ); + const profileCurrentDir = await joplin.settings.globalValue("profileDir"); + + let profileName = path.basename(profileCurrentDir); + if (profileCurrentDir === profileRootDir) { + profileName = "default"; + } + + this.backupBasePath = path.join(this.backupBasePath, profileName); + await handleSubfolderCreation(); } if (path.normalize(profileDir) === this.backupBasePath) { @@ -299,6 +327,9 @@ class Backup { public async loadSettings() { this.log.verbose("loadSettings"); this.createSubfolder = await joplin.settings.value("createSubfolder"); + this.createSubfolderPerProfile = await joplin.settings.value( + "createSubfolderPerProfile" + ); await this.loadBackupPath(); this.backupRetention = await joplin.settings.value("backupRetention"); diff --git a/src/locales/en_US.json b/src/locales/en_US.json index 79b6d55..c6916d3 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -57,6 +57,10 @@ "label": "Create Subfolder", "description": "Create a subfolder in the the configured `Backup path`. Deactivate only if there is no other data in the `Backup path`!" }, + "createSubfolderPerProfile": { + "label": "Create subfolder for Joplin profile", + "description": "Create a subfolder within the backup directory for the current profile. This allows multiple profiles from the same Joplin installation to use the same backup directory without overwriting backups made from other profiles. All profiles that use the same backup directory must have this setting enabled." + }, "zipArchive": { "label": "Create archive", "description": "If a password protected backups is set, an archive is always created" diff --git a/src/settings.ts b/src/settings.ts index bd0c69b..e20c5c2 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -136,6 +136,15 @@ export namespace Settings { label: i18n.__("settings.createSubfolder.label"), description: i18n.__("settings.createSubfolder.description"), }, + createSubfolderPerProfile: { + value: false, + type: SettingItemType.Bool, + section: "backupSection", + public: true, + advanced: true, + label: i18n.__("settings.createSubfolderPerProfile.label"), + description: i18n.__("settings.createSubfolderPerProfile.description"), + }, zipArchive: { value: "no", type: SettingItemType.String, From e6f152ea3adf284a5e8d01ff817b8ff8baf0c99a Mon Sep 17 00:00:00 2001 From: Henry H Date: Wed, 7 Feb 2024 10:14:39 -0800 Subject: [PATCH 06/30] Update README.md with new setting info --- README.md | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ff06c94..6620959 100644 --- a/README.md +++ b/README.md @@ -59,23 +59,24 @@ 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.
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 `{}` | `{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` | +| Option | Description | Default | +| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | +| `Backup path` | Where to save the backups to.
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 `{}` | `{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` | +| `Create subfolder for Joplin profile` | Create a subfolder within the backup directory for the current profile. This allows multiple profiles from the same Joplin app to use the same backup directory without overwriting backups made from other profiles. All profiles that use the same backup directory must have this setting enabled. | `false` | +| `Backup plugins` | Backup the plugin folder from the Joplin profile with all installed plugin jpl files. | `true` | ## Keyboard Shortcuts From b4f7612b4cc5954c110aa69da7258675bb7c6b51 Mon Sep 17 00:00:00 2001 From: Henry H Date: Wed, 7 Feb 2024 11:13:15 -0800 Subject: [PATCH 07/30] Feat: Add a README file to the root backup directory --- src/Backup.ts | 11 +++++++++++ src/locales/en_US.json | 1 + 2 files changed, 12 insertions(+) diff --git a/src/Backup.ts b/src/Backup.ts index 2c0c174..6fec441 100644 --- a/src/Backup.ts +++ b/src/Backup.ts @@ -477,6 +477,7 @@ class Backup { await this.backupNotebooks(); const backupDst = await this.makeBackupSet(); + await this.writeReadme(backupDst); await joplin.settings.setValue( "lastBackup", @@ -684,6 +685,16 @@ class Backup { } } + private async writeReadme(backupFolder: string) { + const readmePath = path.join(backupFolder, "README.md"); + this.log.info("writeReadme to", readmePath); + const readmeText = i18n.__( + "backupReadme", + this.backupStartTime.toLocaleString() + ); + await fs.writeFile(readmePath, readmeText, "utf8"); + } + private async backupNotebooks() { const notebooks = await this.selectNotebooks(); diff --git a/src/locales/en_US.json b/src/locales/en_US.json index 79b6d55..771d14c 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -86,6 +86,7 @@ "description": "Execute command when backup is finished" } }, + "backupReadme": "# Joplin Backup\n\nThe backups in this folder were created with the Simple Backup plugin for Joplin. The most recent backup was created on %s.\n\nSee the [Simple Backup documentation](https://joplinapp.org/plugins/plugin/io.github.jackgruber.backup/#restore) for information about how to restore from this backup.", "command": { "createBackup": "Create backup" } From b77920310c9a2cfdc772872e3cab0f9b7095ef0b Mon Sep 17 00:00:00 2001 From: Henry H Date: Wed, 14 Feb 2024 16:21:25 -0800 Subject: [PATCH 08/30] Add automated test --- __test__/backup.test.ts | 37 +++++++++++++++++++++++++++++++++++++ package.json | 2 +- src/locales/en_US.json | 2 +- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/__test__/backup.test.ts b/__test__/backup.test.ts index c557dde..49e6832 100644 --- a/__test__/backup.test.ts +++ b/__test__/backup.test.ts @@ -27,6 +27,7 @@ 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"); @@ -60,6 +61,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(); @@ -93,6 +101,7 @@ describe("Backup", function () { spyOnShowError.mockReset(); spyOnsSettingsValue.mockReset(); spyOnGlobalValue.mockReset(); + spyOnDataGet.mockReset(); spyOnSaveBackupInfo.mockReset(); }); @@ -1014,4 +1023,32 @@ describe("Backup", function () { expect(backup.log.warn).toHaveBeenCalledTimes(0); }); }); + + describe("create backup readme", () => { + it("should create a README.md in the backup directory", async () => { + backup.backupStartTime = null; + backup.passwordEnabled = false; + + when(spyOnsSettingsValue) + .calledWith("zipArchive") + .mockImplementation(() => "no"); + when(spyOnsSettingsValue) + .calledWith("execFinishCmd") + .mockImplementation(() => ""); + + 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(); + }); + }); }); diff --git a/package.json b/package.json index b937f80..52d6de8 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "husky": "^6.0.0", "jest": "^27.0.4", "jest-when": "^3.3.1", - "joplinplugindevtools": "^1.0.15", + "joplinplugindevtools": "^1.0.16", "lint-staged": "^11.0.0", "mime": "^2.5.2", "on-build-webpack": "^0.1.0", diff --git a/src/locales/en_US.json b/src/locales/en_US.json index 771d14c..f7db718 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -86,7 +86,7 @@ "description": "Execute command when backup is finished" } }, - "backupReadme": "# Joplin Backup\n\nThe backups in this folder were created with the Simple Backup plugin for Joplin. The most recent backup was created on %s.\n\nSee the [Simple Backup documentation](https://joplinapp.org/plugins/plugin/io.github.jackgruber.backup/#restore) for information about how to restore from this backup.", + "backupReadme": "# Joplin Backup\n\nThis folder contains one or more backups of data from the Joplin note taking application. The most recent backup was created on %s.\n\nSee the [Simple Backup documentation](https://joplinapp.org/plugins/plugin/io.github.jackgruber.backup/#restore) for information about how to restore from this backup.", "command": { "createBackup": "Create backup" } From 41a2354bee566874ac33f1b3e0bf741dd35667b0 Mon Sep 17 00:00:00 2001 From: Henry H Date: Wed, 14 Feb 2024 22:40:12 -0800 Subject: [PATCH 09/30] Different profile directory name in dev mode --- __test__/backup.test.ts | 25 ++++++++++++++++++++++++- src/Backup.ts | 6 ++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/__test__/backup.test.ts b/__test__/backup.test.ts index b6005c8..781be57 100644 --- a/__test__/backup.test.ts +++ b/__test__/backup.test.ts @@ -187,21 +187,41 @@ describe("Backup", function () { { 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, expectedProfileName }) => { + async ({ + profileDir, + rootProfileDir, + joplinEnv, + expectedProfileName, + }) => { when(spyOnsSettingsValue) .calledWith("path") .mockImplementation(async () => testPath.backupBasePath); @@ -211,6 +231,9 @@ describe("Backup", function () { 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; diff --git a/src/Backup.ts b/src/Backup.ts index bf2a415..6b411a7 100644 --- a/src/Backup.ts +++ b/src/Backup.ts @@ -312,6 +312,12 @@ class Backup { profileName = "default"; } + // This prevents the default joplin-dev profile's backup from overwriting + // a non-dev profile (both have a default profile named "default"). + if ((await joplin.settings.globalValue("env")) === "dev") { + profileName += "-dev"; + } + this.backupBasePath = path.join(this.backupBasePath, profileName); await handleSubfolderCreation(); } From cc1d19aefa30157b2696b4d9f66e0d9a80edd12f Mon Sep 17 00:00:00 2001 From: Henry H Date: Wed, 14 Feb 2024 22:44:27 -0800 Subject: [PATCH 10/30] Adjust comment --- src/Backup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Backup.ts b/src/Backup.ts index 6b411a7..4962375 100644 --- a/src/Backup.ts +++ b/src/Backup.ts @@ -312,8 +312,8 @@ class Backup { profileName = "default"; } - // This prevents the default joplin-dev profile's backup from overwriting - // a non-dev profile (both have a default profile named "default"). + // Appending a -dev to the profile name prevents a devmode default Joplin + // profile from overwriting a non-devmode Joplin profile. if ((await joplin.settings.globalValue("env")) === "dev") { profileName += "-dev"; } From a3e46fb70eba20c61386b9890ccb9c919a7bf791 Mon Sep 17 00:00:00 2001 From: Henry H Date: Sat, 17 Feb 2024 08:26:53 -0800 Subject: [PATCH 11/30] Fix README written to wrong location when revisions are enabled --- __test__/backup.test.ts | 53 ++++++++++++++++++++++------------------- src/Backup.ts | 2 +- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/__test__/backup.test.ts b/__test__/backup.test.ts index 49e6832..5695db0 100644 --- a/__test__/backup.test.ts +++ b/__test__/backup.test.ts @@ -52,7 +52,10 @@ 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); /* prettier-ignore */ when(spyOnGlobalValue) @@ -1025,30 +1028,30 @@ describe("Backup", function () { }); describe("create backup readme", () => { - it("should create a README.md in the backup directory", async () => { - backup.backupStartTime = null; - backup.passwordEnabled = false; - - when(spyOnsSettingsValue) - .calledWith("zipArchive") - .mockImplementation(() => "no"); - when(spyOnsSettingsValue) - .calledWith("execFinishCmd") - .mockImplementation(() => ""); - - 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(""); + it.each([{ backupRetention: 1 }, { backupRetention: 2 }])( + "should create a README.md in the backup directory (case %#)", + async ({ backupRetention }) => { + when(spyOnsSettingsValue) + .calledWith("backupRetention") + .mockImplementation(async () => backupRetention) + .calledWith("backupInfo") + .mockImplementation(() => Promise.resolve("[]")); + + 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(); - }); + // Prevent "open handle" errors + backup.stopTimer(); + } + ); }); }); diff --git a/src/Backup.ts b/src/Backup.ts index 6fec441..36416a9 100644 --- a/src/Backup.ts +++ b/src/Backup.ts @@ -477,8 +477,8 @@ class Backup { await this.backupNotebooks(); const backupDst = await this.makeBackupSet(); - await this.writeReadme(backupDst); + await this.writeReadme(this.backupBasePath); await joplin.settings.setValue( "lastBackup", this.backupStartTime.getTime() From 3ce62fe8f42ef6a19151b11a56aa0b3bb810c47d Mon Sep 17 00:00:00 2001 From: JackGruber <24863925+JackGruber@users.noreply.github.com> Date: Sun, 18 Feb 2024 16:42:49 +0100 Subject: [PATCH 12/30] Rename Plugin --- README.md | 4 ++-- src/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ff06c94..c5d4bcc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Joplin Backup Plugin +# Joplin Plugin: Backup A plugin to extend Joplin with a manual and automatic backup function. @@ -37,7 +37,7 @@ A plugin to extend Joplin with a manual and automatic backup function. ### Automatic - Go to `Tools > Options > Plugins` -- Search for `Simple Backup` +- Search for `Backup` - Click Install plugin - Restart Joplin to enable the plugin diff --git a/src/manifest.json b/src/manifest.json index 9edb077..c08aee7 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -3,7 +3,7 @@ "id": "io.github.jackgruber.backup", "app_min_version": "2.1.3", "version": "1.3.6", - "name": "Simple Backup", + "name": "Backup", "description": "Plugin to create manual and automatic backups.", "author": "JackGruber", "homepage_url": "https://github.com/JackGruber/joplin-plugin-backup/blob/master/README.md", From 0c2efd899b012ec9acddcbcd6a30f1a4a6b2e498 Mon Sep 17 00:00:00 2001 From: JackGruber <24863925+JackGruber@users.noreply.github.com> Date: Sun, 18 Feb 2024 16:44:12 +0100 Subject: [PATCH 13/30] Add changelog info --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 200fb09..1e87035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## not released +- Renamed Plugin from `Simple Backup` to `Backup` + ## v1.3.6 (2024-01-11) - Add: Screenshots / icon for [https://joplinapp.org/plugins/](https://joplinapp.org/plugins/) From e087ab9b4d1b17c3ed4e5284f4062649fcb654b8 Mon Sep 17 00:00:00 2001 From: JackGruber <24863925+JackGruber@users.noreply.github.com> Date: Sun, 18 Feb 2024 17:27:23 +0100 Subject: [PATCH 14/30] Remove settings descriptions from README --- README.md | 18 ------------------ src/locales/de_DE.json | 28 +++++++++++++++------------- src/locales/en_US.json | 22 ++++++++++++---------- src/settings.ts | 6 +++++- 4 files changed, 32 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index c5d4bcc..ef76162 100644 --- a/README.md +++ b/README.md @@ -59,24 +59,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.
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 `{}` | `{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: diff --git a/src/locales/de_DE.json b/src/locales/de_DE.json index 9749df5..7eadb12 100644 --- a/src/locales/de_DE.json +++ b/src/locales/de_DE.json @@ -20,15 +20,16 @@ }, "settings": { "path": { - "label": "Sicherungs Pfad" + "label": "Sicherungs Pfad", + "description": "Speicherort für die Backups. " }, "exportPath": { "label": "Temporärer Export Pfad", - "description": "Temporärer Pfad für die Notizen währen des Exports, bevor diese in den Sicherungs Pfad verschoben werden" + "description": "Temporärer Pfad für den Datenexport aus Joplin, bevor die Daten in den %s verschoben werden" }, "backupRetention": { "label": "Behalte x Sicherungen", - "description": "Wenn mehr als eine Version konfiguriert ist, werden die Ordner im Sicherungspfad entsprechend der Einstellung `Sicherungsset Namen` erstellt" + "description": "Wie viele Sicherungen aufbewahrt werden sollen. Wenn mehr als eine Version konfiguriert ist, werden die Ordner im Sicherungspfad entsprechend der Einstellung 'Sicherungsset Namen' erstellt" }, "backupInterval": { "label": "Sicherungsinterval in Stunden", @@ -36,11 +37,11 @@ }, "onlyOnChange": { "label": "Nur bei änderung", - "description": "Erstellt eine Sicherung im angegebenen Sicherungsintervall nur dann, wenn es eine Änderung in den Notizen gab" + "description": "Erstellt eine Sicherung im angegebenen Sicherungsintervall nur dann, wenn es eine Änderung in den Notizen, Tags, Dateien oder Notizbücher gab" }, "usePassword": { "label": "Passwort geschütztes Sicherung", - "description": "Die Sicherung wird mittels verschlüsseltem ZIP Archive geschützt" + "description": "Die Sicherung wird mittels verschlüsseltem Archive geschützt" }, "password": { "label": "Passwort", @@ -51,23 +52,24 @@ "description": "Wiederholen Sie das Passwort, um dieses zu bestätigen" }, "fileLogLevel": { - "label": "Protokollierungsebene" + "label": "Protokollierungsebene", + "description": "Protokollierungsebene für die Backup Logdatei" }, "createSubfolder": { "label": "Erstellen eines Unterordners", - "description": "Erstellt einen Unterordner im konfigurierten `Sicherungs Pfad`. Nur deaktivieren, wenn sich keine weiteren Daten im `Sicherungs Pfad` befinden!" + "description": "Erstellt einen Unterordner im konfigurierten %s. Nur deaktivieren, wenn sich keine weiteren Daten im %s befinden!" }, "zipArchive": { - "label": "Erstelle ein ZIP-Archive", - "description": "Wenn ein Passwortschutz für die Sicherung eingestellt ist, wird immer ein Zip-Archiv erstellt" + "label": "Erstelle ein Archive", + "description": "Backup Daten in einem Archiv speichern, wenn ein Passwortschutz für die Sicherung eingestellt ist wird immer ein Archiv erstellt" }, "compressionLevel": { "label": "ZIP Komprimierungsgrad", - "description": "Komprimierungsgrad für das ZIP-Archiv" + "description": "Komprimierungsgrad für das Archiv" }, "backupSetName": { "label": "Sicherungsset Namen", - "description": "Name des Sicherungssatzes, wenn mehrere Sicherungen aufbewahrt werden sollen" + "description": "Name des Sicherungssatzes, wenn mehrere Sicherungen aufbewahrt werden sollen. Moment Token (https://momentjs.com/docs/#/displaying/format/) können mittels {TOKEN} verwendet werden" }, "backupPlugins": { "label": "Plugins sichern", @@ -75,11 +77,11 @@ }, "exportFormat": { "label": "Export Format", - "description": "Sicherungsformat für die Notizen" + "description": "Joplin Datenexportformat während der Sicherung" }, "singleJex": { "label": "Eine JEX Datei", - "description": "Erstellen nur eine JEX-Datei für alle Notizbücher (empfohlen, um den Verlust von internen Notizverknüpfungen und der Ordnerstruktur zu vermeiden)" + "description": "Erstellt nur eine JEX Datei (Empfohlen, um den Verlust interner Notizverknüpfungen oder der Ordnerstruktur bei einer Wiederherstellung zu vermeiden!)" }, "execFinishCmd": { "label": "Befehl nach der Sicherung", diff --git a/src/locales/en_US.json b/src/locales/en_US.json index 79b6d55..ed7b840 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -20,15 +20,16 @@ }, "settings": { "path": { - "label": "Backup path" + "label": "Backup path", + "description": "Storage location for the backups. Dieser Pfad ist exklusiv für die Joplin Backups, es sollten sich keine anderen Daten darin befinden, wenn die Einstellungen für 'Unterordner erstellen' deaktiviert wird!" }, "exportPath": { "label": "Temporary export path", - "description": "Temporary path for note export from Joplin, before they are copyed to backup destination" + "description": "Temporary path for data export from Joplin, before the data is moved to the backup path" }, "backupRetention": { "label": "Keep x backups", - "description": "If more than one version is configured, folders are created in the Backup Path acording to backupSetName setting" + "description": "How many backups should be kept. If more than one version configured, folders are created in the Backup Path acording to 'Backup set name' setting" }, "backupInterval": { "label": "Backup interval in hours", @@ -36,7 +37,7 @@ }, "onlyOnChange": { "label": "Only on change", - "description": "Creates a backup at the specified backup interval only if there was a change" + "description": "Creates a backup at the specified backup interval only if there was a change to a `note`, `tag`, `resource` or `notebook`" }, "usePassword": { "label": "Password protected backups", @@ -51,15 +52,16 @@ "description": "Repeat password to validate" }, "fileLogLevel": { - "label": "Loglevel" + "label": "Loglevel", + "description": "Loglevel for the backup logfile" }, "createSubfolder": { "label": "Create Subfolder", - "description": "Create a subfolder in the the configured `Backup path`. Deactivate only if there is no other data in the `Backup path`!" + "description": "Create a subfolder in the the configured {{backupPath}}. Deactivate only if there is no other data in the {{backupPath}}!" }, "zipArchive": { "label": "Create archive", - "description": "If a password protected backups is set, an archive is always created" + "description": "Save backup data in a archive, if a password protected backups is set, an archive is always created" }, "compressionLevel": { "label": "Compression level", @@ -67,7 +69,7 @@ }, "backupSetName": { "label": "Backup set name", - "description": "Name of the backup set if multiple backups are to be keep" + "description": "Name of the backup set if multiple backups are to be kept. Moment Token (https://momentjs.com/docs/#/displaying/format/) can be used with {TOKEN}" }, "backupPlugins": { "label": "Backup plugins", @@ -75,11 +77,11 @@ }, "exportFormat": { "label": "Export format", - "description": "Backup format for the notes" + "description": "Joplin data export format during the backup" }, "singleJex": { "label": "Single JEX", - "description": "Create only one JEX file for all notebooks (Recommended to prevent the loss of internal note links and folder structure)" + "description": "Create only one JEX file (Recommended to prevent the loss of internal note links or folder structure during a restore!)" }, "execFinishCmd": { "label": "Command on Backup finish", diff --git a/src/settings.ts b/src/settings.ts index bd0c69b..09aae2a 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -18,6 +18,7 @@ export namespace Settings { section: "backupSection", public: true, label: i18n.__("settings.path.label"), + description: i18n.__("settings.path.description"), }; let exportPathSettings = null; @@ -119,6 +120,7 @@ export namespace Settings { isEnum: true, public: true, label: i18n.__("settings.fileLogLevel.label"), + description: i18n.__("settings.fileLogLevel.description"), options: { false: "Off", verbose: "Verbose", @@ -134,7 +136,9 @@ export namespace Settings { public: true, advanced: true, label: i18n.__("settings.createSubfolder.label"), - description: i18n.__("settings.createSubfolder.description"), + description: i18n.__("settings.createSubfolder.description", { + backupPath: i18n.__("settings.path.label"), + }), }, zipArchive: { value: "no", From 0ed619938d4936be401793a1abb5369ed84b3c15 Mon Sep 17 00:00:00 2001 From: JackGruber <24863925+JackGruber@users.noreply.github.com> Date: Sun, 18 Feb 2024 17:28:45 +0100 Subject: [PATCH 15/30] Update to node 20 --- .github/workflows/buildAndTest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/buildAndTest.yml b/.github/workflows/buildAndTest.yml index 2dbb471..b719277 100644 --- a/.github/workflows/buildAndTest.yml +++ b/.github/workflows/buildAndTest.yml @@ -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 From 1ab88ec487378b678c10ab4fc1c8bc06b488b670 Mon Sep 17 00:00:00 2001 From: JackGruber <24863925+JackGruber@users.noreply.github.com> Date: Sun, 18 Feb 2024 18:34:32 +0100 Subject: [PATCH 16/30] Change README content --- src/locales/en_US.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/en_US.json b/src/locales/en_US.json index f7db718..d41196f 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -86,7 +86,7 @@ "description": "Execute command when backup is finished" } }, - "backupReadme": "# Joplin Backup\n\nThis folder contains one or more backups of data from the Joplin note taking application. The most recent backup was created on %s.\n\nSee the [Simple Backup documentation](https://joplinapp.org/plugins/plugin/io.github.jackgruber.backup/#restore) for information about how to restore from this backup.", + "backupReadme": "# Joplin Backup\n\nThis folder contains one or more backups of data from the Joplin note taking application.\n\nSee the [Backup documentation](https://joplinapp.org/plugins/plugin/io.github.jackgruber.backup/#restore) for information about how to restore from this backup.\n", "command": { "createBackup": "Create backup" } From fabb0a07c825dc356b7e09f5c8d81df45306f4b2 Mon Sep 17 00:00:00 2001 From: JackGruber <24863925+JackGruber@users.noreply.github.com> Date: Sun, 18 Feb 2024 18:34:51 +0100 Subject: [PATCH 17/30] Add translation --- src/locales/de_DE.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/locales/de_DE.json b/src/locales/de_DE.json index 9749df5..fe6cd00 100644 --- a/src/locales/de_DE.json +++ b/src/locales/de_DE.json @@ -86,6 +86,7 @@ "description": "Befehl/Program nach der Sicherung ausführen" } }, + "backupReadme": "# Joplin Sicherung\n\nDieser Ordner enthält eine oder mehrere Sicherungen aus der Joplin Note Anwendung.\n\nSiehe [Backup documentation](https://joplinapp.org/plugins/plugin/io.github.jackgruber.backup/#restore) für informationen wie eine Sicherung wieder hergestellt werden kann.\n", "command": { "createBackup": "Backup erstellen" } From 42a541b4cfcb6c3b797e97c094aa2a089ff4e7a3 Mon Sep 17 00:00:00 2001 From: JackGruber <24863925+JackGruber@users.noreply.github.com> Date: Sun, 18 Feb 2024 18:35:01 +0100 Subject: [PATCH 18/30] Add lock --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3970366..f6b62dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "husky": "^6.0.0", "jest": "^27.0.4", "jest-when": "^3.3.1", - "joplinplugindevtools": "^1.0.15", + "joplinplugindevtools": "^1.0.16", "lint-staged": "^11.0.0", "mime": "^2.5.2", "on-build-webpack": "^0.1.0", @@ -10352,9 +10352,9 @@ } }, "node_modules/joplinplugindevtools": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/joplinplugindevtools/-/joplinplugindevtools-1.0.15.tgz", - "integrity": "sha512-8x64ZXrF9Zee2/lMHY4gPEKw+0SedEqKGaLorPItbUQ5lWNdCWDWziQgKfvy3s+xj3z1E6EZjfLZ4ZH47/Z7/Q==", + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/joplinplugindevtools/-/joplinplugindevtools-1.0.16.tgz", + "integrity": "sha512-0gbew7BvMUZRo/kx1lxdXq+3a7igpizRbOvk8ltqyH6/WhXv84YXAegrRQYURAPLaVOiusSfUuVYZNRAMbq/OA==", "dev": true, "dependencies": { "@octokit/rest": "^18.12.0", @@ -24814,9 +24814,9 @@ } }, "joplinplugindevtools": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/joplinplugindevtools/-/joplinplugindevtools-1.0.15.tgz", - "integrity": "sha512-8x64ZXrF9Zee2/lMHY4gPEKw+0SedEqKGaLorPItbUQ5lWNdCWDWziQgKfvy3s+xj3z1E6EZjfLZ4ZH47/Z7/Q==", + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/joplinplugindevtools/-/joplinplugindevtools-1.0.16.tgz", + "integrity": "sha512-0gbew7BvMUZRo/kx1lxdXq+3a7igpizRbOvk8ltqyH6/WhXv84YXAegrRQYURAPLaVOiusSfUuVYZNRAMbq/OA==", "dev": true, "requires": { "@octokit/rest": "^18.12.0", From 5e18f15b2e26b85bef61267a877b30b590ecd51d Mon Sep 17 00:00:00 2001 From: JackGruber <24863925+JackGruber@users.noreply.github.com> Date: Sun, 18 Feb 2024 18:37:16 +0100 Subject: [PATCH 19/30] Remove newline --- src/locales/de_DE.json | 2 +- src/locales/en_US.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/locales/de_DE.json b/src/locales/de_DE.json index fe6cd00..5710b99 100644 --- a/src/locales/de_DE.json +++ b/src/locales/de_DE.json @@ -86,7 +86,7 @@ "description": "Befehl/Program nach der Sicherung ausführen" } }, - "backupReadme": "# Joplin Sicherung\n\nDieser Ordner enthält eine oder mehrere Sicherungen aus der Joplin Note Anwendung.\n\nSiehe [Backup documentation](https://joplinapp.org/plugins/plugin/io.github.jackgruber.backup/#restore) für informationen wie eine Sicherung wieder hergestellt werden kann.\n", + "backupReadme": "# Joplin Sicherung\n\nDieser Ordner enthält eine oder mehrere Sicherungen aus der Joplin Note Anwendung.\n\nSiehe [Backup documentation](https://joplinapp.org/plugins/plugin/io.github.jackgruber.backup/#restore) für Informationen wie eine Sicherung wieder hergestellt werden kann.", "command": { "createBackup": "Backup erstellen" } diff --git a/src/locales/en_US.json b/src/locales/en_US.json index d41196f..b18e1ee 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -86,7 +86,7 @@ "description": "Execute command when backup is finished" } }, - "backupReadme": "# Joplin Backup\n\nThis folder contains one or more backups of data from the Joplin note taking application.\n\nSee the [Backup documentation](https://joplinapp.org/plugins/plugin/io.github.jackgruber.backup/#restore) for information about how to restore from this backup.\n", + "backupReadme": "# Joplin Backup\n\nThis folder contains one or more backups of data from the Joplin note taking application.\n\nSee the [Backup documentation](https://joplinapp.org/plugins/plugin/io.github.jackgruber.backup/#restore) for information about how to restore from this backup.", "command": { "createBackup": "Create backup" } From eb22262fb05ef25ea213ab73155392f4cbded86d Mon Sep 17 00:00:00 2001 From: JackGruber <24863925+JackGruber@users.noreply.github.com> Date: Sun, 18 Feb 2024 19:02:32 +0100 Subject: [PATCH 20/30] Add restore workaround singel note --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index ef76162..d609759 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ 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) @@ -93,6 +94,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 From d9254e043f51f975385e156a9d757ee4042fa716 Mon Sep 17 00:00:00 2001 From: JackGruber <24863925+JackGruber@users.noreply.github.com> Date: Sun, 18 Feb 2024 19:04:31 +0100 Subject: [PATCH 21/30] Add info for Joplin profiles --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index d609759..b83b214 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ A plugin to extend Joplin with a manual and automatic backup function. - [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) - [Changelog](#changelog) - [Links](#links) @@ -52,6 +53,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`. @@ -130,6 +132,11 @@ 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. + ## Changelog See [CHANGELOG.md](CHANGELOG.md) From f284a6dc1b8f6d0c7fe11188ade5326b87305465 Mon Sep 17 00:00:00 2001 From: Henry H Date: Mon, 19 Feb 2024 07:32:28 -0800 Subject: [PATCH 22/30] Fix tests --- __test__/backup.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/__test__/backup.test.ts b/__test__/backup.test.ts index 146809e..c98ef82 100644 --- a/__test__/backup.test.ts +++ b/__test__/backup.test.ts @@ -55,7 +55,8 @@ describe("Backup", function () { .calledWith("path").mockImplementation(() => Promise.resolve(testPath.backupBasePath)) .calledWith("zipArchive").mockImplementation(() => "no") .calledWith("execFinishCmd").mockImplementation(() => "") - .calledWith("usePassword").mockImplementation(() => false); + .calledWith("usePassword").mockImplementation(() => false) + .calledWith("createSubfolderPerProfile").mockImplementation(() => false); /* prettier-ignore */ when(spyOnGlobalValue) @@ -1094,7 +1095,7 @@ describe("Backup", function () { describe("create backup readme", () => { it.each([{ backupRetention: 1 }, { backupRetention: 2 }])( - "should create a README.md in the backup directory (case %#)", + "should create a README.md in the backup directory (case %j)", async ({ backupRetention }) => { when(spyOnsSettingsValue) .calledWith("backupRetention") From 9955a8da36c88842118767dd9bc0ab32ba3bc011 Mon Sep 17 00:00:00 2001 From: Henry H Date: Mon, 19 Feb 2024 08:29:09 -0800 Subject: [PATCH 23/30] Add additional directories to disallow list --- __test__/backup.test.ts | 21 +++++++++++++++++++++ src/Backup.ts | 30 +++++++++++++++++++++--------- src/locales/de_DE.json | 1 - src/locales/en_US.json | 3 +-- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/__test__/backup.test.ts b/__test__/backup.test.ts index 5caf4c9..c71831f 100644 --- a/__test__/backup.test.ts +++ b/__test__/backup.test.ts @@ -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"; @@ -34,6 +35,7 @@ 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(); @@ -192,6 +194,25 @@ 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"), + ])( + "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("Div", function () { diff --git a/src/Backup.ts b/src/Backup.ts index 4460112..5738c11 100644 --- a/src/Backup.ts +++ b/src/Backup.ts @@ -289,16 +289,28 @@ class Backup { } } - const handleInvalidPath = async (errorId: string) => { - const invalidBackupPath = this.backupBasePath; - this.backupBasePath = null; - await this.showError(i18n.__(errorId, invalidBackupPath)); - }; + // Creating a backup can overwrite the backup directory. Thus, + // we mark several system and user directories as not-overwritable. + const systemDirectories = [ + profileDir, + os.homedir(), + + path.join(os.homedir(), "Desktop"), + path.join(os.homedir(), "Documents"), + path.join(os.homedir(), "Downloads"), + path.join(os.homedir(), "Pictures"), + "C:\\Windows", + ]; - if (helper.isSubdirectoryOrEqual(this.backupBasePath, os.homedir())) { - await handleInvalidPath("msg.error.backupPathContainsHomeDir"); - } else if (helper.isSubdirectoryOrEqual(this.backupBasePath, profileDir)) { - await handleInvalidPath("msg.error.backupPathContainsJoplinDir"); + for (const systemDirectory of systemDirectories) { + if (helper.isSubdirectoryOrEqual(this.backupBasePath, systemDirectory)) { + const invalidBackupPath = this.backupBasePath; + this.backupBasePath = null; + await this.showError( + i18n.__("msg.error.backupPathContainsImportantDir", invalidBackupPath) + ); + break; + } } } diff --git a/src/locales/de_DE.json b/src/locales/de_DE.json index 0cb7454..340eae5 100644 --- a/src/locales/de_DE.json +++ b/src/locales/de_DE.json @@ -13,7 +13,6 @@ "Backup": "Backup Fehler für %s: %s", "fileCopy": "Fehler beim kopieren von Datei/Ordner in %s: %s", "deleteFile": "Fehler beim löschen von Datei/Ordner in %s: %s", - "backupPathContainsJoplinDir": "Als Sicherungs Pfad wurde das Joplin profile Verzeichniss (%s) ohne Unterordner ausgewählt, dies ist nicht erlaubt!", "BackupSetNotSupportedChars": "Der Name des Backup-Sets enthält nicht zulässige Zeichen ( %s )!", "passwordDoubleQuotes": "Das Passwort enthält \" (Doppelte Anführungszeichen), diese sind wegen eines Bugs nicht erlaubt. Der Passwortschutz für die Backups wurde deaktivert!" } diff --git a/src/locales/en_US.json b/src/locales/en_US.json index 1f535eb..e5ed212 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -13,8 +13,7 @@ "Backup": "Backup error for %s: %s", "fileCopy": "Error on file/folder copy in %s: %s", "deleteFile": "Error on file/folder delete in %s: %s", - "backupPathContainsJoplinDir": "The backup path is or contains the Joplin profile directory (%s) without subfolders, this is not allowed!", - "backupPathContainsHomeDir": "The backup path is or contains the home directory (%s). Without enabling the subfolder setting, this is not allowed!", + "backupPathContainsImportantDir": "The backup path is or contains an important directory (%s) that could be overwritten by a backup. Without enabling the subfolder setting, this is not allowed!", "BackupSetNotSupportedChars": "Backup set name does contain not allowed characters ( %s )!", "passwordDoubleQuotes": "Password contains \" (double quotes), these are not allowed because of a bug. Password protection for the backup is deactivated!" } From 029a6b7a0e0df761b8a41bb194b1a38b52aee624 Mon Sep 17 00:00:00 2001 From: Henry H Date: Mon, 19 Feb 2024 08:40:32 -0800 Subject: [PATCH 24/30] Add comment to tests, only add Windows system directoies to disallow list on Windows --- __test__/backup.test.ts | 4 ++++ src/Backup.ts | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/__test__/backup.test.ts b/__test__/backup.test.ts index c71831f..788c578 100644 --- a/__test__/backup.test.ts +++ b/__test__/backup.test.ts @@ -200,6 +200,10 @@ describe("Backup", function () { 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) => { diff --git a/src/Backup.ts b/src/Backup.ts index 5738c11..aeebbcc 100644 --- a/src/Backup.ts +++ b/src/Backup.ts @@ -299,9 +299,12 @@ class Backup { path.join(os.homedir(), "Documents"), path.join(os.homedir(), "Downloads"), path.join(os.homedir(), "Pictures"), - "C:\\Windows", ]; + if (os.platform() === "win32") { + systemDirectories.push("C:\\Windows"); + } + for (const systemDirectory of systemDirectories) { if (helper.isSubdirectoryOrEqual(this.backupBasePath, systemDirectory)) { const invalidBackupPath = this.backupBasePath; From a402958dbb107e0f12f0abdbcefdbef35e4f3b72 Mon Sep 17 00:00:00 2001 From: Henry H Date: Wed, 21 Feb 2024 10:55:26 -0800 Subject: [PATCH 25/30] Fix README duplicated for each subfolder --- __test__/backup.test.ts | 14 ++++++++++---- src/Backup.ts | 7 ++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/__test__/backup.test.ts b/__test__/backup.test.ts index 98b7c3a..2f3f9f4 100644 --- a/__test__/backup.test.ts +++ b/__test__/backup.test.ts @@ -201,7 +201,7 @@ describe("Backup", function () { 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. @@ -1119,14 +1119,20 @@ describe("Backup", function () { }); describe("create backup readme", () => { - it.each([{ backupRetention: 1 }, { backupRetention: 2 }])( + 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 }) => { + async ({ backupRetention, createSubfolderPerProfile }) => { when(spyOnsSettingsValue) .calledWith("backupRetention") .mockImplementation(async () => backupRetention) .calledWith("backupInfo") - .mockImplementation(() => Promise.resolve("[]")); + .mockImplementation(() => Promise.resolve("[]")) + .calledWith("createSubfolderPerProfile") + .mockImplementation(() => Promise.resolve(createSubfolderPerProfile)); backup.backupStartTime = null; await backup.start(); diff --git a/src/Backup.ts b/src/Backup.ts index 103efed..183b3b7 100644 --- a/src/Backup.ts +++ b/src/Backup.ts @@ -17,6 +17,7 @@ class Backup { private msgDialog: any; private backupBasePath: string; private activeBackupPath: string; + private readmeOutputDirectory: string; private log: any; private logFile: string; private backupRetention: number; @@ -294,6 +295,10 @@ class Backup { await handleSubfolderCreation(); } + // Set the README output directory before adding a subdirectory for the profile. + // This gives us one README for all backup subfolders. + this.readmeOutputDirectory = this.backupBasePath; + if (this.createSubfolderPerProfile) { this.log.verbose("append profile subfolder"); // We assume that Joplin's profile structure is the following @@ -536,7 +541,7 @@ class Backup { const backupDst = await this.makeBackupSet(); - await this.writeReadme(this.backupBasePath); + await this.writeReadme(this.readmeOutputDirectory); await joplin.settings.setValue( "lastBackup", this.backupStartTime.getTime() From ac49c32e8235baf7d02186b2555f27f45b77f4de Mon Sep 17 00:00:00 2001 From: JackGruber <24863925+JackGruber@users.noreply.github.com> Date: Thu, 22 Feb 2024 16:23:00 +0100 Subject: [PATCH 26/30] Add de_DE createSubfolderPerProfile --- src/locales/de_DE.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/locales/de_DE.json b/src/locales/de_DE.json index 340eae5..18e5b2c 100644 --- a/src/locales/de_DE.json +++ b/src/locales/de_DE.json @@ -58,6 +58,10 @@ "label": "Erstellen eines Unterordners", "description": "Erstellt einen Unterordner im konfigurierten %s. Nur deaktivieren, wenn sich keine weiteren Daten im %s befinden!" }, + "createSubfolderPerProfile": { + "label": "Unterordner für Joplin profile", + "description": "Erstellt einen Unterordner innerhalb des Sicherungsverzeichnisses für das aktuelle Profil. Dadurch können mehrere Profile derselben Joplin Installation dasselbe Sicherungsverzeichnis verwenden, ohne dass Sicherungen anderer Profile überschrieben werden. Alle Profile, die dasselbe Sicherungsverzeichnis verwenden, müssen diese Einstellung aktiviert haben" + }, "zipArchive": { "label": "Erstelle ein Archive", "description": "Backup Daten in einem Archiv speichern, wenn ein Passwortschutz für die Sicherung eingestellt ist wird immer ein Archiv erstellt" From 3bb4a135abcbb64c8454d1f2d6f8d599e59d18b7 Mon Sep 17 00:00:00 2001 From: JackGruber <24863925+JackGruber@users.noreply.github.com> Date: Thu, 22 Feb 2024 16:25:23 +0100 Subject: [PATCH 27/30] Add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e87035..c6a65ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## not released - Renamed Plugin from `Simple Backup` to `Backup` +- Add: Allow creating of subfolders for each profile ## v1.3.6 (2024-01-11) From 6f5f2eec08a0627ba69445eb99d440730db8b2ac Mon Sep 17 00:00:00 2001 From: JackGruber <24863925+JackGruber@users.noreply.github.com> Date: Thu, 22 Feb 2024 16:27:27 +0100 Subject: [PATCH 28/30] Add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6a65ed..2a3caca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## not released +- Changes that are required for the Joplin default plugin - Renamed Plugin from `Simple Backup` to `Backup` - Add: Allow creating of subfolders for each profile From 9d0fa77d8bb75003ac4e3cf15c84656be921d2e4 Mon Sep 17 00:00:00 2001 From: JackGruber <24863925+JackGruber@users.noreply.github.com> Date: Thu, 22 Feb 2024 16:45:52 +0100 Subject: [PATCH 29/30] Update installation info --- README.md | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b83b214..c07c56e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ A plugin to extend Joplin with a manual and automatic backup function. - [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) @@ -28,6 +29,7 @@ A plugin to extend Joplin with a manual and automatic backup function. - [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) @@ -36,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 `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 @@ -137,6 +150,10 @@ The note history and file versions (revisions) are not included in the backup. 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) From a9c2d1008be0c0658041d3b523338ec505938b8f Mon Sep 17 00:00:00 2001 From: JackGruber <24863925+JackGruber@users.noreply.github.com> Date: Thu, 22 Feb 2024 16:51:21 +0100 Subject: [PATCH 30/30] bump version 1.4.0 --- CHANGELOG.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- src/manifest.json | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a3caca..0f4c945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 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 diff --git a/package-lock.json b/package-lock.json index f6b62dc..bc4f08a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "joplin-plugin-backup", - "version": "1.3.6", + "version": "1.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "joplin-plugin-backup", - "version": "1.3.6", + "version": "1.4.0", "license": "MIT", "dependencies": { "@types/i18n": "^0.13.6", diff --git a/package.json b/package.json index 52d6de8..7c29698 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "joplin-plugin-backup", - "version": "1.3.6", + "version": "1.4.0", "scripts": { "dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive", "prepare": "npm run dist && husky install", diff --git a/src/manifest.json b/src/manifest.json index c08aee7..0af6002 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 1, "id": "io.github.jackgruber.backup", "app_min_version": "2.1.3", - "version": "1.3.6", + "version": "1.4.0", "name": "Backup", "description": "Plugin to create manual and automatic backups.", "author": "JackGruber",