You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-30 20:39:46 +02:00
Compare commits
26 Commits
v0.10.45
...
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 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -35,4 +35,6 @@ _vieux/
|
||||
_mydocs
|
||||
.DS_Store
|
||||
Assets/DownloadBadges*.psd
|
||||
node_modules
|
||||
node_modules
|
||||
Tools/github_oauth_token.txt
|
||||
_releases
|
@@ -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() {
|
||||
@@ -223,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_);
|
||||
}
|
||||
@@ -242,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_);
|
||||
|
@@ -81,7 +81,9 @@ class Command extends BaseCommand {
|
||||
const termState = app().gui().termSaveState();
|
||||
|
||||
const spawnSync = require('child_process').spawnSync;
|
||||
spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
|
||||
const result = spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
|
||||
|
||||
if (result.error) this.stdout(_('Error opening note in editor: %s', result.error.message));
|
||||
|
||||
app().gui().termRestoreState(termState);
|
||||
app().gui().hideModalOverlay();
|
||||
|
@@ -23,12 +23,14 @@ 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.
|
||||
|
@@ -770,7 +770,7 @@ msgid "Rename notebook:"
|
||||
msgstr "Renommer le carnet :"
|
||||
|
||||
msgid "Set alarm:"
|
||||
msgstr "Définir ou modifier alarme :"
|
||||
msgstr "Régler alarme :"
|
||||
|
||||
msgid "Layout"
|
||||
msgstr "Disposition"
|
||||
@@ -817,7 +817,7 @@ msgid "Attach file"
|
||||
msgstr "Attacher un fichier"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "Définir ou modifier alarme"
|
||||
msgstr "Régler alarme"
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Rafraîchir"
|
||||
@@ -940,9 +940,9 @@ msgstr "Objets supprimés localement : %d."
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr "Objets distants supprimés : %d."
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr "Objets créés localement : %d."
|
||||
msgstr "Téléchargés : %d/%d."
|
||||
|
||||
#, javascript-format
|
||||
msgid "State: \"%s\"."
|
||||
@@ -1210,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"
|
||||
|
1238
CliClient/locales/nl_BE.po
Normal file
1238
CliClient/locales/nl_BE.po
Normal file
File diff suppressed because it is too large
Load Diff
40
CliClient/package-lock.json
generated
40
CliClient/package-lock.json
generated
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "0.10.87",
|
||||
"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",
|
||||
@@ -442,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",
|
||||
@@ -494,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"
|
||||
}
|
||||
},
|
||||
@@ -953,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",
|
||||
@@ -1920,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"
|
||||
}
|
||||
@@ -2019,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"
|
||||
}
|
||||
},
|
||||
@@ -2037,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": {
|
||||
|
@@ -19,7 +19,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "0.10.87",
|
||||
"version": "0.10.88",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
@@ -56,7 +56,7 @@
|
||||
"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"
|
||||
|
@@ -268,6 +268,24 @@ describe('Synchronizer', function() {
|
||||
expect(deletedItems.length).toBe(0);
|
||||
}));
|
||||
|
||||
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();
|
||||
|
||||
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 });
|
||||
|
@@ -38,6 +38,7 @@ 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);
|
||||
@@ -142,25 +143,6 @@ async function setupDatabase(id = null) {
|
||||
|
||||
BaseModel.db_ = databases_[id];
|
||||
await Setting.load();
|
||||
//return setupDatabase(id);
|
||||
|
||||
|
||||
|
||||
// return databases_[id].open({ name: filePath }).then(() => {
|
||||
// BaseModel.db_ = databases_[id];
|
||||
// return setupDatabase(id);
|
||||
// });
|
||||
|
||||
|
||||
// return fs.unlink(filePath).catch(() => {
|
||||
// // Don't care if the file doesn't exist
|
||||
// }).then(() => {
|
||||
// databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
|
||||
// return databases_[id].open({ name: filePath }).then(() => {
|
||||
// BaseModel.db_ = databases_[id];
|
||||
// return setupDatabase(id);
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
function resourceDir(id = null) {
|
||||
|
@@ -17,6 +17,11 @@
|
||||
.smalltalk .page {
|
||||
max-width: 30em;
|
||||
}
|
||||
.ace_editor * {
|
||||
/* Necessary to make sure Russian text is displayed properly */
|
||||
/* https://github.com/laurent22/joplin/issues/155 */
|
||||
font-family: monospace !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
File diff suppressed because one or more lines are too long
@@ -7,6 +7,7 @@ locales['fr_FR'] = require('./fr_FR.json');
|
||||
locales['hr_HR'] = require('./hr_HR.json');
|
||||
locales['it_IT'] = require('./it_IT.json');
|
||||
locales['ja_JP'] = require('./ja_JP.json');
|
||||
locales['nl_BE'] = require('./nl_BE.json');
|
||||
locales['pt_BR'] = require('./pt_BR.json');
|
||||
locales['ru_RU'] = require('./ru_RU.json');
|
||||
locales['zh_CN'] = require('./zh_CN.json');
|
||||
|
1
ElectronClient/app/locales/nl_BE.json
Normal file
1
ElectronClient/app/locales/nl_BE.json
Normal file
File diff suppressed because one or more lines are too long
@@ -17,11 +17,13 @@ const { FsDriverNode } = require('lib/fs-driver-node.js');
|
||||
const { shimInit } = require('lib/shim-init-node.js');
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
|
||||
|
||||
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.
|
||||
|
2
ElectronClient/app/package-lock.json
generated
2
ElectronClient/app/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "0.10.45",
|
||||
"version": "0.10.47",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "0.10.45",
|
||||
"version": "0.10.47",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
18
README.md
18
README.md
@@ -24,10 +24,10 @@ Linux | <a href='https://github.com/laurent22/joplin/releases/download/
|
||||
|
||||
## Mobile applications
|
||||
|
||||
Operating System | Download
|
||||
-----------------|--------
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a>
|
||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeIOS.png'/></a>
|
||||
Operating System | Download | Alt. Download
|
||||
-----------------|---------------------------
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin/releases/download/android-v0.10.78/joplin-v0.10.78.apk)
|
||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeIOS.png'/></a> | -
|
||||
|
||||
## Terminal application
|
||||
|
||||
@@ -95,6 +95,12 @@ On the **terminal application**, to initiate the synchronisation process, type `
|
||||
|
||||
*/30 * * * * /path/to/joplin sync
|
||||
|
||||
# Encryption
|
||||
|
||||
Joplin supports end-to-end encryption (E2EE) on all the applications. E2EE is a system where only the owner of the notes, notebooks, tags or resources can read them. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developers of Joplin from being able to access the data. Please see the [End-To-End Encryption Tutorial](http://joplin.cozic.net/help/e2ee) for more information about this feature and how to enable it.
|
||||
|
||||
For a more technical description, mostly relevant for development or to review the method being used, please see the [Encryption specification](http://joplin.cozic.net/help/spec).
|
||||
|
||||
# Attachments / Resources
|
||||
|
||||
Any kind of file can be attached to a note. In Markdown, links to these files are represented as a simple ID to the resource. In the note viewer, these files, if they are images, will be displayed or, if they are other files (PDF, text files, etc.) they will be displayed as links. Clicking on this link will open the file in the default application.
|
||||
@@ -107,7 +113,7 @@ On the desktop and mobile apps, an alarm can be associated with any to-do. It wi
|
||||
- **macOS**: >= 10.8 or Growl if earlier.
|
||||
- **Linux**: `notify-osd` or `libnotify-bin` installed (Ubuntu should have this by default). Growl otherwise
|
||||
|
||||
See [documentation and flow chart for reporter choice](./DECISION_FLOW.md)
|
||||
See [documentation and flow chart for reporter choice](https://github.com/mikaelbr/node-notifier/blob/master/DECISION_FLOW.md)
|
||||
|
||||
On mobile, the alarms will be displayed using the built-in notification system.
|
||||
|
||||
@@ -115,7 +121,7 @@ If for any reason the notifications do not work, please [open an issue](https://
|
||||
|
||||
# Localisation
|
||||
|
||||
Joplin is currently available in English, French, Spanish, German, Portuguese, Chinese, Japanese, Russian, Croatian and Italian. If you would like to contribute a translation, it is quite straightforward, please follow these steps:
|
||||
Joplin is currently available in English, French, Spanish, German, Portuguese, Chinese, Japanese, Russian, Croatian, Dutch and Italian. If you would like to contribute a translation, it is quite straightforward, please follow these steps:
|
||||
|
||||
- [Download Poedit](https://poedit.net/), the translation editor, and install it.
|
||||
- [Download the file to be translated](https://raw.githubusercontent.com/laurent22/joplin/master/CliClient/locales/joplin.pot).
|
||||
|
@@ -1,10 +1,10 @@
|
||||
# About End-To-End Encryption (E2EE)
|
||||
|
||||
3. Now you need to synchronise all your notes so that thEnd-to-end encryption (E2EE) is a system where only the owner of the notes, notebooks, tags or resources can read them. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developer of Joplin from being able to access the data.
|
||||
End-to-end encryption (E2EE) is a system where only the owner of the data (i.e. notes, notebooks, tags or resources) can read it. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developers of Joplin from being able to access the data.
|
||||
|
||||
The systems is designed to defeat any attempts at surveillance or tampering because no third parties can decipher the data being communicated or stored.
|
||||
The system is designed to defeat any attempts at surveillance or tampering because no third party can decipher the data being communicated or stored.
|
||||
|
||||
There is a small overhead to using E2EE since data constantly have to be encrypted and decrypted so consider whether you really need the feature.
|
||||
There is a small overhead to using E2EE since data constantly has to be encrypted and decrypted so consider whether you really need the feature.
|
||||
|
||||
# Enabling E2EE
|
||||
|
||||
@@ -13,8 +13,8 @@ Due to the decentralised nature of Joplin, E2EE needs to be manually enabled on
|
||||
To enable it, please follow these steps:
|
||||
|
||||
1. On your first device (eg. on the desktop application), go to the Encryption Config screen and click "Enable encryption"
|
||||
2. Input your password. This is the Master Key password which will be used to encrypt all your notes. Make sure you do not forget it since, for security reason, it cannot be recovered.
|
||||
ey are sent encrypted to the sync target (eg. to OneDrive, Nextcloud, etc.). Wait for any synchronisation that might be in progress and click on "Synchronise".
|
||||
2. Input your password. This is the Master Key password which will be used to encrypt all your notes. Make sure you to not forget it since, for security reason, it cannot be recovered.
|
||||
3. Now you need to synchronise all your notes so that they are sent encrypted to the sync target (eg. to OneDrive, Nextcloud, etc.). Wait for any synchronisation that might be in progress and click on "Synchronise".
|
||||
4. Wait for this synchronisation operation to complete. Since all the data needs to be re-sent (encrypted) to the sync target, it may take a long time, especially if you have many notes and resources. Note that even if synchronisation seems stuck, most likely it is still running - do not cancel it and simply let it run over night if needed.
|
||||
5. Once this first synchronisation operation is done, open the next device you are synchronising with. Click "Synchronise" and wait for the sync operation to complete. The device will receive the master key, and you will need to provide the password for it. At this point E2EE will be automatically enabled on this device. Once done, click Synchronise again and wait for it to complete.
|
||||
6. Repeat step 5 for each device.
|
||||
@@ -23,4 +23,8 @@ Once all the devices are in sync with E2EE enabled, the encryption/decryption sh
|
||||
|
||||
# Disabling E2EE
|
||||
|
||||
Follow the same procedure as above but instead disable E2EE on each device one by one. Again it might be simpler to do it one device at a time and to wait every time for the synchronisation to complete.
|
||||
Follow the same procedure as above but instead disable E2EE on each device one by one. Again it might be simpler to do it one device at a time and to wait every time for the synchronisation to complete.
|
||||
|
||||
# Technical specification
|
||||
|
||||
For a more technical description, mostly relevant for development or to review the method being used, please see the [Encryption specification](http://joplin.cozic.net/help/spec).
|
4
README_faq.md
Normal file
4
README_faq.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# When I open a note in vim, the cursor is not visible
|
||||
|
||||
It seems to be due to the setting `set term=ansi` in .vimrc. Removing it should fix the issue. See https://github.com/laurent22/joplin/issues/147 for more information.
|
||||
|
@@ -19,11 +19,11 @@ Length | 6 chars (Hexa string)
|
||||
Encryption method | 2 chars (Hexa string)
|
||||
Master key ID | 32 chars (Hexa string)
|
||||
|
||||
See lib/services/EncryptionService.js for the list of available encryption methods.
|
||||
See `lib/services/EncryptionService.js` for the list of available encryption methods.
|
||||
|
||||
### Data chunk
|
||||
|
||||
The data is encoded in one or more chuncks for performance reasons. That way it is possible to take a block of data from one file and encrypt it to another block in another file. Encrypting/decrypting the whole file in one go would not work (on mobile especially).
|
||||
The data is encoded in one or more chunks for performance reasons. That way it is possible to take a block of data from one file and encrypt it to another block in another file. Encrypting/decrypting the whole file in one go would not work (on mobile especially).
|
||||
|
||||
Name | Size
|
||||
--------|----------------------------
|
||||
@@ -42,11 +42,11 @@ Only one master key can be active for encryption purposes. For decryption, the a
|
||||
|
||||
## Encryption Service
|
||||
|
||||
The applications make use of the EncryptionService class to handle encryption and decryption. Before it can be used, a least one master key must be loaded into it and marked as "active".
|
||||
The applications make use of the `EncryptionService` class to handle encryption and decryption. Before it can be used, a least one master key must be loaded into it and be marked as "active".
|
||||
|
||||
## Encryption workflow
|
||||
|
||||
Items are encrypted only during synchronisation, when they are serialised (via BaseItem.serializeForSync), so before being sent to the sync target.
|
||||
Items are encrypted only during synchronisation, when they are serialised (via `BaseItem.serializeForSync`), so before being sent to the sync target.
|
||||
|
||||
They are decrypted by DecryptionWorker in the background.
|
||||
|
||||
|
@@ -75,7 +75,7 @@ apply from: "../../node_modules/react-native/react.gradle"
|
||||
* Upload all the APKs to the Play Store and people will download
|
||||
* the correct one based on the CPU architecture of their device.
|
||||
*/
|
||||
def enableSeparateBuildPerCPUArchitecture = true
|
||||
def enableSeparateBuildPerCPUArchitecture = false
|
||||
|
||||
/**
|
||||
* Run Proguard to shrink the Java bytecode in release builds.
|
||||
@@ -90,8 +90,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 22
|
||||
versionCode 88
|
||||
versionName "0.10.73"
|
||||
versionCode 2097256
|
||||
versionName "0.10.78"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
@@ -137,11 +137,11 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':react-native-securerandom')
|
||||
compile project(':react-native-push-notification')
|
||||
compile project(':react-native-fs')
|
||||
compile project(':react-native-image-picker')
|
||||
compile project(':react-native-vector-icons')
|
||||
compile project(':react-native-securerandom')
|
||||
compile project(':react-native-push-notification')
|
||||
compile project(':react-native-fs')
|
||||
compile project(':react-native-image-picker')
|
||||
compile project(':react-native-vector-icons')
|
||||
compile project(':react-native-fs')
|
||||
compile fileTree(dir: "libs", include: ["*.jar"])
|
||||
compile "com.android.support:appcompat-v7:23.0.1"
|
||||
|
@@ -271,7 +271,7 @@ class BaseModel {
|
||||
|
||||
o = temp;
|
||||
|
||||
const modelId = temp.id;
|
||||
let modelId = temp.id;
|
||||
let query = {};
|
||||
|
||||
const timeNow = time.unixMs();
|
||||
|
36
ReactNativeClient/lib/Cache.js
Normal file
36
ReactNativeClient/lib/Cache.js
Normal file
@@ -0,0 +1,36 @@
|
||||
class Cache {
|
||||
|
||||
async getItem(name) {
|
||||
let output = null;
|
||||
try {
|
||||
const storage = await Cache.storage();
|
||||
output = await storage.getItem(name);
|
||||
} catch (error) {
|
||||
console.info(error);
|
||||
// Defaults to returning null
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async setItem(name, value, ttl = null) {
|
||||
try {
|
||||
const storage = await Cache.storage();
|
||||
const options = {};
|
||||
if (ttl !== null) options.ttl = ttl;
|
||||
await storage.setItem(name, value, options);
|
||||
} catch (error) {
|
||||
// Defaults to not saving to cache
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Cache.storage = async function() {
|
||||
if (Cache.storage_) return Cache.storage_;
|
||||
Cache.storage_ = require('node-persist');
|
||||
const osTmpdir = require('os-tmpdir');
|
||||
await Cache.storage_.init({ dir: osTmpdir() + '/joplin-cache', ttl: 1000 * 60 });
|
||||
return Cache.storage_;
|
||||
}
|
||||
|
||||
module.exports = Cache;
|
@@ -24,9 +24,12 @@ class SyncTargetFilesystem extends BaseSyncTarget {
|
||||
}
|
||||
|
||||
async initFileApi() {
|
||||
const fileApi = new FileApi(Setting.value('sync.2.path'), new FileApiDriverLocal());
|
||||
const syncPath = Setting.value('sync.2.path');
|
||||
const driver = new FileApiDriverLocal();
|
||||
const fileApi = new FileApi(syncPath, driver);
|
||||
fileApi.setLogger(this.logger());
|
||||
fileApi.setSyncTargetId(SyncTargetFilesystem.id());
|
||||
await driver.mkdir(syncPath);
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,3 @@
|
||||
const fs = require('fs-extra');
|
||||
const { promiseChain } = require('lib/promise-utils.js');
|
||||
const moment = require('moment');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
@@ -19,73 +16,63 @@ const { time } = require('lib/time-utils.js');
|
||||
|
||||
class FileApiDriverLocal {
|
||||
|
||||
fsErrorToJsError_(error) {
|
||||
fsErrorToJsError_(error, path = null) {
|
||||
let msg = error.toString();
|
||||
if (path !== null) msg += '. Path: ' + path;
|
||||
let output = new Error(msg);
|
||||
if (error.code) output.code = error.code;
|
||||
return output;
|
||||
}
|
||||
|
||||
stat(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.stat(path, (error, s) => {
|
||||
if (error) {
|
||||
if (error.code == 'ENOENT') {
|
||||
resolve(null);
|
||||
} else {
|
||||
reject(this.fsErrorToJsError_(error));
|
||||
}
|
||||
return;
|
||||
}
|
||||
resolve(this.metadataFromStats_(path, s));
|
||||
});
|
||||
});
|
||||
fsDriver() {
|
||||
if (!FileApiDriverLocal.fsDriver_) throw new Error('FileApiDriverLocal.fsDriver_ not set!');
|
||||
return FileApiDriverLocal.fsDriver_;
|
||||
}
|
||||
|
||||
statTimeToTimestampMs_(time) {
|
||||
let m = moment(time, 'YYYY-MM-DDTHH:mm:ss.SSSZ');
|
||||
if (!m.isValid()) {
|
||||
throw new Error('Invalid date: ' + time);
|
||||
async stat(path) {
|
||||
try {
|
||||
const s = await this.fsDriver().stat(path);
|
||||
if (!s) return null;
|
||||
return this.metadataFromStat_(s);
|
||||
} catch (error) {
|
||||
throw this.fsErrorToJsError_(error);
|
||||
}
|
||||
return m.toDate().getTime();
|
||||
}
|
||||
|
||||
metadataFromStats_(path, stats) {
|
||||
metadataFromStat_(stat) {
|
||||
return {
|
||||
path: path,
|
||||
created_time: this.statTimeToTimestampMs_(stats.birthtime),
|
||||
updated_time: this.statTimeToTimestampMs_(stats.mtime),
|
||||
created_time_orig: stats.birthtime,
|
||||
updated_time_orig: stats.mtime,
|
||||
isDir: stats.isDirectory(),
|
||||
path: stat.path,
|
||||
created_time: stat.birthtime.getTime(),
|
||||
updated_time: stat.mtime.getTime(),
|
||||
created_time_orig: stat.birthtime,
|
||||
updated_time_orig: stat.mtime,
|
||||
isDir: stat.isDirectory(),
|
||||
};
|
||||
}
|
||||
|
||||
setTimestamp(path, timestampMs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let t = Math.floor(timestampMs / 1000);
|
||||
fs.utimes(path, t, t, (error) => {
|
||||
if (error) {
|
||||
reject(this.fsErrorToJsError_(error));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
metadataFromStats_(stats) {
|
||||
let output = [];
|
||||
for (let i = 0; i < stats.length; i++) {
|
||||
const mdStat = this.metadataFromStat_(stats[i]);
|
||||
output.push(mdStat);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async setTimestamp(path, timestampMs) {
|
||||
try {
|
||||
await this.fsDriver().setTimestamp(path, new Date(timestampMs));
|
||||
} catch (error) {
|
||||
throw this.fsErrorToJsError_(error);
|
||||
}
|
||||
}
|
||||
|
||||
async delta(path, options) {
|
||||
const itemIds = await options.allItemIdsHandler();
|
||||
|
||||
try {
|
||||
let items = await fs.readdir(path);
|
||||
let output = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let stat = await this.stat(path + '/' + items[i]);
|
||||
if (!stat) continue; // Has been deleted between the readdir() call and now
|
||||
stat.path = items[i];
|
||||
output.push(stat);
|
||||
}
|
||||
const stats = await this.fsDriver().readDirStats(path);
|
||||
let output = this.metadataFromStats_(stats);
|
||||
|
||||
if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
|
||||
|
||||
@@ -117,20 +104,14 @@ class FileApiDriverLocal {
|
||||
items: output,
|
||||
};
|
||||
} catch(error) {
|
||||
throw this.fsErrorToJsError_(error);
|
||||
throw this.fsErrorToJsError_(error, path);
|
||||
}
|
||||
}
|
||||
|
||||
async list(path, options) {
|
||||
try {
|
||||
let items = await fs.readdir(path);
|
||||
let output = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let stat = await this.stat(path + '/' + items[i]);
|
||||
if (!stat) continue; // Has been deleted between the readdir() call and now
|
||||
stat.path = items[i];
|
||||
output.push(stat);
|
||||
}
|
||||
const stats = await this.fsDriver().readDirStats(path);
|
||||
const output = this.metadataFromStats_(stats);
|
||||
|
||||
return {
|
||||
items: output,
|
||||
@@ -138,7 +119,7 @@ class FileApiDriverLocal {
|
||||
context: null,
|
||||
};
|
||||
} catch(error) {
|
||||
throw this.fsErrorToJsError_(error);
|
||||
throw this.fsErrorToJsError_(error, path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,90 +128,125 @@ class FileApiDriverLocal {
|
||||
|
||||
try {
|
||||
if (options.target === 'file') {
|
||||
output = await fs.copy(path, options.path, { overwrite: true });
|
||||
//output = await fs.copy(path, options.path, { overwrite: true });
|
||||
output = await this.fsDriver().copy(path, options.path);
|
||||
} else {
|
||||
output = await fs.readFile(path, options.encoding);
|
||||
//output = await fs.readFile(path, options.encoding);
|
||||
output = await this.fsDriver().readFile(path, options.encoding);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code == 'ENOENT') return null;
|
||||
throw this.fsErrorToJsError_(error);
|
||||
throw this.fsErrorToJsError_(error, path);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
mkdir(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.exists(path, (exists) => {
|
||||
if (exists) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
async mkdir(path) {
|
||||
if (await this.fsDriver().exists(path)) return;
|
||||
|
||||
try {
|
||||
await this.fsDriver().mkdir(path);
|
||||
} catch (error) {
|
||||
throw this.fsErrorToJsError_(error, path);
|
||||
}
|
||||
|
||||
// return new Promise((resolve, reject) => {
|
||||
// fs.exists(path, (exists) => {
|
||||
// if (exists) {
|
||||
// resolve();
|
||||
// return;
|
||||
// }
|
||||
|
||||
fs.mkdirp(path, (error) => {
|
||||
if (error) {
|
||||
reject(this.fsErrorToJsError_(error));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
// fs.mkdirp(path, (error) => {
|
||||
// if (error) {
|
||||
// reject(this.fsErrorToJsError_(error));
|
||||
// } else {
|
||||
// resolve();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
async put(path, content, options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
if (options.source === 'file') content = await fs.readFile(options.path);
|
||||
try {
|
||||
if (options.source === 'file') {
|
||||
await this.fsDriver().copy(options.path, path);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.fsDriver().writeFile(path, content, 'utf8');
|
||||
} catch (error) {
|
||||
throw this.fsErrorToJsError_(error, path);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.writeFile(path, content, function(error) {
|
||||
if (error) {
|
||||
reject(this.fsErrorToJsError_(error));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
// if (!options) options = {};
|
||||
|
||||
// if (options.source === 'file') content = await fs.readFile(options.path);
|
||||
|
||||
// return new Promise((resolve, reject) => {
|
||||
// fs.writeFile(path, content, function(error) {
|
||||
// if (error) {
|
||||
// reject(this.fsErrorToJsError_(error));
|
||||
// } else {
|
||||
// resolve();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
delete(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.unlink(path, function(error) {
|
||||
if (error) {
|
||||
if (error && error.code == 'ENOENT') {
|
||||
// File doesn't exist - it's fine
|
||||
resolve();
|
||||
} else {
|
||||
reject(this.fsErrorToJsError_(error));
|
||||
}
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
async delete(path) {
|
||||
try {
|
||||
await this.fsDriver().unlink(path);
|
||||
} catch (error) {
|
||||
throw this.fsErrorToJsError_(error, path);
|
||||
}
|
||||
|
||||
// return new Promise((resolve, reject) => {
|
||||
// fs.unlink(path, function(error) {
|
||||
// if (error) {
|
||||
// if (error && error.code == 'ENOENT') {
|
||||
// // File doesn't exist - it's fine
|
||||
// resolve();
|
||||
// } else {
|
||||
// reject(this.fsErrorToJsError_(error));
|
||||
// }
|
||||
// } else {
|
||||
// resolve();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
async move(oldPath, newPath) {
|
||||
let lastError = null;
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
try {
|
||||
let output = await fs.move(oldPath, newPath, { overwrite: true });
|
||||
return output;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
// Normally cannot happen with the `overwrite` flag but sometime it still does.
|
||||
// In this case, retry.
|
||||
if (error.code == 'EEXIST') {
|
||||
await time.sleep(1);
|
||||
continue;
|
||||
}
|
||||
throw this.fsErrorToJsError_(error);
|
||||
}
|
||||
try {
|
||||
await this.fsDriver().move(oldPath, newPath);
|
||||
} catch (error) {
|
||||
throw this.fsErrorToJsError_(error, path);
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
// let lastError = null;
|
||||
|
||||
// for (let i = 0; i < 5; i++) {
|
||||
// try {
|
||||
// let output = await fs.move(oldPath, newPath, { overwrite: true });
|
||||
// return output;
|
||||
// } catch (error) {
|
||||
// lastError = error;
|
||||
// // Normally cannot happen with the `overwrite` flag but sometime it still does.
|
||||
// // In this case, retry.
|
||||
// if (error.code == 'EEXIST') {
|
||||
// await time.sleep(1);
|
||||
// continue;
|
||||
// }
|
||||
// throw this.fsErrorToJsError_(error);
|
||||
// }
|
||||
// }
|
||||
|
||||
// throw lastError;
|
||||
}
|
||||
|
||||
format() {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
const fs = require('fs-extra');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
class FsDriverNode {
|
||||
|
||||
@@ -15,14 +16,67 @@ class FsDriverNode {
|
||||
return fs.writeFile(path, buffer);
|
||||
}
|
||||
|
||||
move(source, dest) {
|
||||
return fs.move(source, dest, { overwrite: true });
|
||||
writeFile(path, string, encoding = 'base64') {
|
||||
return fs.writeFile(path, string, { encoding: encoding });
|
||||
}
|
||||
|
||||
async move(source, dest) {
|
||||
let lastError = null;
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
try {
|
||||
const output = await fs.move(source, dest, { overwrite: true });
|
||||
return output;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
// Normally cannot happen with the `overwrite` flag but sometime it still does.
|
||||
// In this case, retry.
|
||||
if (error.code == 'EEXIST') {
|
||||
await time.sleep(1);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
exists(path) {
|
||||
return fs.pathExists(path);
|
||||
}
|
||||
|
||||
async mkdir(path) {
|
||||
return fs.mkdirp(path);
|
||||
}
|
||||
|
||||
async stat(path) {
|
||||
try {
|
||||
const s = await fs.stat(path);
|
||||
s.path = path;
|
||||
return s;
|
||||
} catch (error) {
|
||||
if (error.code == 'ENOENT') return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async setTimestamp(path, timestampDate) {
|
||||
return fs.utimes(path, timestampDate, timestampDate);
|
||||
}
|
||||
|
||||
async readDirStats(path) {
|
||||
let items = await fs.readdir(path);
|
||||
let output = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let stat = await this.stat(path + '/' + items[i]);
|
||||
if (!stat) continue; // Has been deleted between the readdir() call and now
|
||||
stat.path = stat.path.substr(path.length + 1);
|
||||
output.push(stat);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
open(path, mode) {
|
||||
return fs.open(path, mode);
|
||||
}
|
||||
@@ -31,8 +85,14 @@ class FsDriverNode {
|
||||
return fs.close(handle);
|
||||
}
|
||||
|
||||
readFile(path) {
|
||||
return fs.readFile(path);
|
||||
readFile(path, encoding = 'utf8') {
|
||||
if (encoding === 'Buffer') return fs.readFile(path); // Returns the raw buffer
|
||||
return fs.readFile(path, encoding);
|
||||
}
|
||||
|
||||
// Always overwrite destination
|
||||
async copy(source, dest) {
|
||||
return fs.copy(source, dest, { overwrite: true });
|
||||
}
|
||||
|
||||
async unlink(path) {
|
||||
|
@@ -10,10 +10,36 @@ class FsDriverRN {
|
||||
return RNFS.appendFile(path, string, encoding);
|
||||
}
|
||||
|
||||
writeFile(path, string, encoding = 'base64') {
|
||||
return RNFS.writeFile(path, string, encoding);
|
||||
}
|
||||
|
||||
writeBinaryFile(path, content) {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
// Returns a format compatible with Node.js format
|
||||
rnfsStatToStd_(stat, path) {
|
||||
return {
|
||||
birthtime: stat.ctime ? stat.ctime : stat.mtime, // Confusingly, "ctime" normally means "change time" but here it's used as "creation time". Also sometimes it is null
|
||||
mtime: stat.mtime,
|
||||
isDirectory: () => stat.isDirectory(),
|
||||
path: path,
|
||||
size: stat.size,
|
||||
};
|
||||
}
|
||||
|
||||
async readDirStats(path) {
|
||||
let items = await RNFS.readDir(path);
|
||||
let output = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const relativePath = item.path.substr(path.length + 1);
|
||||
output.push(this.rnfsStatToStd_(item, relativePath));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async move(source, dest) {
|
||||
return RNFS.moveFile(source, dest);
|
||||
}
|
||||
@@ -22,11 +48,38 @@ class FsDriverRN {
|
||||
return RNFS.exists(path);
|
||||
}
|
||||
|
||||
async mkdir(path) {
|
||||
return RNFS.mkdir(path);
|
||||
}
|
||||
|
||||
async stat(path) {
|
||||
try {
|
||||
const r = await RNFS.stat(path);
|
||||
return this.rnfsStatToStd_(r, path);
|
||||
} catch (error) {
|
||||
if (error && error.message && error.message.indexOf('exist') >= 0) {
|
||||
// Probably { [Error: File does not exist] framesToPop: 1, code: 'EUNSPECIFIED' }
|
||||
// which unfortunately does not have a proper error code. Can be ignored.
|
||||
return null;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: DOES NOT WORK - no error is thrown and the function is called with the right
|
||||
// arguments but the function returns `false` and the timestamp is not set.
|
||||
// Current setTimestamp is not really used so keep it that way, but careful if it
|
||||
// becomes needed.
|
||||
async setTimestamp(path, timestampDate) {
|
||||
// return RNFS.touch(path, timestampDate, timestampDate);
|
||||
}
|
||||
|
||||
async open(path, mode) {
|
||||
// Note: RNFS.read() doesn't provide any way to know if the end of file has been reached.
|
||||
// So instead we stat the file here and use stat.size to manually check for end of file.
|
||||
// Bug: https://github.com/itinance/react-native-fs/issues/342
|
||||
const stat = await RNFS.stat(path);
|
||||
const stat = await this.stat(path);
|
||||
return {
|
||||
path: path,
|
||||
offset: 0,
|
||||
@@ -39,8 +92,23 @@ class FsDriverRN {
|
||||
return null;
|
||||
}
|
||||
|
||||
readFile(path) {
|
||||
throw new Error('Not implemented');
|
||||
readFile(path, encoding = 'utf8') {
|
||||
if (encoding === 'Buffer') throw new Error('Raw buffer output not supported for FsDriverRN.readFile');
|
||||
return RNFS.readFile(path, encoding);
|
||||
}
|
||||
|
||||
// Always overwrite destination
|
||||
async copy(source, dest) {
|
||||
let retry = false;
|
||||
try {
|
||||
await RNFS.copyFile(source, dest);
|
||||
} catch (error) {
|
||||
// On iOS it will throw an error if the file already exist
|
||||
retry = true;
|
||||
await this.unlink(dest);
|
||||
}
|
||||
|
||||
if (retry) await RNFS.copyFile(source, dest);
|
||||
}
|
||||
|
||||
async unlink(path) {
|
||||
|
@@ -174,6 +174,15 @@ class BaseItem extends BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Currently, once a deleted_items entry has been processed, it is removed from the database. In practice it means that
|
||||
// the following case will not work as expected:
|
||||
// - Client 1 creates a note and sync with target 1 and 2
|
||||
// - Client 2 sync with target 1
|
||||
// - Client 2 deletes note and sync with target 1
|
||||
// - Client 1 syncs with target 1 only (note is deleted from local machine, as expected)
|
||||
// - Client 1 syncs with target 2 only => the note is *not* deleted from target 2 because no information
|
||||
// that it was previously deleted exist (deleted_items entry has been deleted).
|
||||
// The solution would be to permanently store the list of deleted items on each client.
|
||||
static deletedItems(syncTarget) {
|
||||
return this.db().selectAll('SELECT * FROM deleted_items WHERE sync_target = ?', [syncTarget]);
|
||||
}
|
||||
|
@@ -120,7 +120,7 @@ class Resource extends BaseItem {
|
||||
}
|
||||
|
||||
static async content(resource) {
|
||||
return this.fsDriver().readFile(this.fullPath(resource));
|
||||
return this.fsDriver().readFile(this.fullPath(resource), 'Buffer');
|
||||
}
|
||||
|
||||
static setContent(resource, content) {
|
||||
|
@@ -192,7 +192,8 @@ class Setting extends BaseModel {
|
||||
|
||||
if (c.value === value) return;
|
||||
|
||||
this.logger().info('Setting: ' + key + ' = ' + c.value + ' => ' + value);
|
||||
// Don't log this to prevent sensitive info (passwords, auth tokens...) to end up in logs
|
||||
// this.logger().info('Setting: ' + key + ' = ' + c.value + ' => ' + value);
|
||||
|
||||
c.value = value;
|
||||
|
||||
|
@@ -18,7 +18,7 @@ class Synchronizer {
|
||||
this.state_ = 'idle';
|
||||
this.db_ = db;
|
||||
this.api_ = api;
|
||||
this.syncDirName_ = '.sync';
|
||||
//this.syncDirName_ = '.sync';
|
||||
this.resourceDirName_ = '.resource';
|
||||
this.logger_ = new Logger();
|
||||
this.appType_ = appType;
|
||||
@@ -92,14 +92,12 @@ class Synchronizer {
|
||||
if (local) {
|
||||
let s = [];
|
||||
s.push(local.id);
|
||||
if ('title' in local) s.push('"' + local.title + '"');
|
||||
line.push('(Local ' + s.join(', ') + ')');
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
let s = [];
|
||||
s.push(remote.id ? remote.id : remote.path);
|
||||
if ('title' in remote) s.push('"' + remote.title + '"');
|
||||
line.push('(Remote ' + s.join(', ') + ')');
|
||||
}
|
||||
|
||||
@@ -197,7 +195,7 @@ class Synchronizer {
|
||||
this.logSyncOperation('starting', null, null, 'Starting synchronisation to target ' + syncTargetId + '... [' + synchronizationId + ']');
|
||||
|
||||
try {
|
||||
await this.api().mkdir(this.syncDirName_);
|
||||
//await this.api().mkdir(this.syncDirName_);
|
||||
await this.api().mkdir(this.resourceDirName_);
|
||||
|
||||
let donePaths = [];
|
||||
@@ -585,7 +583,7 @@ class Synchronizer {
|
||||
if (noteIds.length) { // CONFLICT
|
||||
await Folder.markNotesAsConflict(item.id);
|
||||
}
|
||||
await Folder.delete(item.id, { deleteChildren: false });
|
||||
await Folder.delete(item.id, { deleteChildren: false, trackDeleted: false });
|
||||
}
|
||||
}
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@@ -7,6 +7,7 @@ locales['fr_FR'] = require('./fr_FR.json');
|
||||
locales['hr_HR'] = require('./hr_HR.json');
|
||||
locales['it_IT'] = require('./it_IT.json');
|
||||
locales['ja_JP'] = require('./ja_JP.json');
|
||||
locales['nl_BE'] = require('./nl_BE.json');
|
||||
locales['pt_BR'] = require('./pt_BR.json');
|
||||
locales['ru_RU'] = require('./ru_RU.json');
|
||||
locales['zh_CN'] = require('./zh_CN.json');
|
||||
|
1
ReactNativeClient/locales/nl_BE.json
Normal file
1
ReactNativeClient/locales/nl_BE.json
Normal file
File diff suppressed because one or more lines are too long
@@ -44,14 +44,19 @@ const { _, setLocale, closestSupportedLocale, defaultLocale } = require('lib/loc
|
||||
const RNFetchBlob = require('react-native-fetch-blob').default;
|
||||
const { PoorManIntervals } = require('lib/poor-man-intervals.js');
|
||||
const { reducer, defaultState } = require('lib/reducer.js');
|
||||
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
|
||||
const DropdownAlert = require('react-native-dropdownalert').default;
|
||||
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
|
||||
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
||||
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
||||
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
|
||||
|
||||
// Disabled because not fully working
|
||||
//SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||
|
||||
const FsDriverRN = require('lib/fs-driver-rn.js').FsDriverRN;
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
@@ -341,6 +346,7 @@ async function initialize(dispatch) {
|
||||
const fsDriver = new FsDriverRN();
|
||||
|
||||
Resource.fsDriver_ = fsDriver;
|
||||
FileApiDriverLocal.fsDriver_ = fsDriver;
|
||||
|
||||
AlarmService.setDriver(new AlarmServiceDriver());
|
||||
AlarmService.setLogger(mainLogger);
|
||||
|
13
Tools/package-lock.json
generated
13
Tools/package-lock.json
generated
@@ -73,6 +73,11 @@
|
||||
"is-stream": "1.1.0"
|
||||
}
|
||||
},
|
||||
"pct-encode": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pct-encode/-/pct-encode-1.0.2.tgz",
|
||||
"integrity": "sha1-uZt7BE1r18OeSDmnqAEirXUVyqU="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||
@@ -82,6 +87,14 @@
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
|
||||
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
|
||||
},
|
||||
"uri-template": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-template/-/uri-template-1.0.1.tgz",
|
||||
"integrity": "sha1-FKklo35Nk/diVDKqEWsF5Qyuga0=",
|
||||
"requires": {
|
||||
"pct-encode": "1.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@
|
||||
"gettext-parser": "^1.3.0",
|
||||
"marked": "^0.3.7",
|
||||
"mustache": "^2.3.0",
|
||||
"node-fetch": "^1.7.3"
|
||||
"node-fetch": "^1.7.3",
|
||||
"uri-template": "^1.0.1"
|
||||
}
|
||||
}
|
||||
|
132
Tools/release-android.js
Normal file
132
Tools/release-android.js
Normal file
@@ -0,0 +1,132 @@
|
||||
const fs = require('fs-extra');
|
||||
const { execCommand } = require('./tool-utils.js');
|
||||
const path = require('path');
|
||||
const fetch = require('node-fetch');
|
||||
const uriTemplate = require('uri-template');
|
||||
|
||||
const rnDir = __dirname + '/../ReactNativeClient';
|
||||
const rootDir = path.dirname(__dirname);
|
||||
const releaseDir = rootDir + '/_releases';
|
||||
|
||||
function increaseGradleVersionCode(content) {
|
||||
const newContent = content.replace(/versionCode\s+(\d+)/, function(a, versionCode, c) {
|
||||
const n = Number(versionCode);
|
||||
if (isNaN(n) || !n) throw new Error('Invalid version code: ' + versionCode);
|
||||
return 'versionCode ' + (n + 1);
|
||||
});
|
||||
|
||||
if (newContent === content) throw new Error('Could not update version code');
|
||||
|
||||
return newContent;
|
||||
}
|
||||
|
||||
function increaseGradleVersionName(content) {
|
||||
const newContent = content.replace(/(versionName\s+"\d+?\.\d+?\.)(\d+)"/, function(match, prefix, buildNum) {
|
||||
const n = Number(buildNum);
|
||||
if (isNaN(n) || !n) throw new Error('Invalid version code: ' + versionCode);
|
||||
return prefix + (n + 1) + '"';
|
||||
});
|
||||
|
||||
if (newContent === content) throw new Error('Could not update version name');
|
||||
|
||||
return newContent;
|
||||
}
|
||||
|
||||
function updateGradleConfig() {
|
||||
let content = fs.readFileSync(rnDir + '/android/app/build.gradle', 'utf8');
|
||||
content = increaseGradleVersionCode(content);
|
||||
content = increaseGradleVersionName(content);
|
||||
fs.writeFileSync(rnDir + '/android/app/build.gradle', content);
|
||||
return content;
|
||||
}
|
||||
|
||||
function gradleVersionName(content) {
|
||||
const matches = content.match(/versionName\s+"(\d+?\.\d+?\.\d+)"/);
|
||||
if (!matches || matches.length < 1) throw new Error('Cannot get gradle version name');
|
||||
return matches[1];
|
||||
}
|
||||
|
||||
async function githubOauthToken() {
|
||||
const r = await fs.readFile(rootDir + '/Tools/github_oauth_token.txt');
|
||||
return r.toString();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const oauthToken = await githubOauthToken();
|
||||
|
||||
const newContent = updateGradleConfig();
|
||||
const version = gradleVersionName(newContent);
|
||||
const tagName = 'android-v' + version;
|
||||
const apkFilename = 'joplin-v' + version + '.apk';
|
||||
const apkFilePath = releaseDir + '/' + apkFilename;
|
||||
const downloadUrl = 'https://github.com/laurent22/joplin/releases/download/' + tagName + '/' + apkFilename;
|
||||
|
||||
process.chdir(rootDir);
|
||||
|
||||
console.info('Running from: ' + process.cwd());
|
||||
|
||||
console.info('Building APK file...');
|
||||
const output = await execCommand('/mnt/c/Windows/System32/cmd.exe /c "cd ReactNativeClient\\android && gradlew.bat assembleRelease -PbuildDir=build --console plain"');
|
||||
console.info(output);
|
||||
|
||||
await fs.mkdirp(releaseDir);
|
||||
|
||||
console.info('Copying APK to ' + apkFilePath);
|
||||
await fs.copy('ReactNativeClient/android/app/build/outputs/apk/app-release.apk', apkFilePath);
|
||||
|
||||
console.info('Updating Readme URL...');
|
||||
|
||||
let readmeContent = await fs.readFile('README.md', 'utf8');
|
||||
readmeContent = readmeContent.replace(/(https:\/\/github.com\/laurent22\/joplin\/releases\/download\/.*?\.apk)/, downloadUrl);
|
||||
await fs.writeFile('README.md', readmeContent);
|
||||
|
||||
await execCommand('git add -A');
|
||||
await execCommand('git commit -m "Android release v' + version + '"');
|
||||
await execCommand('git tag ' + tagName);
|
||||
await execCommand('git push');
|
||||
await execCommand('git push --tags');
|
||||
|
||||
console.info('Creating GitHub release ' + tagName + '...');
|
||||
|
||||
const response = await fetch('https://api.github.com/repos/laurent22/joplin/releases', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
tag_name: tagName,
|
||||
name: tagName,
|
||||
draft: true,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'token ' + oauthToken,
|
||||
},
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
const responseJson = JSON.parse(responseText);
|
||||
if (!responseJson.upload_url) throw new Error('No upload URL for release: ' + responseText);
|
||||
|
||||
const uploadUrlTemplate = uriTemplate.parse(responseJson.upload_url);
|
||||
const uploadUrl = uploadUrlTemplate.expand({ name: apkFilename });
|
||||
|
||||
const binaryBody = await fs.readFile(apkFilePath);
|
||||
|
||||
console.info('Uploading ' + apkFilename + ' to ' + uploadUrl);
|
||||
|
||||
const uploadResponse = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
body: binaryBody,
|
||||
headers: {
|
||||
'Content-Type': 'application/vnd.android.package-archive',
|
||||
'Authorization': 'token ' + oauthToken,
|
||||
'Content-Length': binaryBody.length,
|
||||
},
|
||||
});
|
||||
|
||||
const uploadResponseText = await uploadResponse.text();
|
||||
console.info(uploadResponseText);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error');
|
||||
console.error(error);
|
||||
});
|
@@ -200,18 +200,16 @@
|
||||
|
||||
<div class="content">
|
||||
<h1 id="about-end-to-end-encryption-e2ee-">About End-To-End Encryption (E2EE)</h1>
|
||||
<ol>
|
||||
<li>Now you need to synchronise all your notes so that thEnd-to-end encryption (E2EE) is a system where only the owner of the notes, notebooks, tags or resources can read them. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developer of Joplin from being able to access the data.</li>
|
||||
</ol>
|
||||
<p>The systems is designed to defeat any attempts at surveillance or tampering because no third parties can decipher the data being communicated or stored.</p>
|
||||
<p>There is a small overhead to using E2EE since data constantly have to be encrypted and decrypted so consider whether you really need the feature.</p>
|
||||
<p>End-to-end encryption (E2EE) is a system where only the owner of the data (i.e. notes, notebooks, tags or resources) can read it. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developers of Joplin from being able to access the data.</p>
|
||||
<p>The system is designed to defeat any attempts at surveillance or tampering because no third party can decipher the data being communicated or stored.</p>
|
||||
<p>There is a small overhead to using E2EE since data constantly has to be encrypted and decrypted so consider whether you really need the feature.</p>
|
||||
<h1 id="enabling-e2ee">Enabling E2EE</h1>
|
||||
<p>Due to the decentralised nature of Joplin, E2EE needs to be manually enabled on all the applications that you synchronise with. It is recommended to first enable it on the desktop or terminal application since they generally run on more powerful devices (unlike the mobile application), and so they can encrypt the initial data faster.</p>
|
||||
<p>To enable it, please follow these steps:</p>
|
||||
<ol>
|
||||
<li>On your first device (eg. on the desktop application), go to the Encryption Config screen and click "Enable encryption"</li>
|
||||
<li>Input your password. This is the Master Key password which will be used to encrypt all your notes. Make sure you do not forget it since, for security reason, it cannot be recovered.
|
||||
ey are sent encrypted to the sync target (eg. to OneDrive, Nextcloud, etc.). Wait for any synchronisation that might be in progress and click on "Synchronise".</li>
|
||||
<li>Input your password. This is the Master Key password which will be used to encrypt all your notes. Make sure you to not forget it since, for security reason, it cannot be recovered.</li>
|
||||
<li>Now you need to synchronise all your notes so that they are sent encrypted to the sync target (eg. to OneDrive, Nextcloud, etc.). Wait for any synchronisation that might be in progress and click on "Synchronise".</li>
|
||||
<li>Wait for this synchronisation operation to complete. Since all the data needs to be re-sent (encrypted) to the sync target, it may take a long time, especially if you have many notes and resources. Note that even if synchronisation seems stuck, most likely it is still running - do not cancel it and simply let it run over night if needed.</li>
|
||||
<li>Once this first synchronisation operation is done, open the next device you are synchronising with. Click "Synchronise" and wait for the sync operation to complete. The device will receive the master key, and you will need to provide the password for it. At this point E2EE will be automatically enabled on this device. Once done, click Synchronise again and wait for it to complete.</li>
|
||||
<li>Repeat step 5 for each device.</li>
|
||||
@@ -219,6 +217,8 @@ ey are sent encrypted to the sync target (eg. to OneDrive, Nextcloud, etc.). Wai
|
||||
<p>Once all the devices are in sync with E2EE enabled, the encryption/decryption should be mostly transparent. Occasionally you may see encrypted items but they will get decrypted in the background eventually.</p>
|
||||
<h1 id="disabling-e2ee">Disabling E2EE</h1>
|
||||
<p>Follow the same procedure as above but instead disable E2EE on each device one by one. Again it might be simpler to do it one device at a time and to wait every time for the synchronisation to complete.</p>
|
||||
<h1 id="technical-specification">Technical specification</h1>
|
||||
<p>For a more technical description, mostly relevant for development or to review the method being used, please see the <a href="http://joplin.cozic.net/help/spec">Encryption specification</a>.</p>
|
||||
|
||||
<script>
|
||||
function stickyHeader() {
|
||||
|
@@ -244,9 +244,9 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>See lib/services/EncryptionService.js for the list of available encryption methods.</p>
|
||||
<p>See <code>lib/services/EncryptionService.js</code> for the list of available encryption methods.</p>
|
||||
<h3 id="data-chunk">Data chunk</h3>
|
||||
<p>The data is encoded in one or more chuncks for performance reasons. That way it is possible to take a block of data from one file and encrypt it to another block in another file. Encrypting/decrypting the whole file in one go would not work (on mobile especially).</p>
|
||||
<p>The data is encoded in one or more chunks for performance reasons. That way it is possible to take a block of data from one file and encrypt it to another block in another file. Encrypting/decrypting the whole file in one go would not work (on mobile especially).</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -271,9 +271,9 @@
|
||||
<p>The application supports multiple master keys in order to handle cases where one offline client starts encrypting notes, then another offline client starts encrypting notes too, and later both sync. Both master keys will have to be decrypted separately with the user password.</p>
|
||||
<p>Only one master key can be active for encryption purposes. For decryption, the algorithm will check the Master Key ID in the header, then check if it's available to the current app and, if so, use this for decryption.</p>
|
||||
<h2 id="encryption-service">Encryption Service</h2>
|
||||
<p>The applications make use of the EncryptionService class to handle encryption and decryption. Before it can be used, a least one master key must be loaded into it and marked as "active".</p>
|
||||
<p>The applications make use of the <code>EncryptionService</code> class to handle encryption and decryption. Before it can be used, a least one master key must be loaded into it and be marked as "active".</p>
|
||||
<h2 id="encryption-workflow">Encryption workflow</h2>
|
||||
<p>Items are encrypted only during synchronisation, when they are serialised (via BaseItem.serializeForSync), so before being sent to the sync target.</p>
|
||||
<p>Items are encrypted only during synchronisation, when they are serialised (via <code>BaseItem.serializeForSync</code>), so before being sent to the sync target.</p>
|
||||
<p>They are decrypted by DecryptionWorker in the background.</p>
|
||||
<p>The apps handle displaying both decrypted and encrypted items, so that user is aware that these items are there even if not yet decrypted. Encrypted items are mostly read-only to the user, except that they can be deleted.</p>
|
||||
<h2 id="enabling-and-disabling-encryption">Enabling and disabling encryption</h2>
|
||||
|
@@ -236,16 +236,19 @@
|
||||
<tr>
|
||||
<th>Operating System</th>
|
||||
<th>Download</th>
|
||||
<th>Alt. Download</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Android</td>
|
||||
<td><a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a></td>
|
||||
<td>or <a href="https://github.com/laurent22/joplin/releases/download/android-v0.10.78/joplin-v0.10.78.apk">Download APK File</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>iOS</td>
|
||||
<td><a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeIOS.png'/></a></td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -296,7 +299,10 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
|
||||
<p>On the <strong>desktop application</strong>, to initiate the synchronisation process, click on the "Synchronise" button in the sidebar. You will be asked to login to OneDrive to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.</p>
|
||||
<p>On the <strong>terminal application</strong>, to initiate the synchronisation process, type <code>:sync</code>. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running. It is possible to also synchronise outside of the user interface by typing <code>joplin sync</code> from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:</p>
|
||||
<pre><code>*/30 * * * * /path/to/joplin sync
|
||||
</code></pre><h1 id="attachments-resources">Attachments / Resources</h1>
|
||||
</code></pre><h1 id="encryption">Encryption</h1>
|
||||
<p>Joplin supports end-to-end encryption (E2EE) on all the applications. E2EE is a system where only the owner of the notes, notebooks, tags or resources can read them. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developers of Joplin from being able to access the data. Please see the <a href="http://joplin.cozic.net/help/e2ee">End-To-End Encryption Tutorial</a> for more information about this feature and how to enable it.</p>
|
||||
<p>For a more technical description, mostly relevant for development or to review the method being used, please see the <a href="http://joplin.cozic.net/help/spec">Encryption specification</a>.</p>
|
||||
<h1 id="attachments-resources">Attachments / Resources</h1>
|
||||
<p>Any kind of file can be attached to a note. In Markdown, links to these files are represented as a simple ID to the resource. In the note viewer, these files, if they are images, will be displayed or, if they are other files (PDF, text files, etc.) they will be displayed as links. Clicking on this link will open the file in the default application.</p>
|
||||
<h1 id="notifications">Notifications</h1>
|
||||
<p>On the desktop and mobile apps, an alarm can be associated with any to-do. It will be triggered at the given time by displaying a notification. How the notification will be displayed depends on the operating system since each has a different way to handle this. Please see below for the requirements for the desktop applications:</p>
|
||||
@@ -305,11 +311,11 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
|
||||
<li><strong>macOS</strong>: >= 10.8 or Growl if earlier.</li>
|
||||
<li><strong>Linux</strong>: <code>notify-osd</code> or <code>libnotify-bin</code> installed (Ubuntu should have this by default). Growl otherwise</li>
|
||||
</ul>
|
||||
<p>See <a href="./DECISION_FLOW.md">documentation and flow chart for reporter choice</a></p>
|
||||
<p>See <a href="https://github.com/mikaelbr/node-notifier/blob/master/DECISION_FLOW.md">documentation and flow chart for reporter choice</a></p>
|
||||
<p>On mobile, the alarms will be displayed using the built-in notification system.</p>
|
||||
<p>If for any reason the notifications do not work, please <a href="https://github.com/laurent22/joplin/issues">open an issue</a>.</p>
|
||||
<h1 id="localisation">Localisation</h1>
|
||||
<p>Joplin is currently available in English, French, Spanish, German, Portuguese, Chinese, Japanese, Russian, Croatian and Italian. If you would like to contribute a translation, it is quite straightforward, please follow these steps:</p>
|
||||
<p>Joplin is currently available in English, French, Spanish, German, Portuguese, Chinese, Japanese, Russian, Croatian, Dutch and Italian. If you would like to contribute a translation, it is quite straightforward, please follow these steps:</p>
|
||||
<ul>
|
||||
<li><a href="https://poedit.net/">Download Poedit</a>, the translation editor, and install it.</li>
|
||||
<li><a href="https://raw.githubusercontent.com/laurent22/joplin/master/CliClient/locales/joplin.pot">Download the file to be translated</a>.</li>
|
||||
|
Reference in New Issue
Block a user