Compare commits
230 Commits
android-v0
...
android-v0
Author | SHA1 | Date | |
---|---|---|---|
|
c984c19fee | ||
|
ac8e91e82e | ||
|
738ef2b0fa | ||
|
9746a3964b | ||
|
9efbf74b6f | ||
|
c16ea6b237 | ||
|
b06a3b588f | ||
|
6ff67e0995 | ||
|
1a5c8d126d | ||
|
f632580eed | ||
|
1d73f0cdee | ||
|
99c7111f8c | ||
|
ae9806561a | ||
|
fffdf5b5b7 | ||
|
3de19c3db7 | ||
|
56e074b4ef | ||
|
1a79253780 | ||
|
b67908df11 | ||
|
6a5089f71d | ||
|
f710463b67 | ||
|
6ae0c3aba0 | ||
|
07c6347014 | ||
|
b10999e83e | ||
|
961b5bfd25 | ||
|
d1f1d1068a | ||
|
faade0afe2 | ||
|
a442a49e2f | ||
|
7d3fbbcaba | ||
|
d9bb7c3271 | ||
|
4d1dd17fa2 | ||
|
c5c6c777be | ||
|
1fd1a73fda | ||
|
feeb498a79 | ||
|
1d7f30d441 | ||
|
424443a2d8 | ||
|
08b58f0e4c | ||
|
c2a0d8600f | ||
|
ede3c2ce2f | ||
|
0b93515711 | ||
|
2f13e689b9 | ||
|
ea135a0d28 | ||
|
f67e4a03e4 | ||
|
e9268edeff | ||
|
a8576a55d6 | ||
|
eb500cdf9e | ||
|
7b9dc66121 | ||
|
bba2c68c6f | ||
|
c70d8bea78 | ||
|
176bda66ad | ||
|
78ce10ddf0 | ||
|
29f14681a8 | ||
|
aaf617e41c | ||
|
b99146ed7f | ||
|
d136161650 | ||
|
39051a27a1 | ||
|
30bc9dd551 | ||
|
cc2f665313 | ||
|
37c989ed28 | ||
|
adc5885980 | ||
|
8de5b4219d | ||
|
e096ddebd4 | ||
|
83398dd0bc | ||
|
ddc78ebb41 | ||
|
70b69eb31e | ||
|
3fa891e136 | ||
|
6f7a9f3295 | ||
|
8e8793943b | ||
|
83d9faf2fe | ||
|
f45a4fff8b | ||
|
1e02aa3120 | ||
|
77fec75f23 | ||
|
2c9feae20b | ||
|
a81788b3fa | ||
|
88cfba53a3 | ||
|
d659d975cd | ||
|
277ad90f72 | ||
|
f2e3bedde6 | ||
|
98c0f2315a | ||
|
0926755635 | ||
|
75f8234db5 | ||
|
b8e85e1587 | ||
|
b6add857f9 | ||
|
4e41731c08 | ||
|
6ac3b8a8f9 | ||
|
7ab2b11c9d | ||
|
1eb1c5914c | ||
|
5780951f7d | ||
|
67e790e393 | ||
|
0d472486ca | ||
|
2d218904d0 | ||
|
c3fbcb8feb | ||
|
64031ac6cf | ||
|
caae7f7cf8 | ||
|
d56b247adf | ||
|
11bdfbde61 | ||
|
a59bf55c16 | ||
|
043be1916c | ||
|
42e34b5c3b | ||
|
931083b2e2 | ||
|
09b9df4228 | ||
|
37663bd110 | ||
|
f01c6aa8d1 | ||
|
8838017830 | ||
|
1d6fb8058f | ||
|
bb51729bea | ||
|
507e7e6014 | ||
|
f42908b11c | ||
|
03ec406627 | ||
|
c703521b6c | ||
|
cf97bf9a77 | ||
|
304b9a582f | ||
|
4d5c4b1743 | ||
|
4abe5d07c4 | ||
|
f6633e23f5 | ||
|
a6d6201ecb | ||
|
0115e74163 | ||
|
4314c392f6 | ||
|
685f541bb4 | ||
|
f9634ea283 | ||
|
6252a4d8c8 | ||
|
d13d7bec45 | ||
|
74e284d795 | ||
|
7b82dacbf2 | ||
|
f82162f94a | ||
|
e2d5128624 | ||
|
9074412472 | ||
|
0db9c66317 | ||
|
b22211fada | ||
|
7efeaa3a22 | ||
|
79efac2f71 | ||
|
9d7b7092f5 | ||
|
71e877d369 | ||
|
7e086a7730 | ||
|
3f810c71b0 | ||
|
500fbc5294 | ||
|
6d0f60d9a1 | ||
|
e19c26fdd1 | ||
|
86b86513d9 | ||
|
6ff19063ef | ||
|
6a75451539 | ||
|
70a33f8533 | ||
|
23722719f0 | ||
|
d499251206 | ||
|
d180e7b5e1 | ||
|
9e869a5b1f | ||
|
ab959623aa | ||
|
08d2655f13 | ||
|
20632ae1c1 | ||
|
bef7c38724 | ||
|
d1abf4971d | ||
|
d13c2cf8d7 | ||
|
3146273409 | ||
|
1891eb4998 | ||
|
70b03971f6 | ||
|
38c050b47e | ||
|
0bf5c9ebdd | ||
|
6683da804b | ||
|
7750b954fc | ||
|
edbff5a26a | ||
|
18846c11ed | ||
|
cc02c1d585 | ||
|
26bf7c4d46 | ||
|
2959fa1080 | ||
|
3f4f154949 | ||
|
4c0b472f67 | ||
|
4756238821 | ||
|
f5d26e0d81 | ||
|
e9bb5bee9d | ||
|
2c608bca3c | ||
|
d9c1e30e9b | ||
|
5bc72e2b44 | ||
|
df05d04dad | ||
|
888ac8f4c2 | ||
|
55266e5694 | ||
|
39c73e1649 | ||
|
3bf9d01f0a | ||
|
89ef33f7ca | ||
|
f71fe9a1a6 | ||
|
1008b1835b | ||
|
2ffa5419e2 | ||
|
bd20ecff78 | ||
|
a073514c46 | ||
|
c6ff14226f | ||
|
ee02f8255e | ||
|
5951ed3f55 | ||
|
f6fbf3ba0f | ||
|
9bce52a92a | ||
|
e44975622a | ||
|
92b857d83b | ||
|
671e538740 | ||
|
8a282fd2e1 | ||
|
cda623a95c | ||
|
0f343bccda | ||
|
a513f6f3f0 | ||
|
3b4809714e | ||
|
238b5ab9b9 | ||
|
08d9e9b6aa | ||
|
b22900eb3a | ||
|
89fc2c4779 | ||
|
353b79f5e5 | ||
|
b929b46281 | ||
|
5c4a536dad | ||
|
91e337307c | ||
|
2855b68ed4 | ||
|
45ca6284f9 | ||
|
145ee13356 | ||
|
6f97747199 | ||
|
f3751e4ba6 | ||
|
5cd55cada6 | ||
|
bad4b2ecb8 | ||
|
7aafd63ff3 | ||
|
3227a13035 | ||
|
027f96d100 | ||
|
f242a3c215 | ||
|
ad6c347180 | ||
|
4f0431da55 | ||
|
b873fdd029 | ||
|
c1ff820913 | ||
|
7008daf92a | ||
|
ed914c6907 | ||
|
73e81a54b4 | ||
|
ab8c66a361 | ||
|
4b55fefcb1 | ||
|
0eac8b25e1 | ||
|
aec556ff7d | ||
|
110dc29bd4 | ||
|
b1efea1bd9 | ||
|
8671467ed3 | ||
|
485ef1f2c2 | ||
|
b9194e94aa |
4
.gitignore
vendored
@@ -35,4 +35,6 @@ _vieux/
|
||||
_mydocs
|
||||
.DS_Store
|
||||
Assets/DownloadBadges*.psd
|
||||
node_modules
|
||||
node_modules
|
||||
Tools/github_oauth_token.txt
|
||||
_releases
|
@@ -44,6 +44,6 @@ before_install:
|
||||
script:
|
||||
- |
|
||||
cd ElectronClient/app
|
||||
rsync -aP ../../ReactNativeClient/lib/ lib/
|
||||
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
|
||||
npm install
|
||||
yarn dist
|
||||
|
BIN
Assets/All.psd
Before Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 30 KiB |
BIN
Assets/Icon.psd
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 3.3 KiB |
@@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
iconutil --convert icns macOs.iconset
|
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1472 930v318q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q63 0 117 25 15 7 18 23 3 17-9 29l-49 49q-10 10-23 10-3 0-9-2-23-6-45-6h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-254q0-13 9-22l64-64q10-10 23-10 6 0 12 3 20 8 20 29zm231-489l-814 814q-24 24-57 24t-57-24l-430-430q-24-24-24-57t24-57l110-110q24-24 57-24t57 24l263 263 647-647q24-24 57-24t57 24l110 110q24 24 24 57t-24 57z"/></svg>
|
Before Width: | Height: | Size: 612 B |
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M813 1299l614-614q19-19 19-45t-19-45l-102-102q-19-19-45-19t-45 19l-467 467-211-211q-19-19-45-19t-45 19l-102 102q-19 19-19 45t19 45l358 358q19 19 45 19t45-19zm851-883v960q0 119-84.5 203.5t-203.5 84.5h-960q-119 0-203.5-84.5t-84.5-203.5v-960q0-119 84.5-203.5t203.5-84.5h960q119 0 203.5 84.5t84.5 203.5z"/></svg>
|
Before Width: | Height: | Size: 447 B |
@@ -1,2 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<svg viewBox='0 0 1792 1792' xmlns='http://www.w3.org/2000/svg'><path d='M1312 256h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-832q0-66-47-113t-113-47zm288 160v832q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q119 0 203.5 84.5t84.5 203.5z'/></svg>
|
Before Width: | Height: | Size: 370 B |
32
BUILD.md
@@ -1,38 +1,52 @@
|
||||
# General information
|
||||
|
||||
- All the applications share the same library, which, for historical reasons, is in ReactNativeClient/lib. This library is copied to the relevant directories when builing each app.
|
||||
- The translations are built by running CliClient/build-translation.sh. For this reasons, it's generally better to get the CLI app to build first so that everything is setup correctly.
|
||||
- All the applications share the same library, which, for historical reasons, is in ReactNativeClient/lib. This library is copied to the relevant directories when building each app.
|
||||
- The translations are built by running CliClient/build-translation.sh. You normally don't need to run this if you haven't updated the translation since the compiled files are on the repository.
|
||||
|
||||
## macOS dependencies
|
||||
|
||||
brew install yarn node xgettext
|
||||
echo 'export PATH="/usr/local/opt/gettext/bin:$PATH"' >> ~/.bash_profile
|
||||
source ~/.bash_profile
|
||||
brew install yarn node
|
||||
echo 'export PATH="/usr/local/opt/gettext/bin:$PATH"' >> ~/.bash_profile
|
||||
source ~/.bash_profile
|
||||
|
||||
If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
|
||||
|
||||
## Linux and Windows dependencies
|
||||
## Linux and Windows (WSL) dependencies
|
||||
|
||||
- Install yarn - https://yarnpkg.com/lang/en/docs/install/
|
||||
- Install node v8.x (check with `node --version`) - https://nodejs.org/en/
|
||||
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
|
||||
|
||||
# Building the Electron application
|
||||
|
||||
```
|
||||
cd ElectronClient/app
|
||||
rsync -a ../../ReactNativeClient/lib/ lib/
|
||||
rsync --delete -a ../../ReactNativeClient/lib/ lib/
|
||||
npm install
|
||||
yarn dist
|
||||
```
|
||||
|
||||
If there's an error `while loading shared libraries: libgconf-2.so.4: cannot open shared object file: No such file or directory`, run `sudo apt-get install libgconf-2-4`
|
||||
|
||||
For node-gyp to work, you might need to install the `windows-build-tools` using `npm install --global windows-build-tools`.
|
||||
|
||||
That will create the executable file in the `dist` directory.
|
||||
|
||||
From `/ElectronClient` you can also run `run.sh` to run the app for testing.
|
||||
|
||||
# Building the Mobile application
|
||||
|
||||
From `/ReactNativeClient`, run `npm install`, then `react-native run-ios` or `react-native run-android`.
|
||||
First you need to setup React Native to build projects with native code. For this, follow the instructions on the [Get Started](https://facebook.github.io/react-native/docs/getting-started.html) tutorial, in the "Building Projects with Native Code" tab.
|
||||
|
||||
Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios` or `react-native run-android`.
|
||||
|
||||
# Building the Terminal application
|
||||
|
||||
From `/CliClient`, run `npm install` then run `run.sh`. If you get an error about `xgettext`, comment out the command `node build-translation.js --silent` in build.sh
|
||||
```
|
||||
cd CliClient
|
||||
npm install
|
||||
./build.sh
|
||||
rsync --delete -aP ../ReactNativeClient/locales/ build/locales/
|
||||
```
|
||||
|
||||
Run `run.sh` to start the application for testing.
|
6
CONTRIBUTING.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Adding new features
|
||||
If you want to add a new feature, consider asking about it before implementing it to make sure it is within the scope of the project. Of course you are free to create the pull request directly but it is not guaranteed it is going to be accepted.
|
||||
|
||||
# Style
|
||||
- Only use tabs for indentation, not spaces.
|
||||
- Do not remove or add optional characters from other lines (such as colons or new line characters) as it can make the commit needlessly big, and create conflicts with other changes.
|
@@ -1,6 +1,6 @@
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { Resource } = require('lib/models/resource.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const { netUtils } = require('lib/net-utils.js');
|
||||
|
||||
const http = require("http");
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Resource } = require('lib/models/resource.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const { reducer, defaultState } = require('lib/reducer.js');
|
||||
const { splitCommandString } = require('lib/string-utils.js');
|
||||
@@ -14,6 +14,7 @@ const chalk = require('chalk');
|
||||
const tk = require('terminal-kit');
|
||||
const TermWrapper = require('tkwidgets/framework/TermWrapper.js');
|
||||
const Renderer = require('tkwidgets/framework/Renderer.js');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||
|
||||
const BaseWidget = require('tkwidgets/BaseWidget.js');
|
||||
const ListWidget = require('tkwidgets/ListWidget.js');
|
||||
@@ -65,6 +66,7 @@ class AppGui {
|
||||
// a regular command it's not necessary since the process
|
||||
// exits right away.
|
||||
reg.setupRecurrentSync();
|
||||
DecryptionWorker.instance().scheduleStart();
|
||||
}
|
||||
|
||||
store() {
|
||||
@@ -80,8 +82,16 @@ class AppGui {
|
||||
await this.renderer_.renderRoot();
|
||||
}
|
||||
|
||||
prompt(initialText = '', promptString = ':') {
|
||||
return this.widget('statusBar').prompt(initialText, promptString);
|
||||
termSaveState() {
|
||||
return this.term().saveState();
|
||||
}
|
||||
|
||||
termRestoreState(state) {
|
||||
return this.term().restoreState(state);
|
||||
}
|
||||
|
||||
prompt(initialText = '', promptString = ':', options = null) {
|
||||
return this.widget('statusBar').prompt(initialText, promptString, options);
|
||||
}
|
||||
|
||||
stdoutMaxWidth() {
|
||||
@@ -548,6 +558,10 @@ class AppGui {
|
||||
}
|
||||
|
||||
this.widget('console').scrollBottom();
|
||||
|
||||
// Invalidate so that the screen is redrawn in case inputting a command has moved
|
||||
// the GUI up (in particular due to autocompletion), it's moved back to the right position.
|
||||
this.widget('root').invalidate();
|
||||
}
|
||||
|
||||
async updateFolderList() {
|
||||
@@ -826,4 +840,4 @@ class AppGui {
|
||||
AppGui.INPUT_MODE_NORMAL = 1;
|
||||
AppGui.INPUT_MODE_META = 2;
|
||||
|
||||
module.exports = AppGui;
|
||||
module.exports = AppGui;
|
||||
|
@@ -5,12 +5,12 @@ const { JoplinDatabase } = require('lib/joplin-database.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
|
||||
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
@@ -21,6 +21,7 @@ const os = require('os');
|
||||
const fs = require('fs-extra');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const EventEmitter = require('events');
|
||||
const Cache = require('lib/Cache');
|
||||
|
||||
class Application extends BaseApplication {
|
||||
|
||||
@@ -34,6 +35,7 @@ class Application extends BaseApplication {
|
||||
this.allCommandsLoaded_ = false;
|
||||
this.showStackTraces_ = false;
|
||||
this.gui_ = null;
|
||||
this.cache_ = new Cache();
|
||||
}
|
||||
|
||||
gui() {
|
||||
@@ -144,13 +146,15 @@ class Application extends BaseApplication {
|
||||
message += ' (' + options.answers.join('/') + ')';
|
||||
}
|
||||
|
||||
let answer = await this.gui().prompt('', message + ' ');
|
||||
let answer = await this.gui().prompt('', message + ' ', options);
|
||||
|
||||
if (options.type === 'boolean') {
|
||||
if (answer === null) return false; // Pressed ESCAPE
|
||||
if (!answer) answer = options.answers[0];
|
||||
let positiveIndex = options.booleanAnswerDefault == 'y' ? 0 : 1;
|
||||
return answer.toLowerCase() === options.answers[positiveIndex].toLowerCase();
|
||||
} else {
|
||||
return answer;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -177,22 +181,33 @@ class Application extends BaseApplication {
|
||||
await doExit();
|
||||
}
|
||||
|
||||
commands() {
|
||||
if (this.allCommandsLoaded_) return this.commands_;
|
||||
commands(uiType = null) {
|
||||
if (!this.allCommandsLoaded_) {
|
||||
fs.readdirSync(__dirname).forEach((path) => {
|
||||
if (path.indexOf('command-') !== 0) return;
|
||||
const ext = fileExtension(path)
|
||||
if (ext != 'js') return;
|
||||
|
||||
fs.readdirSync(__dirname).forEach((path) => {
|
||||
if (path.indexOf('command-') !== 0) return;
|
||||
const ext = fileExtension(path)
|
||||
if (ext != 'js') return;
|
||||
let CommandClass = require('./' + path);
|
||||
let cmd = new CommandClass();
|
||||
if (!cmd.enabled()) return;
|
||||
cmd = this.setupCommand(cmd);
|
||||
this.commands_[cmd.name()] = cmd;
|
||||
});
|
||||
|
||||
let CommandClass = require('./' + path);
|
||||
let cmd = new CommandClass();
|
||||
if (!cmd.enabled()) return;
|
||||
cmd = this.setupCommand(cmd);
|
||||
this.commands_[cmd.name()] = cmd;
|
||||
});
|
||||
this.allCommandsLoaded_ = true;
|
||||
}
|
||||
|
||||
this.allCommandsLoaded_ = true;
|
||||
if (uiType !== null) {
|
||||
let temp = [];
|
||||
for (let n in this.commands_) {
|
||||
if (!this.commands_.hasOwnProperty(n)) continue;
|
||||
const c = this.commands_[n];
|
||||
if (!c.supportsUi(uiType)) continue;
|
||||
temp[n] = c;
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
return this.commands_;
|
||||
}
|
||||
@@ -210,12 +225,8 @@ class Application extends BaseApplication {
|
||||
async commandMetadata() {
|
||||
if (this.commandMetadata_) return this.commandMetadata_;
|
||||
|
||||
const osTmpdir = require('os-tmpdir');
|
||||
const storage = require('node-persist');
|
||||
await storage.init({ dir: osTmpdir() + '/commandMetadata', ttl: 1000 * 60 * 60 * 24 });
|
||||
|
||||
let output = await storage.getItem('metadata');
|
||||
if (Setting.value('env') != 'dev' && output) {
|
||||
let output = await this.cache_.getItem('metadata');
|
||||
if (output) {
|
||||
this.commandMetadata_ = output;
|
||||
return Object.assign({}, this.commandMetadata_);
|
||||
}
|
||||
@@ -229,7 +240,7 @@ class Application extends BaseApplication {
|
||||
output[n] = cmd.metadata();
|
||||
}
|
||||
|
||||
await storage.setItem('metadata', output);
|
||||
await this.cache_.setItem('metadata', output, 1000 * 60 * 60 * 24);
|
||||
|
||||
this.commandMetadata_ = output;
|
||||
return Object.assign({}, this.commandMetadata_);
|
||||
@@ -246,9 +257,13 @@ class Application extends BaseApplication {
|
||||
try {
|
||||
CommandClass = require(__dirname + '/command-' + name + '.js');
|
||||
} catch (error) {
|
||||
let e = new Error('No such command: ' + name);
|
||||
e.type = 'notFound';
|
||||
throw e;
|
||||
if (error.message && error.message.indexOf('Cannot find module') >= 0) {
|
||||
let e = new Error(_('No such command: %s', name));
|
||||
e.type = 'notFound';
|
||||
throw e;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
let cmd = new CommandClass();
|
||||
@@ -260,7 +275,7 @@ class Application extends BaseApplication {
|
||||
dummyGui() {
|
||||
return {
|
||||
isDummy: () => { return true; },
|
||||
prompt: (initialText = '', promptString = '') => { return cliUtils.prompt(initialText, promptString); },
|
||||
prompt: (initialText = '', promptString = '', options = null) => { return cliUtils.prompt(initialText, promptString, options); },
|
||||
showConsole: () => {},
|
||||
maximizeConsole: () => {},
|
||||
stdout: (text) => { console.info(text); },
|
||||
@@ -268,7 +283,10 @@ class Application extends BaseApplication {
|
||||
exit: () => {},
|
||||
showModalOverlay: (text) => {},
|
||||
hideModalOverlay: () => {},
|
||||
stdoutMaxWidth: () => { return 78; }
|
||||
stdoutMaxWidth: () => { return 78; },
|
||||
forceRender: () => {},
|
||||
termSaveState: () => {},
|
||||
termRestoreState: (state) => {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -306,6 +324,8 @@ class Application extends BaseApplication {
|
||||
if (argv.length) {
|
||||
this.gui_ = this.dummyGui();
|
||||
|
||||
this.currentFolder_ = await Folder.load(Setting.value('activeFolderId'));
|
||||
|
||||
try {
|
||||
await this.execCommand(argv);
|
||||
} catch (error) {
|
||||
@@ -334,7 +354,7 @@ class Application extends BaseApplication {
|
||||
|
||||
this.dispatch({
|
||||
type: 'TAG_UPDATE_ALL',
|
||||
tags: tags,
|
||||
items: tags,
|
||||
});
|
||||
|
||||
this.store().dispatch({
|
||||
|
185
CliClient/app/autocompletion.js
Normal file
@@ -0,0 +1,185 @@
|
||||
var { app } = require('./app.js');
|
||||
var Note = require('lib/models/Note.js');
|
||||
var Folder = require('lib/models/Folder.js');
|
||||
var Tag = require('lib/models/Tag.js');
|
||||
var { cliUtils } = require('./cli-utils.js');
|
||||
var yargParser = require('yargs-parser');
|
||||
|
||||
async function handleAutocompletionPromise(line) {
|
||||
// Auto-complete the command name
|
||||
const names = await app().commandNames();
|
||||
let words = getArguments(line);
|
||||
//If there is only one word and it is not already a command name then you
|
||||
//should look for commmands it could be
|
||||
if (words.length == 1) {
|
||||
if (names.indexOf(words[0]) === -1) {
|
||||
let x = names.filter((n) => n.indexOf(words[0]) === 0);
|
||||
if (x.length === 1) {
|
||||
return x[0] + ' ';
|
||||
}
|
||||
return x.length > 0 ? x.map((a) => a + ' ') : line;
|
||||
} else {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
//There is more than one word and it is a command
|
||||
const metadata = (await app().commandMetadata())[words[0]];
|
||||
//If for some reason this command does not have any associated metadata
|
||||
//just don't autocomplete. However, this should not happen.
|
||||
if (metadata === undefined) {
|
||||
return line;
|
||||
}
|
||||
//complete an option
|
||||
let next = words.length > 1 ? words[words.length - 1] : '';
|
||||
let l = [];
|
||||
if (next[0] === '-') {
|
||||
for (let i = 0; i<metadata.options.length; i++) {
|
||||
const options = metadata.options[i][0].split(' ');
|
||||
//if there are multiple options then they will be seperated by comma and
|
||||
//space. The comma should be removed
|
||||
if (options[0][options[0].length - 1] === ',') {
|
||||
options[0] = options[0].slice(0, -1);
|
||||
}
|
||||
if (words.includes(options[0]) || words.includes(options[1])) {
|
||||
continue;
|
||||
}
|
||||
//First two elements are the flag and the third is the description
|
||||
//Only autocomplete long
|
||||
if (options.length > 1 && options[1].indexOf(next) === 0) {
|
||||
l.push(options[1]);
|
||||
} else if (options[0].indexOf(next) === 0) {
|
||||
l.push(options[2]);
|
||||
}
|
||||
}
|
||||
if (l.length === 0) {
|
||||
return line;
|
||||
}
|
||||
let ret = l.map(a=>toCommandLine(a));
|
||||
ret.prefix = toCommandLine(words.slice(0, -1)) + ' ';
|
||||
return ret;
|
||||
}
|
||||
//Complete an argument
|
||||
//Determine the number of positional arguments by counting the number of
|
||||
//words that don't start with a - less one for the command name
|
||||
const positionalArgs = words.filter((a)=>a.indexOf('-') !== 0).length - 1;
|
||||
|
||||
let cmdUsage = yargParser(metadata.usage)['_'];
|
||||
cmdUsage.splice(0, 1);
|
||||
|
||||
if (cmdUsage.length >= positionalArgs) {
|
||||
|
||||
let argName = cmdUsage[positionalArgs - 1];
|
||||
argName = cliUtils.parseCommandArg(argName).name;
|
||||
|
||||
if (argName == 'note' || argName == 'note-pattern' && app().currentFolder()) {
|
||||
const notes = await Note.previews(app().currentFolder().id, { titlePattern: next + '*' });
|
||||
l.push(...notes.map((n) => n.title));
|
||||
}
|
||||
|
||||
if (argName == 'notebook') {
|
||||
const folders = await Folder.search({ titlePattern: next + '*' });
|
||||
l.push(...folders.map((n) => n.title));
|
||||
}
|
||||
|
||||
if (argName == 'tag') {
|
||||
let tags = await Tag.search({ titlePattern: next + '*' });
|
||||
l.push(...tags.map((n) => n.title));
|
||||
}
|
||||
|
||||
if (argName == 'tag-command') {
|
||||
let c = filterList(['add', 'remove', 'list'], next);
|
||||
l.push(...c);
|
||||
}
|
||||
|
||||
if (argName == 'todo-command') {
|
||||
let c = filterList(['toggle', 'clear'], next);
|
||||
l.push(...c);
|
||||
}
|
||||
}
|
||||
if (l.length === 1) {
|
||||
return toCommandLine([...words.slice(0, -1), l[0]]);
|
||||
} else if (l.length > 1) {
|
||||
let ret = l.map(a=>toCommandLine(a));
|
||||
ret.prefix = toCommandLine(words.slice(0, -1)) + ' ';
|
||||
return ret;
|
||||
}
|
||||
return line;
|
||||
|
||||
}
|
||||
function handleAutocompletion(str, callback) {
|
||||
handleAutocompletionPromise(str).then(function(res) {
|
||||
callback(undefined, res);
|
||||
});
|
||||
}
|
||||
function toCommandLine(args) {
|
||||
if (Array.isArray(args)) {
|
||||
return args.map(function(a) {
|
||||
if(a.indexOf('"') !== -1 || a.indexOf(' ') !== -1) {
|
||||
return "'" + a + "'";
|
||||
} else if (a.indexOf("'") !== -1) {
|
||||
return '"' + a + '"';
|
||||
} else {
|
||||
return a;
|
||||
}
|
||||
}).join(' ');
|
||||
} else {
|
||||
if(args.indexOf('"') !== -1 || args.indexOf(' ') !== -1) {
|
||||
return "'" + args + "' ";
|
||||
} else if (args.indexOf("'") !== -1) {
|
||||
return '"' + args + '" ';
|
||||
} else {
|
||||
return args + ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
function getArguments(line) {
|
||||
let inSingleQuotes = false;
|
||||
let inDoubleQuotes = false;
|
||||
let currentWord = '';
|
||||
let parsed = [];
|
||||
for(let i = 0; i<line.length; i++) {
|
||||
if(line[i] === '"') {
|
||||
if(inDoubleQuotes) {
|
||||
inDoubleQuotes = false;
|
||||
//maybe push word to parsed?
|
||||
//currentWord += '"';
|
||||
} else {
|
||||
inDoubleQuotes = true;
|
||||
//currentWord += '"';
|
||||
}
|
||||
} else if(line[i] === "'") {
|
||||
if(inSingleQuotes) {
|
||||
inSingleQuotes = false;
|
||||
//maybe push word to parsed?
|
||||
//currentWord += "'";
|
||||
} else {
|
||||
inSingleQuotes = true;
|
||||
//currentWord += "'";
|
||||
}
|
||||
} else if (/\s/.test(line[i]) &&
|
||||
!(inDoubleQuotes || inSingleQuotes)) {
|
||||
if (currentWord !== '') {
|
||||
parsed.push(currentWord);
|
||||
currentWord = '';
|
||||
}
|
||||
} else {
|
||||
currentWord += line[i];
|
||||
}
|
||||
}
|
||||
if (!(inSingleQuotes || inDoubleQuotes) && /\s/.test(line[line.length - 1])) {
|
||||
parsed.push('');
|
||||
} else {
|
||||
parsed.push(currentWord);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
function filterList(list, next) {
|
||||
let output = [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].indexOf(next) !== 0) continue;
|
||||
output.push(list[i]);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
module.exports = { handleAutocompletion };
|
@@ -12,6 +12,10 @@ class BaseCommand {
|
||||
throw new Error('Usage not defined');
|
||||
}
|
||||
|
||||
encryptionCheck(item) {
|
||||
if (item && item.encryption_applied) throw new Error(_('Cannot change encrypted item'));
|
||||
}
|
||||
|
||||
description() {
|
||||
throw new Error('Description not defined');
|
||||
}
|
||||
|
@@ -5,10 +5,10 @@ const { Logger } = require('lib/logger.js');
|
||||
const { dirname } = require('lib/path-utils.js');
|
||||
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
||||
const { JoplinDatabase } = require('lib/joplin-database.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const exec = require('child_process').exec
|
||||
|
||||
|
@@ -178,38 +178,39 @@ cliUtils.promptConfirm = function(message, answers = null) {
|
||||
});
|
||||
}
|
||||
|
||||
cliUtils.promptInput = function(message) {
|
||||
const readline = require('readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
rl.question(message + ' ', (answer) => {
|
||||
rl.close();
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Note: initialText is there to have the same signature as statusBar.prompt() so that
|
||||
// it can be a drop-in replacement, however initialText is not used (and cannot be
|
||||
// with readline.question?).
|
||||
cliUtils.prompt = function(initialText = '', promptString = ':') {
|
||||
cliUtils.prompt = function(initialText = '', promptString = ':', options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
const readline = require('readline');
|
||||
const Writable = require('stream').Writable;
|
||||
|
||||
const mutableStdout = new Writable({
|
||||
write: function(chunk, encoding, callback) {
|
||||
if (!this.muted)
|
||||
process.stdout.write(chunk, encoding);
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
output: mutableStdout,
|
||||
terminal: true,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
mutableStdout.muted = false;
|
||||
|
||||
rl.question(promptString, (answer) => {
|
||||
rl.close();
|
||||
if (!!options.secure) this.stdout_('');
|
||||
resolve(answer);
|
||||
});
|
||||
|
||||
mutableStdout.muted = !!options.secure;
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
@@ -19,6 +19,7 @@ class Command extends BaseCommand {
|
||||
let title = args['note'];
|
||||
|
||||
let note = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
|
||||
this.encryptionCheck(note);
|
||||
if (!note) throw new Error(_('Cannot find "%s".', title));
|
||||
|
||||
const localFilePath = args['file'];
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -21,10 +21,6 @@ class Command extends BaseCommand {
|
||||
];
|
||||
}
|
||||
|
||||
enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
let title = args['note'];
|
||||
|
||||
@@ -33,6 +29,9 @@ class Command extends BaseCommand {
|
||||
|
||||
const content = args.options.verbose ? await Note.serialize(item) : await Note.serializeForEdit(item);
|
||||
this.stdout(content);
|
||||
|
||||
app().gui().showConsole();
|
||||
app().gui().maximizeConsole();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { _, setLocale } = require('lib/locale.js');
|
||||
const { app } = require('./app.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -23,7 +23,8 @@ class Command extends BaseCommand {
|
||||
const verbose = args.options.verbose;
|
||||
|
||||
const renderKeyValue = (name) => {
|
||||
const value = Setting.value(name);
|
||||
let value = Setting.value(name);
|
||||
if (typeof value === 'object' || Array.isArray(value)) value = JSON.stringify(value);
|
||||
if (Setting.isEnum(name)) {
|
||||
return _('%s = %s (%s)', name, value, Setting.enumOptionsDoc(name));
|
||||
} else {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
@@ -16,8 +16,9 @@ class Command extends BaseCommand {
|
||||
return _('Marks a to-do as done.');
|
||||
}
|
||||
|
||||
static async handleAction(args, isCompleted) {
|
||||
static async handleAction(commandInstance, args, isCompleted) {
|
||||
const note = await app().loadItem(BaseModel.TYPE_NOTE, args.note);
|
||||
commandInstance.encryptionCheck(note);
|
||||
if (!note) throw new Error(_('Cannot find "%s".', args.note));
|
||||
if (!note.is_todo) throw new Error(_('Note is not a to-do: "%s"', args.note));
|
||||
|
||||
@@ -32,7 +33,7 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
await Command.handleAction(args, true);
|
||||
await Command.handleAction(this, args, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
183
CliClient/app/command-e2ee.js
Normal file
@@ -0,0 +1,183 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||
const MasterKey = require('lib/models/MasterKey');
|
||||
const BaseItem = require('lib/models/BaseItem');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
usage() {
|
||||
return 'e2ee <command> [path]';
|
||||
}
|
||||
|
||||
description() {
|
||||
return _('Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status` and `target-status`.');
|
||||
}
|
||||
|
||||
options() {
|
||||
return [
|
||||
// This is here mostly for testing - shouldn't be used
|
||||
['-p, --password <password>', 'Use this password as master password (For security reasons, it is not recommended to use this option).'],
|
||||
['-v, --verbose', 'More verbose output for the `target-status` command'],
|
||||
];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
// change-password
|
||||
|
||||
const options = args.options;
|
||||
|
||||
if (args.command === 'enable') {
|
||||
const password = options.password ? options.password.toString() : await this.prompt(_('Enter master password:'), { type: 'string', secure: true });
|
||||
if (!password) {
|
||||
this.stdout(_('Operation cancelled'));
|
||||
return;
|
||||
}
|
||||
|
||||
await EncryptionService.instance().generateMasterKeyAndEnableEncryption(password);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.command === 'disable') {
|
||||
await EncryptionService.instance().disableEncryption();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.command === 'decrypt') {
|
||||
this.stdout(_('Starting decryption... Please wait as it may take several minutes depending on how much there is to decrypt.'));
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
await DecryptionWorker.instance().start();
|
||||
break;
|
||||
} catch (error) {
|
||||
if (error.code === 'masterKeyNotLoaded') {
|
||||
const masterKeyId = error.masterKeyId;
|
||||
const password = await this.prompt(_('Enter master password:'), { type: 'string', secure: true });
|
||||
if (!password) {
|
||||
this.stdout(_('Operation cancelled'));
|
||||
return;
|
||||
}
|
||||
Setting.setObjectKey('encryption.passwordCache', masterKeyId, password);
|
||||
await EncryptionService.instance().loadMasterKeysFromSettings();
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
this.stdout(_('Completed decryption.'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.command === 'status') {
|
||||
this.stdout(_('Encryption is: %s', Setting.value('encryption.enabled') ? _('Enabled') : _('Disabled')));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.command === 'target-status') {
|
||||
const fs = require('fs-extra');
|
||||
const pathUtils = require('lib/path-utils.js');
|
||||
const fsDriver = new (require('lib/fs-driver-node.js').FsDriverNode)();
|
||||
|
||||
const targetPath = args.path;
|
||||
if (!targetPath) throw new Error('Please specify the sync target path.');
|
||||
|
||||
const dirPaths = function(targetPath) {
|
||||
let paths = [];
|
||||
fs.readdirSync(targetPath).forEach((path) => {
|
||||
paths.push(path);
|
||||
});
|
||||
return paths;
|
||||
}
|
||||
|
||||
let itemCount = 0;
|
||||
let resourceCount = 0;
|
||||
let encryptedItemCount = 0;
|
||||
let encryptedResourceCount = 0;
|
||||
let otherItemCount = 0;
|
||||
|
||||
let encryptedPaths = [];
|
||||
let decryptedPaths = [];
|
||||
|
||||
let paths = dirPaths(targetPath);
|
||||
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
const path = paths[i];
|
||||
const fullPath = targetPath + '/' + path;
|
||||
const stat = await fs.stat(fullPath);
|
||||
|
||||
// this.stdout(fullPath);
|
||||
|
||||
if (path === '.resource') {
|
||||
let resourcePaths = dirPaths(fullPath);
|
||||
for (let j = 0; j < resourcePaths.length; j++) {
|
||||
const resourcePath = resourcePaths[j];
|
||||
resourceCount++;
|
||||
const fullResourcePath = fullPath + '/' + resourcePath;
|
||||
const isEncrypted = await EncryptionService.instance().fileIsEncrypted(fullResourcePath);
|
||||
if (isEncrypted) {
|
||||
encryptedResourceCount++;
|
||||
encryptedPaths.push(fullResourcePath);
|
||||
} else {
|
||||
decryptedPaths.push(fullResourcePath);
|
||||
}
|
||||
}
|
||||
} else if (stat.isDirectory()) {
|
||||
continue;
|
||||
} else {
|
||||
itemCount++;
|
||||
const content = await fs.readFile(fullPath, 'utf8');
|
||||
const item = await BaseItem.unserialize(content);
|
||||
const ItemClass = BaseItem.itemClass(item);
|
||||
|
||||
if (!ItemClass.encryptionSupported()) {
|
||||
otherItemCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const isEncrypted = await EncryptionService.instance().itemIsEncrypted(item);
|
||||
|
||||
if (isEncrypted) {
|
||||
encryptedItemCount++;
|
||||
encryptedPaths.push(fullPath);
|
||||
} else {
|
||||
decryptedPaths.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.stdout('Encrypted items: ' + encryptedItemCount + '/' + itemCount);
|
||||
this.stdout('Encrypted resources: ' + encryptedResourceCount + '/' + resourceCount);
|
||||
this.stdout('Other items (never encrypted): ' + otherItemCount);
|
||||
|
||||
if (options.verbose) {
|
||||
this.stdout('');
|
||||
this.stdout('# Encrypted paths');
|
||||
this.stdout('');
|
||||
for (let i = 0; i < encryptedPaths.length; i++) {
|
||||
const path = encryptedPaths[i];
|
||||
this.stdout(path);
|
||||
}
|
||||
|
||||
this.stdout('');
|
||||
this.stdout('# Decrypted paths');
|
||||
this.stdout('');
|
||||
for (let i = 0; i < decryptedPaths.length; i++) {
|
||||
const path = decryptedPaths[i];
|
||||
this.stdout(path);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Command;
|
@@ -3,10 +3,10 @@ const { BaseCommand } = require('./base-command.js');
|
||||
const { uuid } = require('lib/uuid.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
@@ -44,6 +44,8 @@ class Command extends BaseCommand {
|
||||
if (!app().currentFolder()) throw new Error(_('No active notebook.'));
|
||||
let note = await app().loadItem(BaseModel.TYPE_NOTE, title);
|
||||
|
||||
this.encryptionCheck(note);
|
||||
|
||||
if (!note) {
|
||||
const ok = await this.prompt(_('Note does not exist: "%s". Create it?', title));
|
||||
if (!ok) return;
|
||||
@@ -76,12 +78,14 @@ class Command extends BaseCommand {
|
||||
|
||||
app().gui().showModalOverlay(_('Starting to edit note. Close the editor to get back to the prompt.'));
|
||||
await app().gui().forceRender();
|
||||
const termState = app().gui().term().saveState();
|
||||
const termState = app().gui().termSaveState();
|
||||
|
||||
const spawnSync = require('child_process').spawnSync;
|
||||
spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
|
||||
const result = spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
|
||||
|
||||
app().gui().term().restoreState(termState);
|
||||
if (result.error) this.stdout(_('Error opening note in editor: %s', result.error.message));
|
||||
|
||||
app().gui().termRestoreState(termState);
|
||||
app().gui().hideModalOverlay();
|
||||
app().gui().forceRender();
|
||||
|
||||
|
@@ -12,6 +12,10 @@ class Command extends BaseCommand {
|
||||
return _('Exits the application.');
|
||||
}
|
||||
|
||||
compatibleUis() {
|
||||
return ['gui'];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
await app().exit();
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const { app } = require('./app.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { ReportService } = require('lib/services/report.js');
|
||||
const fs = require('fs-extra');
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { Exporter } = require('lib/services/exporter.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -2,7 +2,7 @@ const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { renderCommandHelp } = require('./help-utils.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { wrap } = require('lib/string-utils.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
@@ -18,7 +18,7 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
allCommands() {
|
||||
const commands = app().commands();
|
||||
const commands = app().commands(app().uiType());
|
||||
let output = [];
|
||||
for (let n in commands) {
|
||||
if (!commands.hasOwnProperty(n)) continue;
|
||||
@@ -65,7 +65,7 @@ class Command extends BaseCommand {
|
||||
} else {
|
||||
const commandNames = this.allCommands().map((a) => a.name());
|
||||
|
||||
this.stdout(_('Type `help [command]` for more information about a command.'));
|
||||
this.stdout(_('Type `help [command]` for more information about a command; or type `help all` for the complete usage information.'));
|
||||
this.stdout('');
|
||||
this.stdout(_('The possible commands are:'));
|
||||
this.stdout('');
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const { importEnex } = require('lib/import-enex');
|
||||
const { filename, basename } = require('lib/path-utils.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
|
@@ -1,10 +1,10 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -20,6 +20,7 @@ class Command extends BaseCommand {
|
||||
const name = args['name'];
|
||||
|
||||
const item = await app().loadItem('folderOrNote', pattern);
|
||||
this.encryptionCheck(item);
|
||||
if (!item) throw new Error(_('Cannot find "%s".', pattern));
|
||||
|
||||
const newItem = {
|
||||
|
@@ -1,10 +1,10 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
@@ -29,7 +29,7 @@ class Command extends BaseCommand {
|
||||
|
||||
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
|
||||
if (!folder) throw new Error(_('Cannot find "%s".', pattern));
|
||||
const ok = force ? true : await this.prompt(_('Delete notebook "%s"?', folder.title), { booleanAnswerDefault: 'n' });
|
||||
const ok = force ? true : await this.prompt(_('Delete notebook? All notes within this notebook will also be deleted.'), { booleanAnswerDefault: 'n' });
|
||||
if (!ok) return;
|
||||
|
||||
await Folder.delete(folder.id);
|
||||
|
@@ -1,10 +1,10 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { uuid } = require('lib/uuid.js');
|
||||
|
@@ -1,10 +1,11 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -12,16 +13,16 @@ class Command extends BaseCommand {
|
||||
return 'set <note> <name> [value]';
|
||||
}
|
||||
|
||||
enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
description() {
|
||||
return _('Sets the property <name> of the given <note> to the given [value].');
|
||||
}
|
||||
const fields = Note.fields();
|
||||
const s = [];
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const f = fields[i];
|
||||
if (f.name === 'id') continue;
|
||||
s.push(f.name + ' (' + Database.enumName('fieldType', f.type) + ')');
|
||||
}
|
||||
|
||||
hidden() {
|
||||
return true;
|
||||
return _('Sets the property <name> of the given <note> to the given [value]. Possible properties are:\n\n%s', s.join(', '));
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
@@ -34,6 +35,8 @@ class Command extends BaseCommand {
|
||||
if (!notes.length) throw new Error(_('Cannot find "%s".', title));
|
||||
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
this.encryptionCheck(notes[i]);
|
||||
|
||||
let newNote = {
|
||||
id: notes[i].id,
|
||||
type_: notes[i].type_,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const { app } = require('./app.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { ReportService } = require('lib/services/report.js');
|
||||
|
||||
|
@@ -2,8 +2,8 @@ const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { OneDriveApiNodeUtils } = require('./onedrive-api-node-utils.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
@@ -32,7 +32,6 @@ class Command extends BaseCommand {
|
||||
options() {
|
||||
return [
|
||||
['--target <target>', _('Sync to provided target (defaults to sync.target config value)')],
|
||||
['--random-failures', 'For debugging purposes. Do not use.'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -140,7 +139,6 @@ class Command extends BaseCommand {
|
||||
cliUtils.redrawDone();
|
||||
this.stdout(msg);
|
||||
},
|
||||
randomFailures: args.options['random-failures'] === true,
|
||||
};
|
||||
|
||||
this.stdout(_('Synchronisation target: %s (%s)', Setting.enumOptionLabel('sync.target', this.syncTargetId_), this.syncTargetId_));
|
||||
|
@@ -1,8 +1,8 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
@@ -25,6 +25,8 @@ class Command extends BaseCommand {
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
const note = notes[i];
|
||||
|
||||
this.encryptionCheck(note);
|
||||
|
||||
let toSave = {
|
||||
id: note.id,
|
||||
};
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
const CommandDone = require('./command-done.js');
|
||||
@@ -19,7 +19,7 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
await CommandDone.handleAction(args, false);
|
||||
await CommandDone.handleAction(this, args, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -18,8 +18,8 @@ class Command extends BaseCommand {
|
||||
return { data: autocompleteFolders };
|
||||
}
|
||||
|
||||
enabled() {
|
||||
return false;
|
||||
compatibleUis() {
|
||||
return ['cli'];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { Resource } = require('lib/models/resource.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const { dirname } = require('lib/path-utils.js');
|
||||
const { FsDriverNode } = require('./fs-driver-node.js');
|
||||
const lodash = require('lodash');
|
||||
|
@@ -1,6 +1,6 @@
|
||||
const Folder = require('lib/models/folder.js').Folder;
|
||||
const Tag = require('lib/models/tag.js').Tag;
|
||||
const BaseModel = require('lib/base-model.js').BaseModel;
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const ListWidget = require('tkwidgets/ListWidget.js');
|
||||
const _ = require('lib/locale.js')._;
|
||||
|
||||
@@ -24,9 +24,9 @@ class FolderListWidget extends ListWidget {
|
||||
if (item === '-') {
|
||||
output.push('-'.repeat(this.innerWidth));
|
||||
} else if (item.type_ === Folder.modelType()) {
|
||||
output.push(item.title);
|
||||
output.push(Folder.displayTitle(item));
|
||||
} else if (item.type_ === Tag.modelType()) {
|
||||
output.push('[' + item.title + ']');
|
||||
output.push('[' + Folder.displayTitle(item) + ']');
|
||||
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
|
||||
output.push(_('Search:'));
|
||||
output.push(item.title);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
const Note = require('lib/models/note.js').Note;
|
||||
const Note = require('lib/models/Note.js');
|
||||
const ListWidget = require('tkwidgets/ListWidget.js');
|
||||
|
||||
class NoteListWidget extends ListWidget {
|
||||
@@ -10,7 +10,7 @@ class NoteListWidget extends ListWidget {
|
||||
this.updateIndexFromSelectedNoteId_ = false;
|
||||
|
||||
this.itemRenderer = (note) => {
|
||||
let label = note.title; // + ' ' + note.id;
|
||||
let label = Note.displayTitle(note); // + ' ' + note.id;
|
||||
if (note.is_todo) {
|
||||
label = '[' + (note.todo_completed ? 'X' : ' ') + '] ' + label;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
const Note = require('lib/models/note.js').Note;
|
||||
const Note = require('lib/models/Note.js');
|
||||
const TextWidget = require('tkwidgets/TextWidget.js');
|
||||
|
||||
class NoteMetadataWidget extends TextWidget {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
const Note = require('lib/models/note.js').Note;
|
||||
const Note = require('lib/models/Note.js');
|
||||
const TextWidget = require('tkwidgets/TextWidget.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
class NoteWidget extends TextWidget {
|
||||
|
||||
@@ -32,11 +33,24 @@ class NoteWidget extends TextWidget {
|
||||
this.reloadNote();
|
||||
}
|
||||
|
||||
welcomeText() {
|
||||
return _('Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.');
|
||||
}
|
||||
|
||||
reloadNote() {
|
||||
if (this.noteId_) {
|
||||
if (!this.noteId_ && !this.notes.length) {
|
||||
this.text = this.welcomeText();
|
||||
this.scrollTop = 0;
|
||||
} else if (this.noteId_) {
|
||||
this.doAsync('loadNote', async () => {
|
||||
this.note_ = await Note.load(this.noteId_);
|
||||
this.text = this.note_ ? this.note_.title + "\n\n" + this.note_.body : '';
|
||||
|
||||
if (this.note_ && this.note_.encryption_applied) {
|
||||
this.text = _('One or more items are currently encrypted and you may need to supply a master password. To do so please type `e2ee decrypt`. If you have already supplied the password, the encrypted items are being decrypted in the background and will be available soon.');
|
||||
} else {
|
||||
this.text = this.note_ ? this.note_.title + "\n\n" + this.note_.body : '';
|
||||
}
|
||||
|
||||
if (this.lastLoadedNoteId_ !== this.noteId_) this.scrollTop = 0;
|
||||
this.lastLoadedNoteId_ = this.noteId_;
|
||||
});
|
||||
|
@@ -2,6 +2,7 @@ const BaseWidget = require('tkwidgets/BaseWidget.js');
|
||||
const chalk = require('chalk');
|
||||
const termutils = require('tkwidgets/framework/termutils.js');
|
||||
const stripAnsi = require('strip-ansi');
|
||||
const { handleAutocompletion } = require('../autocompletion.js');
|
||||
|
||||
class StatusBarWidget extends BaseWidget {
|
||||
|
||||
@@ -41,6 +42,7 @@ class StatusBarWidget extends BaseWidget {
|
||||
};
|
||||
|
||||
if ('cursorPosition' in options) this.promptState_.cursorPosition = options.cursorPosition;
|
||||
if ('secure' in options) this.promptState_.secure = options.secure;
|
||||
|
||||
this.promptState_.promise = new Promise((resolve, reject) => {
|
||||
this.promptState_.resolve = resolve;
|
||||
@@ -104,13 +106,19 @@ class StatusBarWidget extends BaseWidget {
|
||||
|
||||
this.term.showCursor(true);
|
||||
|
||||
const isSecurePrompt = !!this.promptState_.secure;
|
||||
|
||||
let options = {
|
||||
cancelable: true,
|
||||
history: this.history,
|
||||
default: this.promptState_.initialText,
|
||||
autoComplete: handleAutocompletion,
|
||||
autoCompleteHint : true,
|
||||
autoCompleteMenu : true,
|
||||
};
|
||||
|
||||
if ('cursorPosition' in this.promptState_) options.cursorPosition = this.promptState_.cursorPosition;
|
||||
if (isSecurePrompt) options.echoChar = true;
|
||||
|
||||
this.inputEventEmitter_ = this.term.inputField(options, (error, input) => {
|
||||
let resolveResult = null;
|
||||
@@ -125,7 +133,7 @@ class StatusBarWidget extends BaseWidget {
|
||||
resolveResult = input ? input.trim() : input;
|
||||
// Add the command to history but only if it's longer than one character.
|
||||
// Below that it's usually an answer like "y"/"n", etc.
|
||||
if (input && input.length > 1) this.history_.push(input);
|
||||
if (!isSecurePrompt && input && input.length > 1) this.history_.push(input);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,4 +167,4 @@ class StatusBarWidget extends BaseWidget {
|
||||
|
||||
}
|
||||
|
||||
module.exports = StatusBarWidget;
|
||||
module.exports = StatusBarWidget;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
const fs = require('fs-extra');
|
||||
const { wrap } = require('lib/string-utils.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { fileExtension, basename, dirname } = require('lib/path-utils.js');
|
||||
const { _, setLocale, languageCode } = require('lib/locale.js');
|
||||
|
||||
|
@@ -1,26 +1,36 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Loading time: 20170803: 1.5s with no commands
|
||||
|
||||
// Make it possible to require("/lib/...") without specifying full path
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const compareVersion = require('compare-version');
|
||||
const nodeVersion = process && process.versions && process.versions.node ? process.versions.node : '0.0.0';
|
||||
if (compareVersion(nodeVersion, '8.0.0') < 0) {
|
||||
console.error('Joplin requires Node 8+. Detected version ' + nodeVersion);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { app } = require('./app.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Resource } = require('lib/models/resource.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const { NoteTag } = require('lib/models/note-tag.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const NoteTag = require('lib/models/NoteTag.js');
|
||||
const MasterKey = require('lib/models/MasterKey');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
||||
const { shimInit } = require('lib/shim-init-node.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
|
||||
const fsDriver = new FsDriverNode();
|
||||
Logger.fsDriver_ = fsDriver;
|
||||
Resource.fsDriver_ = fsDriver;
|
||||
EncryptionService.fsDriver_ = fsDriver;
|
||||
FileApiDriverLocal.fsDriver_ = fsDriver;
|
||||
|
||||
// That's not good, but it's to avoid circular dependency issues
|
||||
// in the BaseItem class.
|
||||
@@ -29,6 +39,7 @@ BaseItem.loadClass('Folder', Folder);
|
||||
BaseItem.loadClass('Resource', Resource);
|
||||
BaseItem.loadClass('Tag', Tag);
|
||||
BaseItem.loadClass('NoteTag', NoteTag);
|
||||
BaseItem.loadClass('MasterKey', MasterKey);
|
||||
|
||||
Setting.setConstant('appId', 'net.cozic.joplin-cli');
|
||||
Setting.setConstant('appType', 'cli');
|
||||
|
@@ -1,3 +0,0 @@
|
||||
#/bin/bash
|
||||
CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
NODE_PATH="$CLIENT_DIR/build" node "$CLIENT_DIR/build/build-translation.js" --silent
|
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
"$ROOT_DIR/build.sh" && NODE_PATH="$ROOT_DIR/build" node "$ROOT_DIR/build/build-website.js"
|
@@ -4,9 +4,6 @@ ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
BUILD_DIR="$ROOT_DIR/build"
|
||||
|
||||
rsync -a --exclude "node_modules/" "$ROOT_DIR/app/" "$BUILD_DIR/"
|
||||
rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
|
||||
rsync -a --delete "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
|
||||
cp "$ROOT_DIR/package.json" "$BUILD_DIR"
|
||||
chmod 755 "$BUILD_DIR/main.js"
|
||||
|
||||
# cd "$BUILD_DIR"
|
||||
# node build-translation.js --silent
|
||||
chmod 755 "$BUILD_DIR/main.js"
|
@@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
"$CLIENT_DIR/publish.sh"
|
||||
npm install -g joplin
|
1270
CliClient/locales/de_DE.po
Normal file
@@ -100,10 +100,17 @@ msgstr ""
|
||||
msgid "Cancelling background synchronisation... Please wait."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "No such command: %s"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cannot change encrypted item"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Missing required argument: %s"
|
||||
msgstr ""
|
||||
@@ -161,6 +168,35 @@ msgstr ""
|
||||
msgid "Note is not a to-do: \"%s\""
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
|
||||
"`status` and `target-status`."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter master password:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Operation cancelled"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Starting decryption... Please wait as it may take several minutes depending "
|
||||
"on how much there is to decrypt."
|
||||
msgstr ""
|
||||
|
||||
msgid "Completed decryption."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Encryption is: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit note."
|
||||
msgstr ""
|
||||
|
||||
@@ -204,7 +240,9 @@ msgstr ""
|
||||
msgid "Shortcuts are not available in CLI mode."
|
||||
msgstr ""
|
||||
|
||||
msgid "Type `help [command]` for more information about a command."
|
||||
msgid ""
|
||||
"Type `help [command]` for more information about a command; or type `help "
|
||||
"all` for the complete usage information."
|
||||
msgstr ""
|
||||
|
||||
msgid "The possible commands are:"
|
||||
@@ -339,8 +377,7 @@ msgstr ""
|
||||
msgid "Deletes the notebook without asking for confirmation."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Delete notebook \"%s\"?"
|
||||
msgid "Delete notebook? All notes within this notebook will also be deleted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Deletes the notes matching <note-pattern>."
|
||||
@@ -359,7 +396,12 @@ msgstr ""
|
||||
msgid "Searches for the given <pattern> in all the notes."
|
||||
msgstr ""
|
||||
|
||||
msgid "Sets the property <name> of the given <note> to the given [value]."
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"Sets the property <name> of the given <note> to the given [value]. Possible "
|
||||
"properties are:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Displays summary about the notes and notebooks."
|
||||
@@ -469,6 +511,22 @@ msgstr ""
|
||||
msgid "Search:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Welcome to Joplin!\n"
|
||||
"\n"
|
||||
"Type `:help shortcuts` for the list of keyboard shortcuts, or just `:help` "
|
||||
"for usage information.\n"
|
||||
"\n"
|
||||
"For example, to create a notebook press `mb`; to create a note press `mn`."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"One or more items are currently encrypted and you may need to supply a "
|
||||
"master password. To do so please type `e2ee decrypt`. If you have already "
|
||||
"supplied the password, the encrypted items are being decrypted in the "
|
||||
"background and will be available soon."
|
||||
msgstr ""
|
||||
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
@@ -508,7 +566,13 @@ msgstr ""
|
||||
msgid "Tools"
|
||||
msgstr ""
|
||||
|
||||
msgid "Options"
|
||||
msgid "Synchronisation status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encryption options"
|
||||
msgstr ""
|
||||
|
||||
msgid "General Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Help"
|
||||
@@ -530,6 +594,68 @@ msgstr ""
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Disabling encryption means *all* your notes and attachments are going to be "
|
||||
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
|
||||
"continue?"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Enabling encryption means *all* your notes and attachments are going to be "
|
||||
"re-synchronised and sent encrypted to the sync target. Do not lose the "
|
||||
"password as, for security purposes, this will be the *only* way to decrypt "
|
||||
"the data! To enable encryption, please enter your password below."
|
||||
msgstr ""
|
||||
|
||||
msgid "Disable encryption"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable encryption"
|
||||
msgstr ""
|
||||
|
||||
msgid "Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Source"
|
||||
msgstr ""
|
||||
|
||||
msgid "Created"
|
||||
msgstr ""
|
||||
|
||||
msgid "Updated"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password OK"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Note: Only one master key is going to be used for encryption (the one marked "
|
||||
"as \"active\"). Any of the keys might be used for decryption, depending on "
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encryption is:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
@@ -541,15 +667,9 @@ msgstr ""
|
||||
msgid "Please create a notebook first."
|
||||
msgstr ""
|
||||
|
||||
msgid "Note title:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please create a notebook first"
|
||||
msgstr ""
|
||||
|
||||
msgid "To-do title:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notebook title:"
|
||||
msgstr ""
|
||||
|
||||
@@ -568,6 +688,18 @@ msgstr ""
|
||||
msgid "Layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr ""
|
||||
|
||||
msgid "View them now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some items cannot be decrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Set the password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr ""
|
||||
|
||||
@@ -583,6 +715,10 @@ msgstr ""
|
||||
msgid "No notes in here. Create one by clicking on \"New note\"."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr ""
|
||||
@@ -605,7 +741,13 @@ msgstr ""
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete notebook?"
|
||||
msgid "Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encryption Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove this tag from all the notes?"
|
||||
@@ -629,6 +771,9 @@ msgstr ""
|
||||
msgid "Searches"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please select where the sync status should be exported to"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Usage: %s"
|
||||
msgstr ""
|
||||
@@ -696,6 +841,10 @@ msgstr ""
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "State: \"%s\"."
|
||||
msgstr ""
|
||||
@@ -711,6 +860,12 @@ msgstr ""
|
||||
msgid "Synchronisation is already in progress. State: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encrypted"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encrypted items cannot be modified"
|
||||
msgstr ""
|
||||
|
||||
msgid "Conflicts"
|
||||
msgstr ""
|
||||
|
||||
@@ -736,14 +891,6 @@ msgstr ""
|
||||
msgid "Cannot move note to \"%s\" notebook"
|
||||
msgstr ""
|
||||
|
||||
msgid "File system synchronisation target directory"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The path to synchronise with when file system synchronisation is enabled. "
|
||||
"See `sync.target`."
|
||||
msgstr ""
|
||||
|
||||
msgid "Text editor"
|
||||
msgstr ""
|
||||
|
||||
@@ -779,9 +926,6 @@ msgstr ""
|
||||
msgid "Synchronisation interval"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "%d minutes"
|
||||
msgstr ""
|
||||
@@ -808,10 +952,31 @@ msgid ""
|
||||
"`sync.2.path` to specify the target directory."
|
||||
msgstr ""
|
||||
|
||||
msgid "Directory to synchronise with (absolute path)"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The path to synchronise with when file system synchronisation is enabled. "
|
||||
"See `sync.target`."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr ""
|
||||
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "%s (%s): %s"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"These items will remain on the device but will not be uploaded to the sync "
|
||||
"target. In order to find these items, either search for the title or the ID "
|
||||
"(which is displayed in brackets above)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr ""
|
||||
|
||||
@@ -854,10 +1019,10 @@ msgstr ""
|
||||
msgid "Log"
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgid "Export Debug Report"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export Debug Report"
|
||||
msgid "Encryption Config"
|
||||
msgstr ""
|
||||
|
||||
msgid "Configuration"
|
||||
@@ -870,6 +1035,9 @@ msgstr ""
|
||||
msgid "Move %d notes to notebook \"%s\"?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Press to set the decryption password."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select date"
|
||||
msgstr ""
|
||||
|
||||
@@ -879,6 +1047,23 @@ msgstr ""
|
||||
msgid "Cancel synchronisation"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Master Key %s"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Created: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password cannot be empty"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "The notebook could not be saved: %s"
|
||||
msgstr ""
|
||||
|
1235
CliClient/locales/es_ES.po
Normal file
@@ -100,11 +100,18 @@ msgstr "o"
|
||||
msgid "Cancelling background synchronisation... Please wait."
|
||||
msgstr "Annulation de la synchronisation... Veuillez patienter."
|
||||
|
||||
#, javascript-format
|
||||
msgid "No such command: %s"
|
||||
msgstr "Commande invalide : %s"
|
||||
|
||||
#, javascript-format
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr ""
|
||||
"La commande \"%s\" est disponible uniquement en mode d'interface graphique"
|
||||
|
||||
msgid "Cannot change encrypted item"
|
||||
msgstr "Un objet crypté ne peut pas être modifié"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Missing required argument: %s"
|
||||
msgstr "Paramètre requis manquant : %s"
|
||||
@@ -167,6 +174,39 @@ msgstr "Marquer la tâche comme complétée."
|
||||
msgid "Note is not a to-do: \"%s\""
|
||||
msgstr "La note n'est pas une tâche : \"%s\""
|
||||
|
||||
msgid ""
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
|
||||
"`status` and `target-status`."
|
||||
msgstr ""
|
||||
"Gérer la configuration E2EE (Cryptage de bout à bout). Les commandes sont "
|
||||
"`enable`, `disable`, `decrypt` et `status` et `target-status`."
|
||||
|
||||
msgid "Enter master password:"
|
||||
msgstr "Entrer le mot de passe maître :"
|
||||
|
||||
msgid "Operation cancelled"
|
||||
msgstr "Opération annulée"
|
||||
|
||||
msgid ""
|
||||
"Starting decryption... Please wait as it may take several minutes depending "
|
||||
"on how much there is to decrypt."
|
||||
msgstr ""
|
||||
"Démarrage du décryptage... Veuillez patienter car cela pourrait prendre "
|
||||
"plusieurs minutes selon le nombre d'objets à décrypter."
|
||||
|
||||
msgid "Completed decryption."
|
||||
msgstr "Décryptage complété."
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "Activé"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Désactivé"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Encryption is: %s"
|
||||
msgstr "Le cryptage est : %s"
|
||||
|
||||
msgid "Edit note."
|
||||
msgstr "Éditer la note."
|
||||
|
||||
@@ -217,8 +257,12 @@ msgstr "Affiche les informations d'utilisation."
|
||||
msgid "Shortcuts are not available in CLI mode."
|
||||
msgstr "Les raccourcis ne sont pas disponible en mode de ligne de commande."
|
||||
|
||||
msgid "Type `help [command]` for more information about a command."
|
||||
msgstr "Tapez `help [command]` pour plus d'information sur une commande."
|
||||
msgid ""
|
||||
"Type `help [command]` for more information about a command; or type `help "
|
||||
"all` for the complete usage information."
|
||||
msgstr ""
|
||||
"Tapez `help [command]` pour plus d'information sur une commande ; ou tapez "
|
||||
"`help all` pour l'aide complète."
|
||||
|
||||
msgid "The possible commands are:"
|
||||
msgstr "Les commandes possibles sont :"
|
||||
@@ -370,9 +414,10 @@ msgstr "Supprimer le carnet."
|
||||
msgid "Deletes the notebook without asking for confirmation."
|
||||
msgstr "Supprimer le carnet sans demander la confirmation."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Delete notebook \"%s\"?"
|
||||
msgstr "Supprimer le carnet \"%s\" ?"
|
||||
msgid "Delete notebook? All notes within this notebook will also be deleted."
|
||||
msgstr ""
|
||||
"Effacer le carnet ? Toutes les notes dans ce carnet seront également "
|
||||
"effacées."
|
||||
|
||||
msgid "Deletes the notes matching <note-pattern>."
|
||||
msgstr "Supprimer les notes correspondants à <note-pattern>."
|
||||
@@ -390,8 +435,17 @@ msgstr "Supprimer la note ?"
|
||||
msgid "Searches for the given <pattern> in all the notes."
|
||||
msgstr "Chercher le motif <pattern> dans toutes les notes."
|
||||
|
||||
msgid "Sets the property <name> of the given <note> to the given [value]."
|
||||
msgstr "Assigner la valeur [value] à la propriété <name> de la <note> donnée."
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"Sets the property <name> of the given <note> to the given [value]. Possible "
|
||||
"properties are:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr ""
|
||||
"Assigner la valeur [value] à la propriété <name> de la <note> donnée. Les "
|
||||
"valeurs possibles sont :\n"
|
||||
"\n"
|
||||
"%s"
|
||||
|
||||
msgid "Displays summary about the notes and notebooks."
|
||||
msgstr "Afficher un résumé des notes et carnets."
|
||||
@@ -519,6 +573,33 @@ msgstr ""
|
||||
msgid "Search:"
|
||||
msgstr "Recherche :"
|
||||
|
||||
msgid ""
|
||||
"Welcome to Joplin!\n"
|
||||
"\n"
|
||||
"Type `:help shortcuts` for the list of keyboard shortcuts, or just `:help` "
|
||||
"for usage information.\n"
|
||||
"\n"
|
||||
"For example, to create a notebook press `mb`; to create a note press `mn`."
|
||||
msgstr ""
|
||||
"Bienvenue dans Joplin!\n"
|
||||
"\n"
|
||||
"Tapez `:help shortcuts` pour la liste des raccourcis claviers, ou simplement "
|
||||
"`:help` pour une vue d'ensemble.\n"
|
||||
"\n"
|
||||
"Par exemple, pour créer un carnet, pressez `mb` ; pour créer une note "
|
||||
"pressed `mn`."
|
||||
|
||||
msgid ""
|
||||
"One or more items are currently encrypted and you may need to supply a "
|
||||
"master password. To do so please type `e2ee decrypt`. If you have already "
|
||||
"supplied the password, the encrypted items are being decrypted in the "
|
||||
"background and will be available soon."
|
||||
msgstr ""
|
||||
"Au moins un objet est actuellement crypté et il se peut que vous deviez "
|
||||
"fournir votre mot de passe maître. Pour se faire, veuillez taper `e2ee "
|
||||
"decrypt`. Si vous avez déjà fourni ce mot de passe, les objets cryptés vont "
|
||||
"être décrypté en tâche de fond et seront disponible prochainement."
|
||||
|
||||
msgid "File"
|
||||
msgstr "Fichier"
|
||||
|
||||
@@ -558,8 +639,14 @@ msgstr "Chercher dans toutes les notes"
|
||||
msgid "Tools"
|
||||
msgstr "Outils"
|
||||
|
||||
msgid "Options"
|
||||
msgstr "Options"
|
||||
msgid "Synchronisation status"
|
||||
msgstr "État de la synchronisation"
|
||||
|
||||
msgid "Encryption options"
|
||||
msgstr "Options de cryptage"
|
||||
|
||||
msgid "General Options"
|
||||
msgstr "Options générales"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Aide"
|
||||
@@ -580,6 +667,80 @@ msgstr "OK"
|
||||
msgid "Cancel"
|
||||
msgstr "Annulation"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
msgstr "Les notes et paramètres se trouve dans : %s"
|
||||
|
||||
msgid "Save"
|
||||
msgstr "Enregistrer"
|
||||
|
||||
msgid ""
|
||||
"Disabling encryption means *all* your notes and attachments are going to be "
|
||||
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
|
||||
"continue?"
|
||||
msgstr ""
|
||||
"Désactiver le cryptage signifie que *toutes* les notes et fichiers vont être "
|
||||
"re-synchronisés et envoyés décryptés sur la cible de la synchronisation. "
|
||||
"Souhaitez vous continuer ?"
|
||||
|
||||
msgid ""
|
||||
"Enabling encryption means *all* your notes and attachments are going to be "
|
||||
"re-synchronised and sent encrypted to the sync target. Do not lose the "
|
||||
"password as, for security purposes, this will be the *only* way to decrypt "
|
||||
"the data! To enable encryption, please enter your password below."
|
||||
msgstr ""
|
||||
"Activer le cryptage signifie que *toutes* les notes et fichiers vont être re-"
|
||||
"synchronisés et envoyés cryptés vers la cible de la synchronisation. Ne "
|
||||
"perdez pas votre mot de passe car, pour des raisons de sécurité, ce sera la "
|
||||
"*seule* façon de décrypter les données ! Pour activer le cryptage, veuillez "
|
||||
"entrer votre mot de passe ci-dessous."
|
||||
|
||||
msgid "Disable encryption"
|
||||
msgstr "Désactiver le cryptage"
|
||||
|
||||
msgid "Enable encryption"
|
||||
msgstr "Activer le cryptage"
|
||||
|
||||
msgid "Master Keys"
|
||||
msgstr "Clefs maître"
|
||||
|
||||
msgid "Active"
|
||||
msgstr "Actif"
|
||||
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
msgid "Source"
|
||||
msgstr "Source"
|
||||
|
||||
msgid "Created"
|
||||
msgstr "Créé"
|
||||
|
||||
msgid "Updated"
|
||||
msgstr "Mis à jour"
|
||||
|
||||
msgid "Password"
|
||||
msgstr "Mot de passe"
|
||||
|
||||
msgid "Password OK"
|
||||
msgstr "Mot de passe OK"
|
||||
|
||||
msgid ""
|
||||
"Note: Only one master key is going to be used for encryption (the one marked "
|
||||
"as \"active\"). Any of the keys might be used for decryption, depending on "
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
"Note : seule une clef maître va être utilisée pour le cryptage (celle "
|
||||
"marquée comme \"actif\" ci-dessus). N'importe quel clef peut-être utilisée "
|
||||
"pour le décryptage, selon la façon dont les notes ou carnets étaient cryptés "
|
||||
"à l'origine."
|
||||
|
||||
msgid "Status"
|
||||
msgstr "État"
|
||||
|
||||
msgid "Encryption is:"
|
||||
msgstr "Le cryptage est :"
|
||||
|
||||
msgid "Back"
|
||||
msgstr "Retour"
|
||||
|
||||
@@ -593,15 +754,9 @@ msgstr ""
|
||||
msgid "Please create a notebook first."
|
||||
msgstr "Veuillez d'abord sélectionner un carnet."
|
||||
|
||||
msgid "Note title:"
|
||||
msgstr "Titre de la note :"
|
||||
|
||||
msgid "Please create a notebook first"
|
||||
msgstr "Veuillez d'abord créer un carnet d'abord"
|
||||
|
||||
msgid "To-do title:"
|
||||
msgstr "Titre de la tâche :"
|
||||
|
||||
msgid "Notebook title:"
|
||||
msgstr "Titre du carnet :"
|
||||
|
||||
@@ -614,13 +769,24 @@ msgstr "Séparez chaque étiquette par une virgule."
|
||||
msgid "Rename notebook:"
|
||||
msgstr "Renommer le carnet :"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Set alarm:"
|
||||
msgstr "Définir ou modifier alarme"
|
||||
msgstr "Régler alarme :"
|
||||
|
||||
msgid "Layout"
|
||||
msgstr "Disposition"
|
||||
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr "Certains objets ne peuvent être synchronisés."
|
||||
|
||||
msgid "View them now"
|
||||
msgstr "Les voir maintenant"
|
||||
|
||||
msgid "Some items cannot be decrypted."
|
||||
msgstr "Certains objets ne peuvent être décryptés."
|
||||
|
||||
msgid "Set the password"
|
||||
msgstr "Définir le mot de passe"
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr "Gérer les étiquettes"
|
||||
|
||||
@@ -637,6 +803,12 @@ msgid "No notes in here. Create one by clicking on \"New note\"."
|
||||
msgstr ""
|
||||
"Pas de notes ici. Créez-en une en pressant le bouton \"Nouvelle note\"."
|
||||
|
||||
msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr ""
|
||||
"Il n'y a pour l'instant aucun carnet. Créez-en un en cliquant sur \"Nouveau "
|
||||
"carnet\"."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr "Lien ou message non géré : %s"
|
||||
@@ -644,9 +816,8 @@ msgstr "Lien ou message non géré : %s"
|
||||
msgid "Attach file"
|
||||
msgstr "Attacher un fichier"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Set alarm"
|
||||
msgstr "Définir ou modifier alarme"
|
||||
msgstr "Régler alarme"
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Rafraîchir"
|
||||
@@ -660,8 +831,14 @@ msgstr "Connexion OneDrive"
|
||||
msgid "Import"
|
||||
msgstr "Importer"
|
||||
|
||||
msgid "Delete notebook?"
|
||||
msgstr "Supprimer le carnet ?"
|
||||
msgid "Options"
|
||||
msgstr "Options"
|
||||
|
||||
msgid "Synchronisation Status"
|
||||
msgstr "État de la synchronisation"
|
||||
|
||||
msgid "Encryption Options"
|
||||
msgstr "Options de cryptage"
|
||||
|
||||
msgid "Remove this tag from all the notes?"
|
||||
msgstr "Enlever cette étiquette de toutes les notes ?"
|
||||
@@ -684,6 +861,10 @@ msgstr "Étiquettes"
|
||||
msgid "Searches"
|
||||
msgstr "Recherches"
|
||||
|
||||
msgid "Please select where the sync status should be exported to"
|
||||
msgstr ""
|
||||
"Veuillez sélectionner un répertoire ou exporter l'état de la synchronisation"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Usage: %s"
|
||||
msgstr "Utilisation : %s"
|
||||
@@ -724,6 +905,12 @@ msgid ""
|
||||
"\n"
|
||||
"Please consider using a regular OneDrive account."
|
||||
msgstr ""
|
||||
"Impossible de synchroniser avec OneDrive.\n"
|
||||
"\n"
|
||||
"Cette erreur se produit lors de l'utilisation de OneDrive for Business, qui "
|
||||
"malheureusement n'est pas compatible.\n"
|
||||
"\n"
|
||||
"Veuillez utiliser à la place un compte OneDrive normal."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Cannot access %s"
|
||||
@@ -753,6 +940,10 @@ msgstr "Objets supprimés localement : %d."
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr "Objets distants supprimés : %d."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr "Téléchargés : %d/%d."
|
||||
|
||||
#, javascript-format
|
||||
msgid "State: \"%s\"."
|
||||
msgstr "État : \"%s\"."
|
||||
@@ -768,6 +959,12 @@ msgstr "Terminé : %s"
|
||||
msgid "Synchronisation is already in progress. State: %s"
|
||||
msgstr "La synchronisation est déjà en cours. État : %s"
|
||||
|
||||
msgid "Encrypted"
|
||||
msgstr "Crypté"
|
||||
|
||||
msgid "Encrypted items cannot be modified"
|
||||
msgstr "Les objets cryptés ne peuvent être modifiés"
|
||||
|
||||
msgid "Conflicts"
|
||||
msgstr "Conflits"
|
||||
|
||||
@@ -793,16 +990,6 @@ msgstr "Impossible de copier la note vers le carnet \"%s\""
|
||||
msgid "Cannot move note to \"%s\" notebook"
|
||||
msgstr "Impossible de déplacer la note vers le carnet \"%s\""
|
||||
|
||||
msgid "File system synchronisation target directory"
|
||||
msgstr "Cible de la synchronisation sur le disque dur"
|
||||
|
||||
msgid ""
|
||||
"The path to synchronise with when file system synchronisation is enabled. "
|
||||
"See `sync.target`."
|
||||
msgstr ""
|
||||
"Le chemin du répertoire avec lequel synchroniser lorsque la synchronisation "
|
||||
"par système de fichier est activée. Voir `sync.target`."
|
||||
|
||||
msgid "Text editor"
|
||||
msgstr "Éditeur de texte"
|
||||
|
||||
@@ -840,9 +1027,6 @@ msgstr "Enregistrer l'emplacement avec les notes"
|
||||
msgid "Synchronisation interval"
|
||||
msgstr "Intervalle de synchronisation"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Désactivé"
|
||||
|
||||
#, javascript-format
|
||||
msgid "%d minutes"
|
||||
msgstr "%d minutes"
|
||||
@@ -871,10 +1055,36 @@ msgstr ""
|
||||
"La cible avec laquelle synchroniser. Pour synchroniser avec le système de "
|
||||
"fichier, veuillez spécifier le répertoire avec `sync.2.path`."
|
||||
|
||||
msgid "Directory to synchronise with (absolute path)"
|
||||
msgstr "Répertoire avec lequel synchroniser (chemin absolu)"
|
||||
|
||||
msgid ""
|
||||
"The path to synchronise with when file system synchronisation is enabled. "
|
||||
"See `sync.target`."
|
||||
msgstr ""
|
||||
"Le chemin du répertoire avec lequel synchroniser lorsque la synchronisation "
|
||||
"par système de fichier est activée. Voir `sync.target`."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Option invalide: \"%s\". Les valeurs possibles sont : %s."
|
||||
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr "Objets qui ne peuvent pas être synchronisés"
|
||||
|
||||
#, javascript-format
|
||||
msgid "%s (%s): %s"
|
||||
msgstr "%s (%s) : %s"
|
||||
|
||||
msgid ""
|
||||
"These items will remain on the device but will not be uploaded to the sync "
|
||||
"target. In order to find these items, either search for the title or the ID "
|
||||
"(which is displayed in brackets above)."
|
||||
msgstr ""
|
||||
"Ces objets resteront sur l'appareil mais ne seront pas envoyé sur la cible "
|
||||
"de la synchronisation. Pour trouver ces objets, faite une recherche sur le "
|
||||
"titre ou l'identifiant de l'objet (affiché ci-dessus entre parenthèses)."
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr "Status de la synchronisation (objets synchro. / total)"
|
||||
|
||||
@@ -919,12 +1129,12 @@ msgstr "Supprimer ces notes ?"
|
||||
msgid "Log"
|
||||
msgstr "Journal"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "État"
|
||||
|
||||
msgid "Export Debug Report"
|
||||
msgstr "Exporter rapport de débogage"
|
||||
|
||||
msgid "Encryption Config"
|
||||
msgstr "Config cryptage"
|
||||
|
||||
msgid "Configuration"
|
||||
msgstr "Configuration"
|
||||
|
||||
@@ -935,6 +1145,9 @@ msgstr "Déplacer la note vers carnet..."
|
||||
msgid "Move %d notes to notebook \"%s\"?"
|
||||
msgstr "Déplacer %d notes vers carnet \"%s\" ?"
|
||||
|
||||
msgid "Press to set the decryption password."
|
||||
msgstr "Définir mot de passe de synchronisation."
|
||||
|
||||
msgid "Select date"
|
||||
msgstr "Sélectionner date"
|
||||
|
||||
@@ -944,6 +1157,23 @@ msgstr "Confirmer"
|
||||
msgid "Cancel synchronisation"
|
||||
msgstr "Annuler synchronisation"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Master Key %s"
|
||||
msgstr "Clef maître %s"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Created: %s"
|
||||
msgstr "Créé : %s"
|
||||
|
||||
msgid "Password:"
|
||||
msgstr "Mot de passe :"
|
||||
|
||||
msgid "Password cannot be empty"
|
||||
msgstr "Mot de passe ne peut être vide"
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "Activer"
|
||||
|
||||
#, javascript-format
|
||||
msgid "The notebook could not be saved: %s"
|
||||
msgstr "Ce carnet n'a pas pu être sauvegardé : %s"
|
||||
@@ -980,10 +1210,10 @@ msgid "Hide metadata"
|
||||
msgstr "Cacher les métadonnées"
|
||||
|
||||
msgid "Show metadata"
|
||||
msgstr "Afficher les métadonnées"
|
||||
msgstr "Voir métadonnées"
|
||||
|
||||
msgid "View on map"
|
||||
msgstr "Voir emplacement sur carte"
|
||||
msgstr "Voir sur carte"
|
||||
|
||||
msgid "Delete notebook"
|
||||
msgstr "Supprimer le carnet"
|
||||
@@ -1006,6 +1236,21 @@ msgstr ""
|
||||
msgid "Welcome"
|
||||
msgstr "Bienvenue"
|
||||
|
||||
#~ msgid "Note title:"
|
||||
#~ msgstr "Titre de la note :"
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "Titre de la tâche :"
|
||||
|
||||
#~ msgid "Delete notebook?"
|
||||
#~ msgstr "Supprimer le carnet ?"
|
||||
|
||||
#~ msgid "Delete notebook \"%s\"?"
|
||||
#~ msgstr "Supprimer le carnet \"%s\" ?"
|
||||
|
||||
#~ msgid "File system synchronisation target directory"
|
||||
#~ msgstr "Cible de la synchronisation sur le disque dur"
|
||||
|
||||
#~ msgid "Set or clear alarm:"
|
||||
#~ msgstr "Définir ou modifier alarme :"
|
||||
|
||||
@@ -1071,17 +1316,10 @@ msgstr "Bienvenue"
|
||||
#~ msgid "Delete a note"
|
||||
#~ msgstr "Supprimer la note"
|
||||
|
||||
#~ msgid "%s (%s)"
|
||||
#~ msgstr "%s (%s)"
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "Show/Hide the console"
|
||||
#~ msgstr "Quitter le logiciel."
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "Last command: %s"
|
||||
#~ msgstr "Commande invalide : \"%s\""
|
||||
|
||||
#~ msgid "Done editing."
|
||||
#~ msgstr "Edition terminée."
|
||||
|
||||
@@ -1183,12 +1421,6 @@ msgstr "Bienvenue"
|
||||
#~ "Tous les ports sont en cours d'utilisation. Veuillez signaler ce problème "
|
||||
#~ "sur %s"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "There is currently no notebook. Create one by clicking on the (+) button."
|
||||
#~ msgstr ""
|
||||
#~ "Il n'y a pour l'instant aucun carnet. Créez-en un en cliquant sur le "
|
||||
#~ "bouton (+)"
|
||||
|
||||
#~ msgid "Synchronizing with directory \"%s\""
|
||||
#~ msgstr "Synchronisation avec dossier \"%s\""
|
||||
|
||||
|
1231
CliClient/locales/hr_HR.po
Normal file
1225
CliClient/locales/it_IT.po
Normal file
1215
CliClient/locales/ja_JP.po
Normal file
@@ -100,10 +100,17 @@ msgstr ""
|
||||
msgid "Cancelling background synchronisation... Please wait."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "No such command: %s"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cannot change encrypted item"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Missing required argument: %s"
|
||||
msgstr ""
|
||||
@@ -161,6 +168,35 @@ msgstr ""
|
||||
msgid "Note is not a to-do: \"%s\""
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
|
||||
"`status` and `target-status`."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter master password:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Operation cancelled"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Starting decryption... Please wait as it may take several minutes depending "
|
||||
"on how much there is to decrypt."
|
||||
msgstr ""
|
||||
|
||||
msgid "Completed decryption."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Encryption is: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit note."
|
||||
msgstr ""
|
||||
|
||||
@@ -204,7 +240,9 @@ msgstr ""
|
||||
msgid "Shortcuts are not available in CLI mode."
|
||||
msgstr ""
|
||||
|
||||
msgid "Type `help [command]` for more information about a command."
|
||||
msgid ""
|
||||
"Type `help [command]` for more information about a command; or type `help "
|
||||
"all` for the complete usage information."
|
||||
msgstr ""
|
||||
|
||||
msgid "The possible commands are:"
|
||||
@@ -339,8 +377,7 @@ msgstr ""
|
||||
msgid "Deletes the notebook without asking for confirmation."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Delete notebook \"%s\"?"
|
||||
msgid "Delete notebook? All notes within this notebook will also be deleted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Deletes the notes matching <note-pattern>."
|
||||
@@ -359,7 +396,12 @@ msgstr ""
|
||||
msgid "Searches for the given <pattern> in all the notes."
|
||||
msgstr ""
|
||||
|
||||
msgid "Sets the property <name> of the given <note> to the given [value]."
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"Sets the property <name> of the given <note> to the given [value]. Possible "
|
||||
"properties are:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Displays summary about the notes and notebooks."
|
||||
@@ -469,6 +511,22 @@ msgstr ""
|
||||
msgid "Search:"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Welcome to Joplin!\n"
|
||||
"\n"
|
||||
"Type `:help shortcuts` for the list of keyboard shortcuts, or just `:help` "
|
||||
"for usage information.\n"
|
||||
"\n"
|
||||
"For example, to create a notebook press `mb`; to create a note press `mn`."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"One or more items are currently encrypted and you may need to supply a "
|
||||
"master password. To do so please type `e2ee decrypt`. If you have already "
|
||||
"supplied the password, the encrypted items are being decrypted in the "
|
||||
"background and will be available soon."
|
||||
msgstr ""
|
||||
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
@@ -508,7 +566,13 @@ msgstr ""
|
||||
msgid "Tools"
|
||||
msgstr ""
|
||||
|
||||
msgid "Options"
|
||||
msgid "Synchronisation status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encryption options"
|
||||
msgstr ""
|
||||
|
||||
msgid "General Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Help"
|
||||
@@ -530,6 +594,68 @@ msgstr ""
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Disabling encryption means *all* your notes and attachments are going to be "
|
||||
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
|
||||
"continue?"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Enabling encryption means *all* your notes and attachments are going to be "
|
||||
"re-synchronised and sent encrypted to the sync target. Do not lose the "
|
||||
"password as, for security purposes, this will be the *only* way to decrypt "
|
||||
"the data! To enable encryption, please enter your password below."
|
||||
msgstr ""
|
||||
|
||||
msgid "Disable encryption"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable encryption"
|
||||
msgstr ""
|
||||
|
||||
msgid "Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Source"
|
||||
msgstr ""
|
||||
|
||||
msgid "Created"
|
||||
msgstr ""
|
||||
|
||||
msgid "Updated"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password OK"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Note: Only one master key is going to be used for encryption (the one marked "
|
||||
"as \"active\"). Any of the keys might be used for decryption, depending on "
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encryption is:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
@@ -541,15 +667,9 @@ msgstr ""
|
||||
msgid "Please create a notebook first."
|
||||
msgstr ""
|
||||
|
||||
msgid "Note title:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please create a notebook first"
|
||||
msgstr ""
|
||||
|
||||
msgid "To-do title:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notebook title:"
|
||||
msgstr ""
|
||||
|
||||
@@ -568,6 +688,18 @@ msgstr ""
|
||||
msgid "Layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr ""
|
||||
|
||||
msgid "View them now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some items cannot be decrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Set the password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr ""
|
||||
|
||||
@@ -583,6 +715,10 @@ msgstr ""
|
||||
msgid "No notes in here. Create one by clicking on \"New note\"."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr ""
|
||||
@@ -605,7 +741,13 @@ msgstr ""
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete notebook?"
|
||||
msgid "Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encryption Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove this tag from all the notes?"
|
||||
@@ -629,6 +771,9 @@ msgstr ""
|
||||
msgid "Searches"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please select where the sync status should be exported to"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Usage: %s"
|
||||
msgstr ""
|
||||
@@ -696,6 +841,10 @@ msgstr ""
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "State: \"%s\"."
|
||||
msgstr ""
|
||||
@@ -711,6 +860,12 @@ msgstr ""
|
||||
msgid "Synchronisation is already in progress. State: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encrypted"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encrypted items cannot be modified"
|
||||
msgstr ""
|
||||
|
||||
msgid "Conflicts"
|
||||
msgstr ""
|
||||
|
||||
@@ -736,14 +891,6 @@ msgstr ""
|
||||
msgid "Cannot move note to \"%s\" notebook"
|
||||
msgstr ""
|
||||
|
||||
msgid "File system synchronisation target directory"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The path to synchronise with when file system synchronisation is enabled. "
|
||||
"See `sync.target`."
|
||||
msgstr ""
|
||||
|
||||
msgid "Text editor"
|
||||
msgstr ""
|
||||
|
||||
@@ -779,9 +926,6 @@ msgstr ""
|
||||
msgid "Synchronisation interval"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "%d minutes"
|
||||
msgstr ""
|
||||
@@ -808,10 +952,31 @@ msgid ""
|
||||
"`sync.2.path` to specify the target directory."
|
||||
msgstr ""
|
||||
|
||||
msgid "Directory to synchronise with (absolute path)"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The path to synchronise with when file system synchronisation is enabled. "
|
||||
"See `sync.target`."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr ""
|
||||
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "%s (%s): %s"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"These items will remain on the device but will not be uploaded to the sync "
|
||||
"target. In order to find these items, either search for the title or the ID "
|
||||
"(which is displayed in brackets above)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr ""
|
||||
|
||||
@@ -854,10 +1019,10 @@ msgstr ""
|
||||
msgid "Log"
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgid "Export Debug Report"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export Debug Report"
|
||||
msgid "Encryption Config"
|
||||
msgstr ""
|
||||
|
||||
msgid "Configuration"
|
||||
@@ -870,6 +1035,9 @@ msgstr ""
|
||||
msgid "Move %d notes to notebook \"%s\"?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Press to set the decryption password."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select date"
|
||||
msgstr ""
|
||||
|
||||
@@ -879,6 +1047,23 @@ msgstr ""
|
||||
msgid "Cancel synchronisation"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Master Key %s"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Created: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password cannot be empty"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "The notebook could not be saved: %s"
|
||||
msgstr ""
|
||||
|
1238
CliClient/locales/nl_BE.po
Normal file
1218
CliClient/locales/pt_BR.po
Normal file
1234
CliClient/locales/ru_RU.po
Normal file
1177
CliClient/locales/zh_CN.po
Normal file
81
CliClient/package-lock.json
generated
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "0.10.77",
|
||||
"version": "0.10.88",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.3.0.tgz",
|
||||
"integrity": "sha1-RBT/dKUIecII7l/cgm4ywwNUnto=",
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
|
||||
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
|
||||
"requires": {
|
||||
"co": "4.6.0",
|
||||
"fast-deep-equal": "1.0.0",
|
||||
@@ -197,6 +197,11 @@
|
||||
"delayed-stream": "1.0.0"
|
||||
}
|
||||
},
|
||||
"compare-version": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
|
||||
"integrity": "sha1-AWLsLZNR9d3VmpICy6k1NmpyUIA="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -411,12 +416,12 @@
|
||||
}
|
||||
},
|
||||
"fs-extra": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz",
|
||||
"integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz",
|
||||
"integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==",
|
||||
"requires": {
|
||||
"graceful-fs": "4.1.11",
|
||||
"jsonfile": "3.0.1",
|
||||
"jsonfile": "4.0.0",
|
||||
"universalify": "0.1.1"
|
||||
}
|
||||
},
|
||||
@@ -437,7 +442,7 @@
|
||||
"ndarray": "1.0.18",
|
||||
"ndarray-pack": "1.2.1",
|
||||
"node-bitmap": "0.0.1",
|
||||
"omggif": "1.0.8",
|
||||
"omggif": "1.0.9",
|
||||
"parse-data-uri": "0.2.0",
|
||||
"pngjs": "2.3.1",
|
||||
"request": "2.83.0",
|
||||
@@ -460,16 +465,6 @@
|
||||
"assert-plus": "1.0.0"
|
||||
}
|
||||
},
|
||||
"gettext-parser": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.3.0.tgz",
|
||||
"integrity": "sha512-iloxjcw+uTPnQ8DrGICWtqkHNgk3mAiDI77pLmXQCnhM+BxFQXstzTA4zj3EpIYMysRQnnNzHyHzBUEazz80Sw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"encoding": "0.1.12",
|
||||
"safe-buffer": "5.1.1"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||
@@ -499,7 +494,7 @@
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
|
||||
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
|
||||
"requires": {
|
||||
"ajv": "5.3.0",
|
||||
"ajv": "5.5.2",
|
||||
"har-schema": "2.0.0"
|
||||
}
|
||||
},
|
||||
@@ -738,9 +733,9 @@
|
||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz",
|
||||
"integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||
"requires": {
|
||||
"graceful-fs": "4.1.11"
|
||||
}
|
||||
@@ -797,12 +792,6 @@
|
||||
"highlight.js": "9.12.0"
|
||||
}
|
||||
},
|
||||
"marked": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz",
|
||||
"integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=",
|
||||
"dev": true
|
||||
},
|
||||
"md5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
|
||||
@@ -884,12 +873,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"mustache": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.0.tgz",
|
||||
"integrity": "sha1-QCj3d4sXcIpImTCm5SrDvKDaQdA=",
|
||||
"dev": true
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz",
|
||||
@@ -970,9 +953,9 @@
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||
},
|
||||
"omggif": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.8.tgz",
|
||||
"integrity": "sha1-F483sqsLPXtG7ToORr0HkLWNNTA="
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.9.tgz",
|
||||
"integrity": "sha1-3LcCTazVDFK00wPwSALJHAV8dl8="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
@@ -1937,9 +1920,9 @@
|
||||
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
|
||||
},
|
||||
"string-kit": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/string-kit/-/string-kit-0.6.3.tgz",
|
||||
"integrity": "sha512-G2T92klsuE+S9mqdKQyWurFweNQV5X+FRzSKTqYHRdaVUN/4dL6urbYJJ+xb9ep/4XWm+4RNT8j3acncNhFRBg==",
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/string-kit/-/string-kit-0.6.4.tgz",
|
||||
"integrity": "sha512-imrOojdsXlL6xzfERCxvc/iA9Zwpzbfs+qeP6VB0s0rQVnMc3Nwkyhge0e8Uoayph7PVAwPNmLpohox27G3fgA==",
|
||||
"requires": {
|
||||
"xregexp": "3.2.0"
|
||||
}
|
||||
@@ -2036,15 +2019,15 @@
|
||||
}
|
||||
},
|
||||
"terminal-kit": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-1.14.0.tgz",
|
||||
"integrity": "sha512-ir0I2QtcBDSg2w0UvohlqdDpGlS3S2UYBG4NnYKnK/4VywgnbfxgdpXN3el0uCH3OeH6fG38luW7RmDM96FqUw==",
|
||||
"version": "1.14.3",
|
||||
"resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-1.14.3.tgz",
|
||||
"integrity": "sha512-ZHtuElnBhK0IXOYNvQ7eYgaArwEoOv7saQc4Q0Z9p02JeC7iajC20/odV77BKB3jw/Qthvf9mpASf8gNDYv7xQ==",
|
||||
"requires": {
|
||||
"async-kit": "2.2.3",
|
||||
"get-pixels": "3.3.0",
|
||||
"ndarray": "1.0.18",
|
||||
"nextgen-events": "0.10.2",
|
||||
"string-kit": "0.6.3",
|
||||
"string-kit": "0.6.4",
|
||||
"tree-kit": "0.5.26"
|
||||
}
|
||||
},
|
||||
@@ -2054,16 +2037,16 @@
|
||||
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
|
||||
},
|
||||
"tkwidgets": {
|
||||
"version": "0.5.20",
|
||||
"resolved": "https://registry.npmjs.org/tkwidgets/-/tkwidgets-0.5.20.tgz",
|
||||
"integrity": "sha512-9wGsMrrFJvE/6TKUc0dEFFhwxvZLeNsYOxnpy1JCwyk/hYCEF70nuvk7VvJeG4TPaQBaGKPj6c7pCgdREvz4Jw==",
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/tkwidgets/-/tkwidgets-0.5.21.tgz",
|
||||
"integrity": "sha512-gJfpYq3UM6AZ23ZM+D9BZ1PhsJLLHgjCOf487/lS9pO0uDdnkMcVXkkKEfRl00EyjPnGc88QZhEkVOvrtKsuPA==",
|
||||
"requires": {
|
||||
"chalk": "2.3.0",
|
||||
"emphasize": "1.5.0",
|
||||
"node-emoji": "git+https://github.com/laurent22/node-emoji.git#9fa01eac463e94dde1316ef8c53089eeef4973b5",
|
||||
"slice-ansi": "1.0.0",
|
||||
"string-width": "2.1.1",
|
||||
"terminal-kit": "1.14.0",
|
||||
"terminal-kit": "1.14.3",
|
||||
"wrap-ansi": "3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@@ -14,11 +14,12 @@
|
||||
"title": "Joplin CLI",
|
||||
"years": [
|
||||
2016,
|
||||
2017
|
||||
2017,
|
||||
2018
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "0.10.77",
|
||||
"version": "0.10.88",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
@@ -27,9 +28,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"app-module-path": "^2.2.0",
|
||||
"compare-version": "^0.1.2",
|
||||
"follow-redirects": "^1.2.4",
|
||||
"form-data": "^2.1.4",
|
||||
"fs-extra": "^3.0.1",
|
||||
"fs-extra": "^5.0.0",
|
||||
"html-entities": "^1.2.1",
|
||||
"jssha": "^2.3.0",
|
||||
"levenshtein": "^1.0.5",
|
||||
@@ -54,16 +56,13 @@
|
||||
"string-to-stream": "^1.1.0",
|
||||
"strip-ansi": "^4.0.0",
|
||||
"tcp-port-used": "^0.1.2",
|
||||
"tkwidgets": "^0.5.20",
|
||||
"tkwidgets": "^0.5.21",
|
||||
"uuid": "^3.0.1",
|
||||
"word-wrap": "^1.2.3",
|
||||
"yargs-parser": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gettext-parser": "^1.2.2",
|
||||
"jasmine": "^2.6.0",
|
||||
"marked": "^0.3.6",
|
||||
"mustache": "^2.3.0"
|
||||
"jasmine": "^2.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jasmine"
|
||||
|
@@ -9,4 +9,10 @@ bash $SCRIPT_DIR/build.sh
|
||||
cp "$SCRIPT_DIR/package.json" build/
|
||||
cp "$SCRIPT_DIR/../README.md" build/
|
||||
cd "$SCRIPT_DIR/build"
|
||||
npm publish
|
||||
npm publish
|
||||
|
||||
NEW_VERSION=$("cat package.json | jq -r .version")
|
||||
git add -A
|
||||
git commit -m "CLI v$NEW_VERSION"
|
||||
git tag "cli-v$NEW_VERSION"
|
||||
git push && git push --tags
|
@@ -4,4 +4,4 @@ CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
bash "$CLIENT_DIR/build.sh" && node "$CLIENT_DIR/build/main.js" --profile ~/Temp/TestNotes2 --stack-trace-enabled --log-level debug --env dev "$@"
|
||||
|
||||
#bash $CLIENT_DIR/build.sh && NODE_PATH="$CLIENT_DIR/build/" node build/main.js --profile ~/Temp/TestNotes2 --stack-trace-enabled --log-level debug --env dev "$@"
|
||||
# bash $CLIENT_DIR/build.sh && NODE_PATH="$CLIENT_DIR/build/" node build/main.js --profile ~/.config/joplin --stack-trace-enabled --log-level debug "$@"
|
@@ -1,10 +1,17 @@
|
||||
#!/bin/bash
|
||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
BUILD_DIR="$ROOT_DIR/tests-build"
|
||||
TEST_FILE="$1"
|
||||
|
||||
rsync -a --exclude "node_modules/" "$ROOT_DIR/tests/" "$BUILD_DIR/"
|
||||
rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
|
||||
rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/"
|
||||
mkdir -p "$BUILD_DIR/data"
|
||||
|
||||
npm test tests-build/synchronizer.js
|
||||
if [[ $TEST_FILE == "" ]]; then
|
||||
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js)
|
||||
(cd "$ROOT_DIR" && npm test tests-build/encryption.js)
|
||||
(cd "$ROOT_DIR" && npm test tests-build/ArrayUtils.js)
|
||||
else
|
||||
(cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js)
|
||||
fi
|
32
CliClient/tests/ArrayUtils.js
Normal file
@@ -0,0 +1,32 @@
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
||||
const ArrayUtils = require('lib/ArrayUtils.js');
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
describe('Encryption', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should remove array elements', async (done) => {
|
||||
let a = ['un', 'deux', 'trois'];
|
||||
a = ArrayUtils.removeElement(a, 'deux');
|
||||
|
||||
expect(a[0]).toBe('un');
|
||||
expect(a[1]).toBe('trois');
|
||||
expect(a.length).toBe(2);
|
||||
|
||||
a = ['un', 'deux', 'trois'];
|
||||
a = ArrayUtils.removeElement(a, 'not in there');
|
||||
expect(a.length).toBe(3);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
180
CliClient/tests/encryption.js
Normal file
@@ -0,0 +1,180 @@
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const MasterKey = require('lib/models/MasterKey');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
|
||||
const EncryptionService = require('lib/services/EncryptionService.js');
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; // The first test is slow because the database needs to be built
|
||||
|
||||
let service = null;
|
||||
|
||||
describe('Encryption', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
//await setupDatabaseAndSynchronizer(2);
|
||||
//await switchClient(1);
|
||||
service = new EncryptionService();
|
||||
BaseItem.encryptionService_ = service;
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should encode and decode header', async (done) => {
|
||||
const header = {
|
||||
encryptionMethod: EncryptionService.METHOD_SJCL,
|
||||
masterKeyId: '01234568abcdefgh01234568abcdefgh',
|
||||
};
|
||||
|
||||
const encodedHeader = service.encodeHeader_(header);
|
||||
const decodedHeader = service.decodeHeader_(encodedHeader);
|
||||
delete decodedHeader.length;
|
||||
|
||||
expect(objectsEqual(header, decodedHeader)).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should generate and decrypt a master key', async (done) => {
|
||||
const masterKey = await service.generateMasterKey('123456');
|
||||
expect(!!masterKey.checksum).toBe(true);
|
||||
expect(!!masterKey.content).toBe(true);
|
||||
|
||||
let hasThrown = false;
|
||||
try {
|
||||
await service.decryptMasterKey(masterKey, 'wrongpassword');
|
||||
} catch (error) {
|
||||
hasThrown = true;
|
||||
}
|
||||
|
||||
expect(hasThrown).toBe(true);
|
||||
|
||||
const decryptedMasterKey = await service.decryptMasterKey(masterKey, '123456');
|
||||
expect(decryptedMasterKey.length).toBe(512);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should encrypt and decrypt with a master key', async (done) => {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
|
||||
const cipherText = await service.encryptString('some secret');
|
||||
const plainText = await service.decryptString(cipherText);
|
||||
|
||||
expect(plainText).toBe('some secret');
|
||||
|
||||
// Test that a long string, that is going to be split into multiple chunks, encrypt
|
||||
// and decrypt properly too.
|
||||
let veryLongSecret = '';
|
||||
for (let i = 0; i < service.chunkSize() * 3; i++) veryLongSecret += Math.floor(Math.random() * 9);
|
||||
|
||||
const cipherText2 = await service.encryptString(veryLongSecret);
|
||||
const plainText2 = await service.decryptString(cipherText2);
|
||||
|
||||
expect(plainText2 === veryLongSecret).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail to decrypt if master key not present', async (done) => {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
|
||||
const cipherText = await service.encryptString('some secret');
|
||||
|
||||
await service.unloadMasterKey(masterKey);
|
||||
|
||||
let hasThrown = await checkThrowAsync(async () => await service.decryptString(cipherText));
|
||||
|
||||
expect(hasThrown).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it('should fail to decrypt if data tampered with', async (done) => {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
|
||||
let cipherText = await service.encryptString('some secret');
|
||||
cipherText += "ABCDEFGHIJ";
|
||||
|
||||
let hasThrown = await checkThrowAsync(async () => await service.decryptString(cipherText));
|
||||
|
||||
expect(hasThrown).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should encrypt and decrypt notes and folders', async (done) => {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
|
||||
let folder = await Folder.save({ title: 'folder' });
|
||||
let note = await Note.save({ title: 'encrypted note', body: 'something', parent_id: folder.id });
|
||||
let serialized = await Note.serializeForSync(note);
|
||||
let deserialized = Note.filter(await Note.unserialize(serialized));
|
||||
|
||||
// Check that required properties are not encrypted
|
||||
expect(deserialized.id).toBe(note.id);
|
||||
expect(deserialized.parent_id).toBe(note.parent_id);
|
||||
expect(deserialized.updated_time).toBe(note.updated_time);
|
||||
|
||||
// Check that at least title and body are encrypted
|
||||
expect(!deserialized.title).toBe(true);
|
||||
expect(!deserialized.body).toBe(true);
|
||||
|
||||
// Check that encrypted data is there
|
||||
expect(!!deserialized.encryption_cipher_text).toBe(true);
|
||||
|
||||
encryptedNote = await Note.save(deserialized);
|
||||
decryptedNote = await Note.decrypt(encryptedNote);
|
||||
|
||||
expect(decryptedNote.title).toBe(note.title);
|
||||
expect(decryptedNote.body).toBe(note.body);
|
||||
expect(decryptedNote.id).toBe(note.id);
|
||||
expect(decryptedNote.parent_id).toBe(note.parent_id);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should encrypt and decrypt files', async (done) => {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
|
||||
const sourcePath = __dirname + '/../tests/support/photo.jpg';
|
||||
const encryptedPath = __dirname + '/data/photo.crypted';
|
||||
const decryptedPath = __dirname + '/data/photo.jpg';
|
||||
|
||||
await service.encryptFile(sourcePath, encryptedPath);
|
||||
await service.decryptFile(encryptedPath, decryptedPath);
|
||||
|
||||
expect(fileContentEqual(sourcePath, encryptedPath)).toBe(false);
|
||||
expect(fileContentEqual(sourcePath, decryptedPath)).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
BIN
CliClient/tests/support/photo.jpg
Normal file
After Width: | Height: | Size: 2.7 KiB |
@@ -1,21 +1,25 @@
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId } = require('test-utils.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, checkThrowAsync, asyncTest } = require('test-utils.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const fs = require('fs-extra');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const MasterKey = require('lib/models/MasterKey');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 9000; // The first test is slow because the database needs to be built
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; // The first test is slow because the database needs to be built
|
||||
|
||||
async function allItems() {
|
||||
let folders = await Folder.all();
|
||||
@@ -23,6 +27,38 @@ async function allItems() {
|
||||
return folders.concat(notes);
|
||||
}
|
||||
|
||||
async function allSyncTargetItemsEncrypted() {
|
||||
const list = await fileApi().list();
|
||||
const files = list.items;
|
||||
|
||||
//console.info(Setting.value('resourceDir'));
|
||||
|
||||
let totalCount = 0;
|
||||
let encryptedCount = 0;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const remoteContentString = await fileApi().get(file.path);
|
||||
const remoteContent = await BaseItem.unserialize(remoteContentString);
|
||||
const ItemClass = BaseItem.itemClass(remoteContent);
|
||||
|
||||
if (!ItemClass.encryptionSupported()) continue;
|
||||
|
||||
totalCount++;
|
||||
|
||||
if (remoteContent.type_ === BaseModel.TYPE_RESOURCE) {
|
||||
const content = await fileApi().get('.resource/' + remoteContent.id);
|
||||
totalCount++;
|
||||
if (content.substr(0, 5) === 'JED01') output = encryptedCount++;
|
||||
}
|
||||
|
||||
if (!!remoteContent.encryption_applied) encryptedCount++;
|
||||
}
|
||||
|
||||
if (!totalCount) throw new Error('No encryptable item on sync target');
|
||||
|
||||
return totalCount === encryptedCount;
|
||||
}
|
||||
|
||||
async function localItemsSameAsRemote(locals, expect) {
|
||||
try {
|
||||
let files = await fileApi().list();
|
||||
@@ -53,16 +89,22 @@ async function localItemsSameAsRemote(locals, expect) {
|
||||
}
|
||||
}
|
||||
|
||||
let insideBeforeEach = false;
|
||||
|
||||
describe('Synchronizer', function() {
|
||||
|
||||
beforeEach( async (done) => {
|
||||
beforeEach(async (done) => {
|
||||
insideBeforeEach = true;
|
||||
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await setupDatabaseAndSynchronizer(2);
|
||||
await switchClient(1);
|
||||
done();
|
||||
|
||||
insideBeforeEach = false;
|
||||
});
|
||||
|
||||
it('should create remote items', async (done) => {
|
||||
it('should create remote items', asyncTest(async () => {
|
||||
let folder = await Folder.save({ title: "folder1" });
|
||||
await Note.save({ title: "un", parent_id: folder.id });
|
||||
|
||||
@@ -71,11 +113,9 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should update remote item', async (done) => {
|
||||
it('should update remote item', asyncTest(async () => {
|
||||
let folder = await Folder.save({ title: "folder1" });
|
||||
let note = await Note.save({ title: "un", parent_id: folder.id });
|
||||
await synchronizer().start();
|
||||
@@ -86,11 +126,9 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should create local items', async (done) => {
|
||||
it('should create local items', asyncTest(async () => {
|
||||
let folder = await Folder.save({ title: "folder1" });
|
||||
await Note.save({ title: "un", parent_id: folder.id });
|
||||
await synchronizer().start();
|
||||
@@ -100,12 +138,11 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start();
|
||||
|
||||
let all = await allItems();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should update local items', async (done) => {
|
||||
it('should update local items', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@@ -130,11 +167,9 @@ describe('Synchronizer', function() {
|
||||
let all = await allItems();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should resolve note conflicts', async (done) => {
|
||||
it('should resolve note conflicts', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@@ -173,11 +208,9 @@ describe('Synchronizer', function() {
|
||||
if (!noteUpdatedFromRemote.hasOwnProperty(n)) continue;
|
||||
expect(noteUpdatedFromRemote[n]).toBe(note2[n], 'Property: ' + n);
|
||||
}
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should resolve folders conflicts', async (done) => {
|
||||
it('should resolve folders conflicts', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@@ -208,11 +241,9 @@ describe('Synchronizer', function() {
|
||||
|
||||
let folder1_final = await Folder.load(folder1.id);
|
||||
expect(folder1_final.title).toBe(folder1_modRemote.title);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should delete remote notes', async (done) => {
|
||||
it('should delete remote notes', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@@ -235,11 +266,27 @@ describe('Synchronizer', function() {
|
||||
|
||||
let deletedItems = await BaseItem.deletedItems(syncTargetId());
|
||||
expect(deletedItems.length).toBe(0);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
it('should not created deleted_items entries for items deleted via sync', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
|
||||
it('should delete local notes', async (done) => {
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
await Folder.delete(folder1.id);
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(1);
|
||||
|
||||
await synchronizer().start();
|
||||
let deletedItems = await BaseItem.deletedItems(syncTargetId());
|
||||
expect(deletedItems.length).toBe(0);
|
||||
}));
|
||||
|
||||
it('should delete local notes', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@@ -257,11 +304,9 @@ describe('Synchronizer', function() {
|
||||
expect(items.length).toBe(1);
|
||||
let deletedItems = await BaseItem.deletedItems(syncTargetId());
|
||||
expect(deletedItems.length).toBe(0);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should delete remote folder', async (done) => {
|
||||
it('should delete remote folder', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let folder2 = await Folder.save({ title: "folder2" });
|
||||
await synchronizer().start();
|
||||
@@ -278,11 +323,9 @@ describe('Synchronizer', function() {
|
||||
|
||||
let all = await allItems();
|
||||
localItemsSameAsRemote(all, expect);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should delete local folder', async (done) => {
|
||||
it('should delete local folder', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let folder2 = await Folder.save({ title: "folder2" });
|
||||
await synchronizer().start();
|
||||
@@ -303,11 +346,9 @@ describe('Synchronizer', function() {
|
||||
|
||||
let items = await allItems();
|
||||
localItemsSameAsRemote(items, expect);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', async (done) => {
|
||||
it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
await synchronizer().start();
|
||||
|
||||
@@ -325,11 +366,9 @@ describe('Synchronizer', function() {
|
||||
expect(items.length).toBe(1);
|
||||
expect(items[0].title).toBe('note1');
|
||||
expect(items[0].is_conflict).toBe(1);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should resolve conflict if note has been deleted remotely and locally', async (done) => {
|
||||
it('should resolve conflict if note has been deleted remotely and locally', asyncTest(async () => {
|
||||
let folder = await Folder.save({ title: "folder" });
|
||||
let note = await Note.save({ title: "note", parent_id: folder.title });
|
||||
await synchronizer().start();
|
||||
@@ -350,11 +389,9 @@ describe('Synchronizer', function() {
|
||||
expect(items[0].title).toBe('folder');
|
||||
|
||||
localItemsSameAsRemote(items, expect);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should cross delete all folders', async (done) => {
|
||||
it('should cross delete all folders', asyncTest(async () => {
|
||||
// If client1 and 2 have two folders, client 1 deletes item 1 and client
|
||||
// 2 deletes item 2, they should both end up with no items after sync.
|
||||
|
||||
@@ -390,11 +427,9 @@ describe('Synchronizer', function() {
|
||||
|
||||
expect(items1.length).toBe(0);
|
||||
expect(items1.length).toBe(items2.length);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should handle conflict when remote note is deleted then local note is modified', async (done) => {
|
||||
it('should handle conflict when remote note is deleted then local note is modified', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@@ -424,11 +459,9 @@ describe('Synchronizer', function() {
|
||||
let unconflictedNotes = await Note.unconflictedNotes();
|
||||
|
||||
expect(unconflictedNotes.length).toBe(0);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should handle conflict when remote folder is deleted then local folder is renamed', async (done) => {
|
||||
it('should handle conflict when remote folder is deleted then local folder is renamed', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let folder2 = await Folder.save({ title: "folder2" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
@@ -456,11 +489,9 @@ describe('Synchronizer', function() {
|
||||
let items = await allItems();
|
||||
|
||||
expect(items.length).toBe(1);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should allow duplicate folder titles', async (done) => {
|
||||
it('should allow duplicate folder titles', asyncTest(async () => {
|
||||
let localF1 = await Folder.save({ title: "folder" });
|
||||
|
||||
await switchClient(2);
|
||||
@@ -492,11 +523,15 @@ describe('Synchronizer', function() {
|
||||
remoteF2 = await Folder.load(remoteF2.id);
|
||||
|
||||
expect(remoteF2.title == localF2.title).toBe(true);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
async function shoudSyncTagTest(withEncryption) {
|
||||
let masterKey = null;
|
||||
if (withEncryption) {
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
masterKey = await loadEncryptionMasterKey();
|
||||
}
|
||||
|
||||
it('should sync tags', async (done) => {
|
||||
let f1 = await Folder.save({ title: "folder" });
|
||||
let n1 = await Note.save({ title: "mynote" });
|
||||
let n2 = await Note.save({ title: "mynote2" });
|
||||
@@ -506,6 +541,12 @@ describe('Synchronizer', function() {
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
if (withEncryption) {
|
||||
const masterKey_2 = await MasterKey.load(masterKey.id);
|
||||
await encryptionService().loadMasterKey(masterKey_2, '123456', true);
|
||||
let t = await Tag.load(tag.id);
|
||||
await Tag.decrypt(t);
|
||||
}
|
||||
let remoteTag = await Tag.loadByTitle(tag.title);
|
||||
expect(!!remoteTag).toBe(true);
|
||||
expect(remoteTag.id).toBe(tag.id);
|
||||
@@ -531,11 +572,15 @@ describe('Synchronizer', function() {
|
||||
noteIds = await Tag.noteIds(tag.id);
|
||||
expect(noteIds.length).toBe(1);
|
||||
expect(remoteNoteIds[0]).toBe(noteIds[0]);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
it('should sync tags', asyncTest(async () => {
|
||||
await shoudSyncTagTest(false); }));
|
||||
|
||||
it('should not sync notes with conflicts', async (done) => {
|
||||
it('should sync encrypted tags', asyncTest(async () => {
|
||||
await shoudSyncTagTest(true); }));
|
||||
|
||||
it('should not sync notes with conflicts', asyncTest(async () => {
|
||||
let f1 = await Folder.save({ title: "folder" });
|
||||
let n1 = await Note.save({ title: "mynote", parent_id: f1.id, is_conflict: 1 });
|
||||
await synchronizer().start();
|
||||
@@ -547,11 +592,9 @@ describe('Synchronizer', function() {
|
||||
let folders = await Folder.all()
|
||||
expect(notes.length).toBe(0);
|
||||
expect(folders.length).toBe(1);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not try to delete on remote conflicted notes that have been deleted', async (done) => {
|
||||
it('should not try to delete on remote conflicted notes that have been deleted', asyncTest(async () => {
|
||||
let f1 = await Folder.save({ title: "folder" });
|
||||
let n1 = await Note.save({ title: "mynote", parent_id: f1.id });
|
||||
await synchronizer().start();
|
||||
@@ -564,17 +607,13 @@ describe('Synchronizer', function() {
|
||||
const deletedItems = await BaseItem.deletedItems(syncTargetId());
|
||||
|
||||
expect(deletedItems.length).toBe(0);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not consider it is a conflict if neither the title nor body of the note have changed', async (done) => {
|
||||
// That was previously a common conflict:
|
||||
// - Client 1 mark todo as "done", and sync
|
||||
// - Client 2 doesn't sync, mark todo as "done" todo. Then sync.
|
||||
// In theory it is a conflict because the todo_completed dates are different
|
||||
// but in practice it doesn't matter, we can just take the date when the
|
||||
// todo was marked as "done" the first time.
|
||||
}));
|
||||
|
||||
async function ignorableNoteConflictTest(withEncryption) {
|
||||
if (withEncryption) {
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
await loadEncryptionMasterKey();
|
||||
}
|
||||
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
||||
@@ -583,6 +622,10 @@ describe('Synchronizer', function() {
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
if (withEncryption) {
|
||||
await loadEncryptionMasterKey(null, true);
|
||||
await decryptionWorker().start();
|
||||
}
|
||||
let note2 = await Note.load(note1.id);
|
||||
note2.todo_completed = time.unixMs()-1;
|
||||
await Note.save(note2);
|
||||
@@ -597,18 +640,43 @@ describe('Synchronizer', function() {
|
||||
note2conf = await Note.load(note1.id);
|
||||
await synchronizer().start();
|
||||
|
||||
let conflictedNotes = await Note.conflictedNotes();
|
||||
expect(conflictedNotes.length).toBe(0);
|
||||
if (!withEncryption) {
|
||||
// That was previously a common conflict:
|
||||
// - Client 1 mark todo as "done", and sync
|
||||
// - Client 2 doesn't sync, mark todo as "done" todo. Then sync.
|
||||
// In theory it is a conflict because the todo_completed dates are different
|
||||
// but in practice it doesn't matter, we can just take the date when the
|
||||
// todo was marked as "done" the first time.
|
||||
|
||||
let notes = await Note.all();
|
||||
expect(notes.length).toBe(1);
|
||||
expect(notes[0].id).toBe(note1.id);
|
||||
expect(notes[0].todo_completed).toBe(note2.todo_completed);
|
||||
let conflictedNotes = await Note.conflictedNotes();
|
||||
expect(conflictedNotes.length).toBe(0);
|
||||
|
||||
done();
|
||||
});
|
||||
let notes = await Note.all();
|
||||
expect(notes.length).toBe(1);
|
||||
expect(notes[0].id).toBe(note1.id);
|
||||
expect(notes[0].todo_completed).toBe(note2.todo_completed);
|
||||
} else {
|
||||
// If the notes are encrypted however it's not possible to do this kind of
|
||||
// smart conflict resolving since we don't know the content, so in that
|
||||
// case it's handled as a regular conflict.
|
||||
|
||||
it('items should be downloaded again when user cancels in the middle of delta operation', async (done) => {
|
||||
let conflictedNotes = await Note.conflictedNotes();
|
||||
expect(conflictedNotes.length).toBe(1);
|
||||
|
||||
let notes = await Note.all();
|
||||
expect(notes.length).toBe(2);
|
||||
}
|
||||
}
|
||||
|
||||
it('should not consider it is a conflict if neither the title nor body of the note have changed', asyncTest(async () => {
|
||||
await ignorableNoteConflictTest(false);
|
||||
}));
|
||||
|
||||
it('should always handle conflict if local or remote are encrypted', asyncTest(async () => {
|
||||
await ignorableNoteConflictTest(true);
|
||||
}));
|
||||
|
||||
it('items should be downloaded again when user cancels in the middle of delta operation', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@@ -624,8 +692,295 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start({ context: context });
|
||||
notes = await Note.all();
|
||||
expect(notes.length).toBe(1);
|
||||
}));
|
||||
|
||||
it('should skip items that cannot be synced', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
||||
const noteId = note1.id;
|
||||
await synchronizer().start();
|
||||
let disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
|
||||
expect(disabledItems.length).toBe(0);
|
||||
await Note.save({ id: noteId, title: "un mod", });
|
||||
synchronizer().debugFlags_ = ['rejectedByTarget'];
|
||||
await synchronizer().start();
|
||||
synchronizer().debugFlags_ = [];
|
||||
await synchronizer().start(); // Another sync to check that this item is now excluded from sync
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
let notes = await Note.all();
|
||||
expect(notes.length).toBe(1);
|
||||
expect(notes[0].title).toBe('un');
|
||||
|
||||
await switchClient(1);
|
||||
|
||||
disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
|
||||
expect(disabledItems.length).toBe(1);
|
||||
}));
|
||||
|
||||
it('notes and folders should get encrypted when encryption is enabled', asyncTest(async () => {
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
const masterKey = await loadEncryptionMasterKey();
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", body: 'to be encrypted', parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
// After synchronisation, remote items should be encrypted but local ones remain plain text
|
||||
note1 = await Note.load(note1.id);
|
||||
expect(note1.title).toBe('un');
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
let folder1_2 = await Folder.load(folder1.id);
|
||||
let note1_2 = await Note.load(note1.id);
|
||||
let masterKey_2 = await MasterKey.load(masterKey.id);
|
||||
// On this side however it should be received encrypted
|
||||
expect(!note1_2.title).toBe(true);
|
||||
expect(!folder1_2.title).toBe(true);
|
||||
expect(!!note1_2.encryption_cipher_text).toBe(true);
|
||||
expect(!!folder1_2.encryption_cipher_text).toBe(true);
|
||||
// Master key is already encrypted so it does not get re-encrypted during sync
|
||||
expect(masterKey_2.content).toBe(masterKey.content);
|
||||
expect(masterKey_2.checksum).toBe(masterKey.checksum);
|
||||
// Now load the master key we got from client 1 and try to decrypt
|
||||
await encryptionService().loadMasterKey(masterKey_2, '123456', true);
|
||||
// Get the decrypted items back
|
||||
await Folder.decrypt(folder1_2);
|
||||
await Note.decrypt(note1_2);
|
||||
folder1_2 = await Folder.load(folder1.id);
|
||||
note1_2 = await Note.load(note1.id);
|
||||
// Check that properties match the original items. Also check
|
||||
// the encryption did not affect the updated_time timestamp.
|
||||
expect(note1_2.title).toBe(note1.title);
|
||||
expect(note1_2.body).toBe(note1.body);
|
||||
expect(note1_2.updated_time).toBe(note1.updated_time);
|
||||
expect(!note1_2.encryption_cipher_text).toBe(true);
|
||||
expect(folder1_2.title).toBe(folder1.title);
|
||||
expect(folder1_2.updated_time).toBe(folder1.updated_time);
|
||||
expect(!folder1_2.encryption_cipher_text).toBe(true);
|
||||
}));
|
||||
|
||||
it('should enable encryption automatically when downloading new master key (and none was previously available)',asyncTest(async () => {
|
||||
// Enable encryption on client 1 and sync an item
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
await loadEncryptionMasterKey();
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
// Synchronising should enable encryption since we're going to get a master key
|
||||
expect(Setting.value('encryption.enabled')).toBe(false);
|
||||
await synchronizer().start();
|
||||
expect(Setting.value('encryption.enabled')).toBe(true);
|
||||
|
||||
// Check that we got the master key from client 1
|
||||
const masterKey = (await MasterKey.all())[0];
|
||||
expect(!!masterKey).toBe(true);
|
||||
|
||||
// Since client 2 hasn't supplied a password yet, no master key is currently loaded
|
||||
expect(encryptionService().loadedMasterKeyIds().length).toBe(0);
|
||||
|
||||
// If we sync now, nothing should be sent to target since we don't have a password.
|
||||
// Technically it's incorrect to set the property of an encrypted variable but it allows confirming
|
||||
// that encryption doesn't work if user hasn't supplied a password.
|
||||
await BaseItem.forceSync(folder1.id);
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(1);
|
||||
|
||||
await synchronizer().start();
|
||||
folder1 = await Folder.load(folder1.id);
|
||||
expect(folder1.title).toBe('folder1'); // Still at old value
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
// Now client 2 set the master key password
|
||||
Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
|
||||
await encryptionService().loadMasterKeysFromSettings();
|
||||
|
||||
// Now that master key should be loaded
|
||||
expect(encryptionService().loadedMasterKeyIds()[0]).toBe(masterKey.id);
|
||||
|
||||
// Decrypt all the data. Now change the title and sync again - this time the changes should be transmitted
|
||||
await decryptionWorker().start();
|
||||
folder1_2 = await Folder.save({ id: folder1.id, title: "change test" });
|
||||
|
||||
// If we sync now, this time client 1 should get the changes we did earlier
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(1);
|
||||
|
||||
await synchronizer().start();
|
||||
// Decrypt the data we just got
|
||||
await decryptionWorker().start();
|
||||
folder1 = await Folder.load(folder1.id);
|
||||
expect(folder1.title).toBe('change test'); // Got title from client 2
|
||||
}));
|
||||
|
||||
it('should encrypt existing notes too when enabling E2EE', asyncTest(async () => {
|
||||
// First create a folder, without encryption enabled, and sync it
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
await synchronizer().start();
|
||||
let files = await fileApi().list()
|
||||
let content = await fileApi().get(files.items[0].path);
|
||||
expect(content.indexOf('folder1') >= 0).toBe(true)
|
||||
|
||||
// Then enable encryption and sync again
|
||||
let masterKey = await encryptionService().generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
await encryptionService().enableEncryption(masterKey, '123456');
|
||||
await encryptionService().loadMasterKeysFromSettings();
|
||||
await synchronizer().start();
|
||||
|
||||
// Even though the folder has not been changed it should have been synced again so that
|
||||
// an encrypted version of it replaces the decrypted version.
|
||||
files = await fileApi().list()
|
||||
expect(files.items.length).toBe(2);
|
||||
// By checking that the folder title is not present, we can confirm that the item has indeed been encrypted
|
||||
// One of the two items is the master key
|
||||
content = await fileApi().get(files.items[0].path);
|
||||
expect(content.indexOf('folder1') < 0).toBe(true);
|
||||
content = await fileApi().get(files.items[1].path);
|
||||
expect(content.indexOf('folder1') < 0).toBe(true);
|
||||
}));
|
||||
|
||||
it('should sync resources', asyncTest(async () => {
|
||||
while (insideBeforeEach) await time.msleep(100);
|
||||
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
|
||||
let resource1 = (await Resource.all())[0];
|
||||
let resourcePath1 = Resource.fullPath(resource1);
|
||||
await synchronizer().start();
|
||||
expect((await fileApi().list()).items.length).toBe(3);
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
let allResources = await Resource.all();
|
||||
expect(allResources.length).toBe(1);
|
||||
let resource1_2 = allResources[0];
|
||||
let resourcePath1_2 = Resource.fullPath(resource1_2);
|
||||
|
||||
expect(resource1_2.id).toBe(resource1.id);
|
||||
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
|
||||
}));
|
||||
|
||||
it('should encryt resources', asyncTest(async () => {
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
const masterKey = await loadEncryptionMasterKey();
|
||||
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
|
||||
let resource1 = (await Resource.all())[0];
|
||||
let resourcePath1 = Resource.fullPath(resource1);
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
|
||||
await encryptionService().loadMasterKeysFromSettings();
|
||||
|
||||
let resource1_2 = (await Resource.all())[0];
|
||||
resource1_2 = await Resource.decrypt(resource1_2);
|
||||
let resourcePath1_2 = Resource.fullPath(resource1_2);
|
||||
|
||||
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
|
||||
}));
|
||||
|
||||
it('should upload decrypted items to sync target after encryption disabled', asyncTest(async () => {
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
const masterKey = await loadEncryptionMasterKey();
|
||||
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
await synchronizer().start();
|
||||
|
||||
let allEncrypted = await allSyncTargetItemsEncrypted();
|
||||
expect(allEncrypted).toBe(true);
|
||||
|
||||
await encryptionService().disableEncryption();
|
||||
|
||||
await synchronizer().start();
|
||||
allEncrypted = await allSyncTargetItemsEncrypted();
|
||||
expect(allEncrypted).toBe(false);
|
||||
}));
|
||||
|
||||
it('should not upload any item if encryption was enabled, and items have not been decrypted, and then encryption disabled', asyncTest(async () => {
|
||||
// For some reason I can't explain, this test is sometimes executed before beforeEach is finished
|
||||
// which means it's going to fail in unexpected way. So the loop below wait for beforeEach to be done.
|
||||
while (insideBeforeEach) await time.msleep(100);
|
||||
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
const masterKey = await loadEncryptionMasterKey();
|
||||
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
expect(Setting.value('encryption.enabled')).toBe(true);
|
||||
|
||||
// If we try to disable encryption now, it should throw an error because some items are
|
||||
// currently encrypted. They must be decrypted first so that they can be sent as
|
||||
// plain text to the sync target.
|
||||
//let hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption());
|
||||
//expect(hasThrown).toBe(true);
|
||||
|
||||
// Now supply the password, and decrypt the items
|
||||
Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
|
||||
await encryptionService().loadMasterKeysFromSettings();
|
||||
await decryptionWorker().start();
|
||||
|
||||
// Try to disable encryption again
|
||||
hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption());
|
||||
expect(hasThrown).toBe(false);
|
||||
|
||||
// If we sync now the target should receive the decrypted items
|
||||
await synchronizer().start();
|
||||
allEncrypted = await allSyncTargetItemsEncrypted();
|
||||
expect(allEncrypted).toBe(false);
|
||||
}));
|
||||
|
||||
it('should encrypt remote resources after encryption has been enabled', asyncTest(async () => {
|
||||
while (insideBeforeEach) await time.msleep(100);
|
||||
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
|
||||
let resource1 = (await Resource.all())[0];
|
||||
await synchronizer().start();
|
||||
|
||||
expect(await allSyncTargetItemsEncrypted()).toBe(false);
|
||||
|
||||
const masterKey = await loadEncryptionMasterKey();
|
||||
await encryptionService().enableEncryption(masterKey, '123456');
|
||||
await encryptionService().loadMasterKeysFromSettings();
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
expect(await allSyncTargetItemsEncrypted()).toBe(true);
|
||||
}));
|
||||
|
||||
it('should upload encrypted resource, but it should not mark the blob as encrypted locally', asyncTest(async () => {
|
||||
while (insideBeforeEach) await time.msleep(100);
|
||||
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
|
||||
const masterKey = await loadEncryptionMasterKey();
|
||||
await encryptionService().enableEncryption(masterKey, '123456');
|
||||
await encryptionService().loadMasterKeysFromSettings();
|
||||
await synchronizer().start();
|
||||
|
||||
let resource1 = (await Resource.all())[0];
|
||||
expect(resource1.encryption_blob_encrypted).toBe(0);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
@@ -1,34 +1,44 @@
|
||||
const fs = require('fs-extra');
|
||||
const { JoplinDatabase } = require('lib/joplin-database.js');
|
||||
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Resource } = require('lib/models/resource.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const { NoteTag } = require('lib/models/note-tag.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const NoteTag = require('lib/models/NoteTag.js');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const MasterKey = require('lib/models/MasterKey');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const { FileApi } = require('lib/file-api.js');
|
||||
const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js');
|
||||
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
|
||||
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { shimInit } = require('lib/shim-init-node.js');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
|
||||
const SyncTargetMemory = require('lib/SyncTargetMemory.js');
|
||||
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
||||
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
||||
const EncryptionService = require('lib/services/EncryptionService.js');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
|
||||
|
||||
let databases_ = [];
|
||||
let synchronizers_ = [];
|
||||
let encryptionServices_ = [];
|
||||
let decryptionWorkers_ = [];
|
||||
let fileApi_ = null;
|
||||
let currentClient_ = 1;
|
||||
|
||||
shimInit();
|
||||
|
||||
const fsDriver = new FsDriverNode();
|
||||
Logger.fsDriver_ = fsDriver;
|
||||
Resource.fsDriver_ = fsDriver;
|
||||
EncryptionService.fsDriver_ = fsDriver;
|
||||
FileApiDriverLocal.fsDriver_ = fsDriver;
|
||||
|
||||
const logDir = __dirname + '/../tests/logs';
|
||||
fs.mkdirpSync(logDir, 0o755);
|
||||
@@ -37,20 +47,23 @@ SyncTargetRegistry.addClass(SyncTargetMemory);
|
||||
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||
|
||||
const syncTargetId_ = SyncTargetRegistry.nameToId('memory');
|
||||
//const syncTargetId_ = SyncTargetRegistry.nameToId('memory');
|
||||
const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem');
|
||||
const syncDir = __dirname + '/../tests/sync';
|
||||
|
||||
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 400;
|
||||
|
||||
const logger = new Logger();
|
||||
logger.addTarget('console');
|
||||
logger.addTarget('file', { path: logDir + '/log.txt' });
|
||||
logger.setLevel(Logger.LEVEL_DEBUG);
|
||||
logger.setLevel(Logger.LEVEL_WARN); // Set to INFO to display sync process in console
|
||||
|
||||
BaseItem.loadClass('Note', Note);
|
||||
BaseItem.loadClass('Folder', Folder);
|
||||
BaseItem.loadClass('Resource', Resource);
|
||||
BaseItem.loadClass('Tag', Tag);
|
||||
BaseItem.loadClass('NoteTag', NoteTag);
|
||||
BaseItem.loadClass('MasterKey', MasterKey);
|
||||
|
||||
Setting.setConstant('appId', 'net.cozic.joplin-cli');
|
||||
Setting.setConstant('appType', 'cli');
|
||||
@@ -78,10 +91,15 @@ async function switchClient(id) {
|
||||
BaseItem.db_ = databases_[id];
|
||||
Setting.db_ = databases_[id];
|
||||
|
||||
BaseItem.encryptionService_ = encryptionServices_[id];
|
||||
Resource.encryptionService_ = encryptionServices_[id];
|
||||
|
||||
Setting.setConstant('resourceDir', resourceDir(id));
|
||||
|
||||
return Setting.load();
|
||||
}
|
||||
|
||||
function clearDatabase(id = null) {
|
||||
async function clearDatabase(id = null) {
|
||||
if (id === null) id = currentClient_;
|
||||
|
||||
let queries = [
|
||||
@@ -90,35 +108,46 @@ function clearDatabase(id = null) {
|
||||
'DELETE FROM resources',
|
||||
'DELETE FROM tags',
|
||||
'DELETE FROM note_tags',
|
||||
'DELETE FROM master_keys',
|
||||
'DELETE FROM settings',
|
||||
|
||||
'DELETE FROM deleted_items',
|
||||
'DELETE FROM sync_items',
|
||||
];
|
||||
|
||||
return databases_[id].transactionExecBatch(queries);
|
||||
await databases_[id].transactionExecBatch(queries);
|
||||
}
|
||||
|
||||
function setupDatabase(id = null) {
|
||||
async function setupDatabase(id = null) {
|
||||
if (id === null) id = currentClient_;
|
||||
|
||||
Setting.cancelScheduleSave();
|
||||
Setting.cache_ = null;
|
||||
|
||||
if (databases_[id]) {
|
||||
return clearDatabase(id).then(() => {
|
||||
return Setting.load();
|
||||
});
|
||||
await clearDatabase(id);
|
||||
await Setting.load();
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = __dirname + '/data/test-' + id + '.sqlite';
|
||||
return fs.unlink(filePath).catch(() => {
|
||||
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (error) {
|
||||
// Don't care if the file doesn't exist
|
||||
}).then(() => {
|
||||
databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
|
||||
// databases_[id].setLogger(logger);
|
||||
// console.info(filePath);
|
||||
return databases_[id].open({ name: filePath }).then(() => {
|
||||
BaseModel.db_ = databases_[id];
|
||||
return setupDatabase(id);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
|
||||
await databases_[id].open({ name: filePath });
|
||||
|
||||
BaseModel.db_ = databases_[id];
|
||||
await Setting.load();
|
||||
}
|
||||
|
||||
function resourceDir(id = null) {
|
||||
if (id === null) id = currentClient_;
|
||||
return __dirname + '/data/resources-' + id;
|
||||
}
|
||||
|
||||
async function setupDatabaseAndSynchronizer(id = null) {
|
||||
@@ -126,14 +155,25 @@ async function setupDatabaseAndSynchronizer(id = null) {
|
||||
|
||||
await setupDatabase(id);
|
||||
|
||||
EncryptionService.instance_ = null;
|
||||
DecryptionWorker.instance_ = null;
|
||||
|
||||
await fs.remove(resourceDir(id));
|
||||
await fs.mkdirp(resourceDir(id), 0o755);
|
||||
|
||||
if (!synchronizers_[id]) {
|
||||
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_);
|
||||
const syncTarget = new SyncTargetClass(db(id));
|
||||
syncTarget.setFileApi(fileApi());
|
||||
syncTarget.setLogger(logger);
|
||||
synchronizers_[id] = await syncTarget.synchronizer();
|
||||
synchronizers_[id].autoStartDecryptionWorker_ = false; // For testing we disable this since it would make the tests non-deterministic
|
||||
}
|
||||
|
||||
encryptionServices_[id] = new EncryptionService();
|
||||
decryptionWorkers_[id] = new DecryptionWorker();
|
||||
decryptionWorkers_[id].setEncryptionService(encryptionServices_[id]);
|
||||
|
||||
if (syncTargetId_ == SyncTargetRegistry.nameToId('filesystem')) {
|
||||
fs.removeSync(syncDir)
|
||||
fs.mkdirpSync(syncDir, 0o755);
|
||||
@@ -152,6 +192,35 @@ function synchronizer(id = null) {
|
||||
return synchronizers_[id];
|
||||
}
|
||||
|
||||
function encryptionService(id = null) {
|
||||
if (id === null) id = currentClient_;
|
||||
return encryptionServices_[id];
|
||||
}
|
||||
|
||||
function decryptionWorker(id = null) {
|
||||
if (id === null) id = currentClient_;
|
||||
return decryptionWorkers_[id];
|
||||
}
|
||||
|
||||
async function loadEncryptionMasterKey(id = null, useExisting = false) {
|
||||
const service = encryptionService(id);
|
||||
|
||||
let masterKey = null;
|
||||
|
||||
if (!useExisting) { // Create it
|
||||
masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
} else { // Use the one already available
|
||||
materKey = await MasterKey.all();
|
||||
if (!materKey.length) throw new Error('No mater key available');
|
||||
masterKey = materKey[0];
|
||||
}
|
||||
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
|
||||
return masterKey;
|
||||
}
|
||||
|
||||
function fileApi() {
|
||||
if (fileApi_) return fileApi_;
|
||||
|
||||
@@ -184,4 +253,43 @@ function fileApi() {
|
||||
return fileApi_;
|
||||
}
|
||||
|
||||
module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId };
|
||||
function objectsEqual(o1, o2) {
|
||||
if (Object.getOwnPropertyNames(o1).length !== Object.getOwnPropertyNames(o2).length) return false;
|
||||
for (let n in o1) {
|
||||
if (!o1.hasOwnProperty(n)) continue;
|
||||
if (o1[n] !== o2[n]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function checkThrowAsync(asyncFn) {
|
||||
let hasThrown = false;
|
||||
try {
|
||||
await asyncFn();
|
||||
} catch (error) {
|
||||
hasThrown = true;
|
||||
}
|
||||
return hasThrown;
|
||||
}
|
||||
|
||||
function fileContentEqual(path1, path2) {
|
||||
const fs = require('fs-extra');
|
||||
const content1 = fs.readFileSync(path1, 'base64');
|
||||
const content2 = fs.readFileSync(path2, 'base64');
|
||||
return content1 === content2;
|
||||
}
|
||||
|
||||
// Wrap an async test in a try/catch block so that done() is always called
|
||||
// and display a proper error message instead of "unhandled promise error"
|
||||
function asyncTest(callback) {
|
||||
return async function(done) {
|
||||
try {
|
||||
await callback();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest };
|
@@ -19,7 +19,8 @@
|
||||
"title": "Demo for Joplin CLI",
|
||||
"years": [
|
||||
2016,
|
||||
2017
|
||||
2017,
|
||||
2018
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
|