Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New setting: Allow creating subfolders for each profile #69

35 changes: 18 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. <br>This path is exclusive for the Joplin backups, there should be no other data in it when you disable the `Create Subfolder` settings! | |
| `Keep x backups` | How many backups should be kept | `1` |
| `Backups interval in hours` | Create a backup every X hours | `24` |
| `Only on change` | Creates a backup at the specified backup interval only if there was a change to a `note`, `tag`, `resource` or `notebook` | `false` |
| `Password protected backups` | Protect the backups via encrypted Zip archive. | `false` |
| `Logfile` | Loglevel for backup.log | `error` |
| `Create zip archive` | Save backup data in a Zip archive | `No` |
| `Zip compression Level` | Compression level for zip archive archive | `Copy (no compression)` |
| `Temporary export path` | The data is first exported into this path before it is copied to the backup `Backup path`. | `` |
| `Backup set name` | Name of the backup set if multiple backups are to be keep. [Available moment tokens](https://momentjs.com/docs/#/displaying/format/), which can be used with `{<TOKEN>}` | `{YYYYMMDDHHmm}` |
| `Single JEX` | Create only one JEX file for all, this option is recommended to prevent the loss of internal note links or folder structure during a restore! | `true` |
| `Export format` | Selection of the export format of the notes. | `jex` |
| `Command on Backup finish` | Execute command when backup is finished. | |
| `Create Subfolder` | Create a sub folder `JoplinBackup` in the configured `Backup path`. Deactivate only if there is no other data in the `Backup path`! | `true` |
| `Backup plugins` | Backup the plugin folder from the Joplin profile with all installed plugin jpl files. | `true` |
| Option | Description | Default |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
| `Backup path` | Where to save the backups to. <br>This path is exclusive for the Joplin backups, there should be no other data in it when you disable the `Create Subfolder` settings! | |
| `Keep x backups` | How many backups should be kept | `1` |
| `Backups interval in hours` | Create a backup every X hours | `24` |
| `Only on change` | Creates a backup at the specified backup interval only if there was a change to a `note`, `tag`, `resource` or `notebook` | `false` |
| `Password protected backups` | Protect the backups via encrypted Zip archive. | `false` |
| `Logfile` | Loglevel for backup.log | `error` |
| `Create zip archive` | Save backup data in a Zip archive | `No` |
| `Zip compression Level` | Compression level for zip archive archive | `Copy (no compression)` |
| `Temporary export path` | The data is first exported into this path before it is copied to the backup `Backup path`. | `` |
| `Backup set name` | Name of the backup set if multiple backups are to be keep. [Available moment tokens](https://momentjs.com/docs/#/displaying/format/), which can be used with `{<TOKEN>}` | `{YYYYMMDDHHmm}` |
| `Single JEX` | Create only one JEX file for all, this option is recommended to prevent the loss of internal note links or folder structure during a restore! | `true` |
| `Export format` | Selection of the export format of the notes. | `jex` |
| `Command on Backup finish` | Execute command when backup is finished. | |
| `Create Subfolder` | Create a sub folder `JoplinBackup` in the configured `Backup path`. Deactivate only if there is no other data in the `Backup path`! | `true` |
| `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

Expand Down
65 changes: 65 additions & 0 deletions __test__/backup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,71 @@ describe("Backup", function () {
});
});

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 () {
it(`Create empty folder`, async () => {
const folder = await backup.createEmptyFolder(
Expand Down
47 changes: 42 additions & 5 deletions src/Backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -286,6 +285,41 @@ 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]]
Comment on lines +304 to +310
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what Joplin's profile structure currently looks like. However, it doesn't seem to be documented anywhere...

Reading profiles.json (which seems to not always exist) in the root profile directory may be another way to get this information. It also doesn't seem to be documented though.

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();
}

if (path.normalize(profileDir) === this.backupBasePath) {
Expand All @@ -299,6 +333,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");

Expand Down
4 changes: 4 additions & 0 deletions src/locales/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 9 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ export namespace Settings {
label: i18n.__("settings.createSubfolder.label"),
description: i18n.__("settings.createSubfolder.description"),
},
createSubfolderPerProfile: {
value: false,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should enable this by default ... But not for already installed versions.
Can be made in the function upgradeBackupPluginVersion.

Or what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or what do you think?

Currently, the upstream pull request makes Joplin responsible for setting the default backup directory to the home directory (rather this plugin). As such, there are tradeoffs to having this setting enabled by default.

Some reason(s) to not enable this by default:

  • Extra directories: If the default backup directory for this plugin is kept as the user's profile directory, backups would then look like this: profileDir/JoplinBackup/default. Because each subprofile has its own profile directory, subprofile backups would be in directories like this: profileDir/profile-abcd/JoplinBackup/profile-abcd. Observe that each JoplinBackup directory would have exactly one subdirectory.

Some reasons to enable this by default:

  • Consistency: It will need to be enabled by default in the built-in version of Simple Backup to prevent backups from different profiles from overwriting one another. Enabling it here would then be consistent with the built-in version.
    • The default values of settings in built-in plugins can be overridden here.
  • Data safety: If a user manually changes multiple profiles to back up to the same location, having this setting enabled could prevent backups from being overwritten.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To summarize, I think enabling this by default only makes sense in the version built-in to Joplin. In other versions, the default backup location is within the profile directory, which is different for each profile.

type: SettingItemType.Bool,
section: "backupSection",
public: true,
advanced: true,
label: i18n.__("settings.createSubfolderPerProfile.label"),
description: i18n.__("settings.createSubfolderPerProfile.description"),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might make sense to use SettingStorage.File here so that this setting can be backed up. This wouldn't be consistent with the other settings though.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My opinion is that there must be a Joplin API to backup all plugin settings and restore settings from selected plugins.
In my opinion, SettingStorage.File is not a good idea and not a good way to proceed. If so, Joplin should automatically convert all settings in one version to SettingStorage.File and not provide x ways.
I also don't understand why all the settings from the database have to be in a json file. But that is a different topic.

},
zipArchive: {
value: "no",
type: SettingItemType.String,
Expand Down
Loading