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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 200fb09..0f4c945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/) diff --git a/README.md b/README.md index ff06c94..c07c56e 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. @@ -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) @@ -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) @@ -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 @@ -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`. @@ -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.
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: @@ -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 @@ -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) diff --git a/__test__/backup.test.ts b/__test__/backup.test.ts index c557dde..2f3f9f4 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"; @@ -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(); @@ -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) @@ -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(); @@ -93,6 +107,7 @@ describe("Backup", function () { spyOnShowError.mockReset(); spyOnsSettingsValue.mockReset(); spyOnGlobalValue.mockReset(); + spyOnDataGet.mockReset(); spyOnSaveBackupInfo.mockReset(); }); @@ -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)); @@ -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 () { @@ -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(); + } + ); + }); }); diff --git a/__test__/help.test.ts b/__test__/help.test.ts index 8d364a0..69a9ce2 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 () { @@ -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 + ); + } + ); }); diff --git a/package-lock.json b/package-lock.json index 3970366..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", @@ -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", diff --git a/package.json b/package.json index b937f80..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", @@ -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/Backup.ts b/src/Backup.ts index 2c0c174..183b3b7 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"; @@ -16,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; @@ -28,6 +30,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 +275,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,19 +287,81 @@ 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 (path.normalize(profileDir) === this.backupBasePath) { - this.backupBasePath = null; - await this.showError( - i18n.__("msg.error.backupPathJoplinDir", path.normalize(profileDir)) + // 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 + // 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"; + } + + // 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"; + } + + this.backupBasePath = path.join(this.backupBasePath, profileName); + await handleSubfolderCreation(); + } + + // 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"), + ]; + + if (os.platform() === "win32") { + systemDirectories.push("C:\\Windows"); + } + + 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; + } } } 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"); @@ -478,6 +541,7 @@ class Backup { const backupDst = await this.makeBackupSet(); + await this.writeReadme(this.readmeOutputDirectory); await joplin.settings.setValue( "lastBackup", this.backupStartTime.getTime() @@ -684,6 +748,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/helper.ts b/src/helper.ts index 3726fc2..45eba0c 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,28 @@ export namespace helper { return result; } + + // 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, + + // Testing only + pathModule: typeof path = path + ) { + // 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 = pathModule.resolve(parent) + pathModule.sep; + const normalizedChild = pathModule.resolve(possibleChild) + pathModule.sep; + + return normalizedChild.startsWith(normalizedParent); + } } diff --git a/src/locales/de_DE.json b/src/locales/de_DE.json index 9749df5..18e5b2c 100644 --- a/src/locales/de_DE.json +++ b/src/locales/de_DE.json @@ -13,22 +13,22 @@ "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!", "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!" } }, "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 +36,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 +51,28 @@ "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!" + }, + "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 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,17 +80,18 @@ }, "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", "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.", "command": { "createBackup": "Backup erstellen" } diff --git a/src/locales/en_US.json b/src/locales/en_US.json index 79b6d55..880c49e 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -13,22 +13,23 @@ "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!", + "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!" } }, "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,20 @@ "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}}!" + }, + "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" + "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 +73,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,17 +81,18 @@ }, "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", "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.", "command": { "createBackup": "Create backup" } diff --git a/src/manifest.json b/src/manifest.json index 9edb077..0af6002 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 1, "id": "io.github.jackgruber.backup", "app_min_version": "2.1.3", - "version": "1.3.6", - "name": "Simple Backup", + "version": "1.4.0", + "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", diff --git a/src/settings.ts b/src/settings.ts index bd0c69b..3c58652 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,18 @@ 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"), + }), + }, + 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",