Skip to content

Commit

Permalink
Desktop: Resolves laurent22#9857: Back up to a subdirectory of the ho…
Browse files Browse the repository at this point in the history
…me directory by default
  • Loading branch information
personalizedrefrigerator committed Feb 15, 2024
1 parent 4b52022 commit 2e7b466
Show file tree
Hide file tree
Showing 12 changed files with 267 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ packages/lib/components/shared/side-menu-shared.js
packages/lib/database-driver-better-sqlite.js
packages/lib/database.js
packages/lib/debug/DebugService.js
packages/lib/determineProfileDir.js
packages/lib/determineBaseAppDirs.js
packages/lib/dom.js
packages/lib/errorUtils.js
packages/lib/errors.js
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ packages/lib/components/shared/side-menu-shared.js
packages/lib/database-driver-better-sqlite.js
packages/lib/database.js
packages/lib/debug/DebugService.js
packages/lib/determineProfileDir.js
packages/lib/determineBaseAppDirs.js
packages/lib/dom.js
packages/lib/errorUtils.js
packages/lib/errors.js
Expand Down
4 changes: 2 additions & 2 deletions packages/app-desktop/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const FsDriverNode = require('@joplin/lib/fs-driver-node').default;
const envFromArgs = require('@joplin/lib/envFromArgs');
const packageInfo = require('./packageInfo.js');
const { isCallbackUrl } = require('@joplin/lib/callbackUrlUtils');
const determineProfileDir = require('@joplin/lib/determineProfileDir').default;
const determineBaseAppDirs = require('@joplin/lib/determineBaseAppDirs').default;

// Electron takes the application name from package.json `name` and
// displays this in the tray icon toolip and message box titles, however in
Expand Down Expand Up @@ -45,7 +45,7 @@ const isDebugMode = !!process.argv && process.argv.indexOf('--debug') >= 0;
const appId = `net.cozic.joplin${env === 'dev' ? 'dev' : ''}-desktop`;
let appName = env === 'dev' ? 'joplindev' : 'joplin';
if (appId.indexOf('-desktop') >= 0) appName += '-desktop';
const rootProfileDir = determineProfileDir(profileFromArgs, appName);
const { rootProfileDir } = determineBaseAppDirs(profileFromArgs, appName);
const settingsPath = `${rootProfileDir}/settings.json`;
let autoUploadCrashDumps = false;

Expand Down
4 changes: 2 additions & 2 deletions packages/app-desktop/utils/restartInSafeModeFromMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { safeModeFlagFilename } from '@joplin/lib/BaseApplication';
import initProfile from '@joplin/lib/services/profileConfig/initProfile';
import { writeFile } from 'fs-extra';
import { join } from 'path';
import determineProfileDir from '@joplin/lib/determineProfileDir';
import determineBaseAppDirs from '@joplin/lib/determineBaseAppDirs';


const restartInSafeModeFromMain = async () => {
Expand All @@ -21,7 +21,7 @@ const restartInSafeModeFromMain = async () => {
shimInit({});

const startFlags = await processStartFlags(bridge().processArgv());
const rootProfileDir = determineProfileDir(startFlags.matched.profileDir, appName);
const { rootProfileDir } = determineBaseAppDirs(startFlags.matched.profileDir, appName);
const { profileDir } = await initProfile(rootProfileDir);

// We can't access the database, so write to a file instead.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,230 @@
diff --git a/src/Backup.ts b/src/Backup.ts
index 2c0c174..2bd03f3 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";
@@ -28,6 +29,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 +274,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 +286,56 @@ 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))
+ 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();
+ }
+
+ const handleInvalidPath = async (errorId: string) => {
+ const invalidBackupPath = this.backupBasePath;
+ this.backupBasePath = null;
+ await this.showError(i18n.__(errorId, invalidBackupPath));
+ };
+
+ if (helper.isSubdirectoryOrEqual(this.backupBasePath, os.homedir())) {
+ await handleInvalidPath("msg.error.backupPathContainsHomeDir");
+ } else if (helper.isSubdirectoryOrEqual(this.backupBasePath, profileDir)) {
+ await handleInvalidPath("msg.error.backupPathContainsJoplinDir");
}
}

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");

