Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
JackGruber committed Jul 11, 2022
2 parents 3d22802 + b451a3a commit 00ed521
Show file tree
Hide file tree
Showing 13 changed files with 458 additions and 185 deletions.
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
/dist
tsconfig.json
webpack.config.js
.env
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## not released

## v1.1.0 (2022-07-11)

- Improved: The default setting for `Single JEX` is now `true` (Create only one JEX backup file) to prevent the loss of internal notelinks during a restore. Joplin Discourse: [Lost all connections of my notes](https://discourse.joplinapp.org/t/lost-all-connections-of-my-notes/25464)
- Improved: Create a sub folder `JoplinBackup` in the configured `Backup path` (Only for new installations).
- Improved: Use new Joplin DirectoryPath selector for path selection. Not supported in Joplin >= v2.9.1, non-compatible Joplin versions still use a text input field.
- Add: Backup all installed Joplin plugins (Only the jpl files, no plugin settings!)

## v1.0.5 (2021-12-05)

- Fix: #28 No message was displayed that the backup is finished when starting manually
Expand Down
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ 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! | |
| `Single JEX` | Create only one JEX file for all notebooks | `false` |
| `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` |
Expand All @@ -44,6 +43,9 @@ Go to `Tools > Options > Backup`
| `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` |
| `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

Expand All @@ -59,6 +61,7 @@ Under `Options > Keyboard Shortcuts` you can assign a keyboard shortcut for the
- The `userchrome.css` (Your Joplin customization)
- The `userstyle.css` (Your Joplin customization)
- The `templates` folder (Note templates)
- The `plugin` folder (All installed plugins, no plugin settings!)

## Restore

Expand All @@ -78,6 +81,27 @@ The notes are imported via `File > Import > JEX - Joplin Export File`.
The notes are imported additionally, no check for duplicates is performed.
If the folder in which the note was located already exists in you Joplin, than the folder name is extended by one (1).

## FAQ

### Internal Joplin links betwen notes are lost

If several JEX files are imported and the notes have links to each other, these links will be lost.
Therefore it is recommended to create a Single JEX Backup!

### Compine multiple JEX Files to one

By combining the JEX files into one, the Joplin internal links will work again after the import.

1. Open one of the JEX files in a ZIP program like 7-Zip
2. Open a second JEX and add all files to the first JEX
3. Repeat step 2 for all files
4. Import first JEX which now contains all notes

## Open a JEX Backup file

A Joplin JEX Backup file is a tar archive which can be opened with any zip program that supports TAR archive.
The file names in the archive correspond to the Joplin internal IDs.

## Changelog

See [CHANGELOG.md](CHANGELOG.md)
Expand Down
89 changes: 69 additions & 20 deletions __test__/backup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ let spyOnLogVerbose = null;
let spyOnLogInfo = null;
let spyOnLogWarn = null;
let spyOnLogError = null;
let spyOnShowError = null;
let spyOnSaveBackupInfo = null;

const spyOnsSettingsValue = jest.spyOn(joplin.settings, "value");
Expand Down Expand Up @@ -73,13 +74,18 @@ describe("Backup", function () {
spyOnLogError = jest
.spyOn(backup.log, "error")
.mockImplementation(() => {});

spyOnShowError = jest
.spyOn(backup, "showError")
.mockImplementation(() => {});
});

afterEach(async () => {
spyOnLogVerbose.mockReset();
spyOnLogInfo.mockReset();
spyOnLogWarn.mockReset();
spyOnLogError.mockReset();
spyOnShowError.mockReset();
spyOnsSettingsValue.mockReset();
spyOnGlobalValue.mockReset();
spyOnSaveBackupInfo.mockReset();
Expand All @@ -88,29 +94,72 @@ describe("Backup", function () {
afterAll(async () => {
fs.removeSync(testPath.base);
});

describe("Backup path", function () {
it(`Backup path != Profile`, async () => {
await backup.loadBackupPath();
expect(backup.backupBasePath).toBe(testPath.backupBasePath);
expect(backup.backupBasePath).not.toBe(testPath.joplinProfile);

/* prettier-ignore */
when(spyOnsSettingsValue)
.calledWith("path").mockImplementation(() => Promise.resolve(""));
await backup.loadBackupPath();
expect(backup.backupBasePath).not.toBe(testPath.joplinProfile);
expect(backup.backupBasePath).toBe(null);
it(`Path tests`, async () => {
const testCases = [
{
backupPath: testPath.joplinProfile,
createSubfolder: false,
expectedBackupPath: null,
expectedBackupPathExist: null,
},
{
backupPath: testPath.joplinProfile,
createSubfolder: true,
expectedBackupPath: path.join(testPath.joplinProfile, "JoplinBackup"),
expectedBackupPathExist: true,
},
{
backupPath: testPath.backupBasePath,
createSubfolder: false,
expectedBackupPath: testPath.backupBasePath,
expectedBackupPathExist: true,
},
{
backupPath: testPath.backupBasePath,
createSubfolder: true,
expectedBackupPath: path.join(
testPath.backupBasePath,
"JoplinBackup"
),
expectedBackupPathExist: true,
},
{
backupPath: path.join(testPath.backupBasePath, "NotExisting"),
createSubfolder: false,
expectedBackupPath: path.join(testPath.backupBasePath, "NotExisting"),
expectedBackupPathExist: false,
},
{
backupPath: path.join(testPath.backupBasePath, "NotExisting"),
createSubfolder: true,
expectedBackupPath: path.join(
testPath.backupBasePath,
"NotExisting",
"JoplinBackup"
),
expectedBackupPathExist: false,
},
];

/* prettier-ignore */
when(spyOnsSettingsValue)
.calledWith("path").mockImplementation(() => Promise.resolve(testPath.joplinProfile));
await backup.loadBackupPath();
expect(backup.backupBasePath).not.toBe(testPath.joplinProfile);
expect(backup.backupBasePath).toBe(null);
for (const testCase of testCases) {
when(spyOnsSettingsValue)
.calledWith("path")
.mockImplementation(() => Promise.resolve(testCase.backupPath));
backup.createSubfolder = testCase.createSubfolder;
await backup.loadBackupPath();
expect(backup.backupBasePath).toBe(testCase.expectedBackupPath);
expect(backup.backupBasePath).not.toBe(testPath.joplinProfile);

if (testCase.expectedBackupPathExist !== null) {
expect(fs.existsSync(backup.backupBasePath)).toBe(
testCase.expectedBackupPathExist
);
}

expect(backup.log.error).toHaveBeenCalledTimes(0);
expect(backup.log.warn).toHaveBeenCalledTimes(0);
expect(backup.log.error).toHaveBeenCalledTimes(0);
expect(backup.log.warn).toHaveBeenCalledTimes(0);
}
});

it(`relative paths`, async () => {
Expand Down
4 changes: 3 additions & 1 deletion __test__/sevenZip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ describe("Test sevenZip", function () {
expect(fs.existsSync(file3)).toBe(true);
expect(fs.existsSync(zip)).toBe(false);

expect(await sevenZip.add(zip, testBaseDir + "\\*", "secret")).toBe(true);
expect(await sevenZip.add(zip, path.join(testBaseDir, "*"), "secret")).toBe(
true
);
expect(fs.existsSync(zip)).toBe(true);

expect(await sevenZip.passwordProtected(zip)).toBe(true);
Expand Down
1 change: 1 addition & 0 deletions api/Joplin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default class Joplin {
get views(): JoplinViews;
get interop(): JoplinInterop;
get settings(): JoplinSettings;
get versionInfo(): any;
/**
* It is not possible to bundle native packages with a plugin, because they
* need to work cross-platforms. Instead access to certain useful native
Expand Down
87 changes: 80 additions & 7 deletions api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ export interface Command {
execute(...args: any[]): Promise<any | void>;

/**
* Defines whether the command should be enabled or disabled, which in turns affects
* the enabled state of any associated button or menu item.
* Defines whether the command should be enabled or disabled, which in turns
* affects the enabled state of any associated button or menu item.
*
* The condition should be expressed as a "when-clause" (as in Visual Studio Code). It's a simple boolean expression that evaluates to
* `true` or `false`. It supports the following operators:
* The condition should be expressed as a "when-clause" (as in Visual Studio
* Code). It's a simple boolean expression that evaluates to `true` or
* `false`. It supports the following operators:
*
* Operator | Symbol | Example
* -- | -- | --
Expand All @@ -40,7 +41,15 @@ export interface Command {
* Or | \|\| | "noteIsTodo \|\| noteTodoCompleted"
* And | && | "oneNoteSelected && !inConflictFolder"
*
* Currently the supported context variables aren't documented, but you can [find the list here](https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts).
* Joplin, unlike VSCode, also supports parenthesis, which allows creating
* more complex expressions such as `cond1 || (cond2 && cond3)`. Only one
* level of parenthesis is possible (nested ones aren't supported).
*
* Currently the supported context variables aren't documented, but you can
* find the list below:
*
* - [Global When Clauses](https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts)
* - [Desktop app When Clauses](https://github.com/laurent22/joplin/blob/dev/packages/app-desktop/services/commands/stateToWhenClauseContext.ts)
*
* Note: Commands are enabled by default unless you use this property.
*/
Expand Down Expand Up @@ -193,6 +202,31 @@ export interface Disposable {
// dispose():void;
}

export enum ModelType {
Note = 1,
Folder = 2,
Setting = 3,
Resource = 4,
Tag = 5,
NoteTag = 6,
Search = 7,
Alarm = 8,
MasterKey = 9,
ItemChange = 10,
NoteResource = 11,
ResourceLocalState = 12,
Revision = 13,
Migration = 14,
SmartFilter = 15,
Command = 16,
}

export interface VersionInfo {
version: string;
profileVersion: number;
syncVersion: number;
}

// =================================================================
// Menu types
// =================================================================
Expand Down Expand Up @@ -260,6 +294,17 @@ export interface MenuItem {
*/
commandName?: string;

/**
* Arguments that should be passed to the command. They will be as rest
* parameters.
*/
commandArgs?: any[];

/**
* Set to "separator" to create a divider line
*/
type?: ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio');

/**
* Accelerator associated with the menu item
*/
Expand Down Expand Up @@ -325,12 +370,35 @@ export enum SettingItemType {
Button = 6,
}

export enum SettingItemSubType {
FilePathAndArgs = 'file_path_and_args',
FilePath = 'file_path', // Not supported on mobile!
DirectoryPath = 'directory_path', // Not supported on mobile!
}

export enum AppType {
Desktop = 'desktop',
Mobile = 'mobile',
Cli = 'cli',
}

export enum SettingStorage {
Database = 1,
File = 2,
}

// Redefine a simplified interface to mask internal details
// and to remove function calls as they would have to be async.
export interface SettingItem {
value: any;
type: SettingItemType;

/**
* Currently only used to display a file or directory selector. Always set
* `type` to `SettingItemType.String` when using this property.
*/
subType?: SettingItemSubType;

label: string;
description?: string;

Expand Down Expand Up @@ -363,7 +431,7 @@ export interface SettingItem {
/**
* Reserved property. Not used at the moment.
*/
appTypes?: string[];
appTypes?: AppType[];

/**
* Set this to `true` to store secure data, such as passwords. Any such
Expand All @@ -384,6 +452,11 @@ export interface SettingItem {
minimum?: number;
maximum?: number;
step?: number;

/**
* Either store the setting in the database or in settings.json. Defaults to database.
*/
storage?: SettingStorage;
}

export interface SettingSection {
Expand All @@ -410,7 +483,7 @@ export type Path = string[];
// Content Script types
// =================================================================

export type PostMessageHandler = (id: string, message: any)=> Promise<any>;
export type PostMessageHandler = (message: any)=> Promise<any>;

/**
* When a content script is initialised, it receives a `context` object.
Expand Down
Loading

0 comments on commit 00ed521

Please sign in to comment.