@@ -477,6 +514,7 @@ class Backup {
await this.backupNotebooks();

const backupDst = await this.makeBackupSet();
+ await this.writeReadme(backupDst);

await joplin.settings.setValue(
"lastBackup",
@@ -684,6 +722,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..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 79b6d55..f9d5325 100644
--- a/src/locales/en_US.json
+++ b/src/locales/en_US.json
@@ -13,7 +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!",
+ "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!"
}
@@ -57,6 +58,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"
@@ -86,6 +91,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.",
"command": {
"createBackup": "Create backup"
}
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,
diff --git a/src/sevenZip.ts b/src/sevenZip.ts
index ef2a527..d98c777 100644
--- a/src/sevenZip.ts
Expand Down Expand Up @@ -38,10 +265,10 @@ index ef2a527..d98c777 100644
export async function setExecutionFlag() {
if (process.platform !== "win32") {
diff --git a/webpack.config.js b/webpack.config.js
index 34a1797..7b2a480 100644
index b32f37f..9b445d2 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -200,15 +200,9 @@ const pluginConfig = { ...baseConfig, entry: './src/index.ts',
@@ -205,15 +205,9 @@ const pluginConfig = { ...baseConfig, entry: './src/index.ts',
path: distDir,
},
plugins: [
Expand Down
2 changes: 1 addition & 1 deletion packages/default-plugins/pluginRepositories.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"io.github.jackgruber.backup": {
"cloneUrl": "https://github.com/JackGruber/joplin-plugin-backup.git",
"branch": "master",
"commit": "bd49c665bf60c1e0dd9b9862b2ba69cad3d4c9ae"
"commit": "a8f29fdd8153c34862c22a9290c4577dcf4b1834"
}
}
5 changes: 3 additions & 2 deletions packages/lib/BaseApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ import RotatingLogs from './RotatingLogs';
import { NoteEntity } from './services/database/types';
import { join } from 'path';
import processStartFlags from './utils/processStartFlags';
import determineProfileDir from './determineProfileDir';
import determineProfileAndBaseDir from './determineBaseAppDirs';

const appLogger: LoggerWrapper = Logger.create('App');

Expand Down Expand Up @@ -639,7 +639,7 @@ export default class BaseApplication {
// https://immerjs.github.io/immer/docs/freezing
setAutoFreeze(initArgs.env === 'dev');

const rootProfileDir = options.rootProfileDir ? options.rootProfileDir : determineProfileDir(initArgs.profileDir, appName);
const { rootProfileDir, homeDir } = determineProfileAndBaseDir(options.rootProfileDir ?? initArgs.profileDir, appName);
const { profileDir, profileConfig, isSubProfile } = await initProfile(rootProfileDir);
this.profileConfig_ = profileConfig;

Expand All @@ -655,6 +655,7 @@ export default class BaseApplication {
Setting.setConstant('pluginDataDir', `${profileDir}/plugin-data`);
Setting.setConstant('cacheDir', cacheDir);
Setting.setConstant('pluginDir', `${rootProfileDir}/plugins`);
Setting.setConstant('homeDir', homeDir);

SyncTargetRegistry.addClass(SyncTargetNone);
SyncTargetRegistry.addClass(SyncTargetFilesystem);
Expand Down
23 changes: 23 additions & 0 deletions packages/lib/determineBaseAppDirs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { homedir } from 'os';
import { toSystemSlashes } from './path-utils';

export default (profileFromArgs: string, appName: string) => {
let profileDir = '';
let homeDir = '';

if (profileFromArgs) {
profileDir = profileFromArgs;
homeDir = profileDir;
} else if (process && process.env && process.env.PORTABLE_EXECUTABLE_DIR) {
profileDir = `${process.env.PORTABLE_EXECUTABLE_DIR}/JoplinProfile`;
homeDir = process.env.PORTABLE_EXECUTABLE_DIR;
} else {
profileDir = `${homedir()}/.config/${appName}`;
homeDir = homedir();
}

return {
rootProfileDir: toSystemSlashes(profileDir, 'linux'),
homeDir: toSystemSlashes(homeDir, 'linux'),
};
};
16 changes: 0 additions & 16 deletions packages/lib/determineProfileDir.ts

This file was deleted.

2 changes: 2 additions & 0 deletions packages/lib/models/Setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export interface Constants {
pluginDataDir: string;
cacheDir: string;
pluginDir: string;
homeDir: string;
flagOpenDevTools: boolean;
syncVersion: number;
startupDevPlugins: string[];
Expand Down Expand Up @@ -303,6 +304,7 @@ class Setting extends BaseModel {
pluginDataDir: '',
cacheDir: '',
pluginDir: '',
homeDir: '',
flagOpenDevTools: false,
syncVersion: 3,
startupDevPlugins: [],
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/services/plugins/PluginService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export interface Plugins {
}

export interface SettingAndValue {
[settingName: string]: string;
[settingName: string]: string|number|boolean;
}

export interface DefaultPluginSettings {
Expand Down
Loading

0 comments on commit 2e7b466

Please sign in to comment.