1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-24 08:12:24 +02:00

Merge branch 'dev' into release-1.1

This commit is contained in:
Laurent Cozic 2020-09-12 00:03:29 +01:00
commit b7523e1b21
26 changed files with 906 additions and 451 deletions

View File

@ -152,6 +152,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/JoplinServerApi.js
ReactNativeClient/lib/ntpDate.js
ReactNativeClient/lib/services/CommandService.js
ReactNativeClient/lib/services/keychain/KeychainService.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js

1
.gitignore vendored
View File

@ -145,6 +145,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/JoplinServerApi.js
ReactNativeClient/lib/ntpDate.js
ReactNativeClient/lib/services/CommandService.js
ReactNativeClient/lib/services/keychain/KeychainService.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js

View File

@ -15,6 +15,8 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.2.4\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
#: CliClient/app/command-cp.js:13
msgid ""
@ -106,9 +108,9 @@ msgid "Do not ask for confirmation."
msgstr "Χωρίς να ζητείται επιβεβαίωση."
#: CliClient/app/command-import.js:27
#, fuzzy, javascript-format
#, javascript-format
msgid "Output format: %s"
msgstr "Μορφή προέλευσης: %s"
msgstr "Μορφή εξόδου: %s"
#: CliClient/app/command-import.js:47 ElectronClient/gui/ImportScreen.min.js:69
#, javascript-format
@ -222,14 +224,16 @@ msgstr ""
"αρκετά λεπτά ανάλογα με το μέγεθος αυτών που πρέπει να αποκρυπτογραφηθούν."
#: CliClient/app/command-e2ee.js:53
#, fuzzy, javascript-format
#, javascript-format
msgid "Decrypted items: %d"
msgstr "Αποκρυπτογραφημένα στοιχεία: %s / %s"
msgstr "Αποκρυπτογραφημένα στοιχεία: %d"
#: CliClient/app/command-e2ee.js:54
#, javascript-format
msgid "Skipped items: %d (use --retry-failed-items to retry decrypting them)"
msgstr ""
"Στοιχεία που έχουν παραλειφθεί: %d (χρήσιμοποίησε --retry-failed-items για "
"επανάληψη της αποκρυπτογράφησης τους)"
#: CliClient/app/command-e2ee.js:68
msgid "Completed decryption."
@ -373,7 +377,6 @@ msgid "Synchronisation target: %s (%s)"
msgstr "Στόχος συγχρονισμού: %s (%s)"
#: CliClient/app/command-sync.js:177
#, fuzzy
msgid "Cannot initialise synchroniser."
msgstr "Δεν είναι δυνατή η προετοιμασία του συγχρονιστή."
@ -839,14 +842,14 @@ msgid "Goto Anything..."
msgstr "Γρήγορη Μετακίνηση..."
#: ElectronClient/InteropServiceHelper.js:147
#, fuzzy, javascript-format
#, javascript-format
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "Εισαγωγή από \"%s\" σε μορφή \"%s\". Παρακαλώ περιμένετε..."
msgstr "Γίνεται εξαγωγή στο \"%s\" με μορφή \"%s\". Παρακαλώ περιμένετε..."
#: ElectronClient/InteropServiceHelper.js:164
#, fuzzy, javascript-format
#, javascript-format
msgid "Could not export notes: %s"
msgstr "Δεν ήταν δυνατή η αναβάθμιση του master κλειδιού: %s"
msgstr "Δεν ήταν δυνατή η εξαγωγή σημειώσεων: %s"
#: ElectronClient/checkForUpdates.js:138
msgid "Current version is up-to-date."
@ -959,9 +962,10 @@ msgid "Delete"
msgstr "Διαγραφή"
#: ElectronClient/gui/SideBar/SideBar.min.js:276
#, fuzzy, javascript-format
#, javascript-format
msgid "Remove tag \"%s\" and its descendant tags from all notes?"
msgstr "Κατάργηση της ετικέτας \"%s\" από όλες τις σημειώσεις;"
msgstr ""
"Κατάργηση ετικέτας \"%s\" και των απογόνων ετικετών από όλες τις σημειώσεις;"
#: ElectronClient/gui/SideBar/SideBar.min.js:278
#: ElectronClient/gui/SideBar.min.js:292
@ -1031,9 +1035,8 @@ msgstr "έντονη γραφή"
#: ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js:145
#: ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js:261
#, fuzzy
msgid "emphasised text"
msgstr "πλάγια γραφή"
msgstr "κείμενο με έμφαση"
#: ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js:147
#: ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js:263
@ -1091,9 +1094,8 @@ msgstr ""
"επεξεργαστείτε τη σημείωση στον editor."
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js:2151
#, fuzzy
msgid "Checkbox list"
msgstr "Πλαίσιο ελέγχου"
msgstr "Λίστα πλαισίων ελέγχου"
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:470
#: ElectronClient/gui/ConfigScreen.min.js:642
@ -1110,11 +1112,11 @@ msgstr "Επισύναψη αρχείου"
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:535
msgid "Code Block"
msgstr ""
msgstr "Μπλοκ Κώδικα"
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:544
msgid "Inline Code"
msgstr ""
msgstr "Ενσωματωμένος κώδικας"
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:559
#: ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js:80
@ -1124,13 +1126,12 @@ msgstr "Εισαγωγή Ημερομηνίας Ώρας"
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:903
msgid "Drop notes or files here"
msgstr ""
msgstr "Απόθεση σημειώσεων ή αρχείων εδώ"
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:903
#: ElectronClient/gui/MainScreen/MainScreen.min.js:401
#, fuzzy
msgid "Code View"
msgstr "Κώδικας"
msgstr "Προβολή κώδικα"
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:903
#, javascript-format
@ -1138,6 +1139,8 @@ msgid ""
"Please wait for all attachments to be downloaded and decrypted. You may also "
"switch to %s to edit the note."
msgstr ""
"Παρακαλώ περιμένετε να γίνει λήψη και αποκρυπτογράφηση όλων των συνημμένων. "
"Μπορείτε επίσης να μεταβείτε στο %s για να επεξεργαστείτε τη σημείωση."
#: ElectronClient/gui/NoteEditor/utils/useMessageHandler.js:70
#: ElectronClient/gui/NoteText.min.js:833
@ -1162,7 +1165,7 @@ msgstr "Αποθήκευση ως..."
#: ElectronClient/gui/NoteEditor/utils/contextMenu.js:65
msgid "Reveal file in folder"
msgstr ""
msgstr "Αποκάλυψη αρχείου στο φάκελο"
#: ElectronClient/gui/NoteEditor/utils/contextMenu.js:73
#: ElectronClient/gui/NoteText.min.js:797
@ -1342,7 +1345,7 @@ msgstr "Αναζήτηση..."
#: ElectronClient/gui/MainScreen/commands/showNoteContentProperties.js:16
msgid "Statistics..."
msgstr ""
msgstr "Στατιστικά..."
#: ElectronClient/gui/MainScreen/commands/renameFolder.js:17
#: ElectronClient/gui/MainScreen/commands/renameTag.js:17
@ -1605,13 +1608,12 @@ msgstr "Yποβολή"
#: ElectronClient/gui/NoteList.min.js:147
#: ElectronClient/gui/NoteList/NoteList.min.js:152
msgid "Custom order"
msgstr ""
msgstr "Προσαρμοσμένη σειρά"
#: ElectronClient/gui/NoteList.min.js:147
#: ElectronClient/gui/NoteList/NoteList.min.js:152
#, fuzzy
msgid "View"
msgstr "&Εμφάνιση"
msgstr "Προβολή"
#: ElectronClient/gui/NoteList.min.js:147
#: ElectronClient/gui/NoteList/NoteList.min.js:152
@ -1626,12 +1628,13 @@ msgid ""
"To manually sort the notes, the sort order must be changed to \"%s\" in the "
"menu \"%s\" > \"%s\""
msgstr ""
"Για να ταξινομήσετε τις σημειώσεις χειροκίνητα, η σειρά ταξινόμησης πρέπει "
"να αλλάξει σε \"%s\" στο μενού \"%s\" - \"%s\""
#: ElectronClient/gui/NoteList.min.js:148
#: ElectronClient/gui/NoteList/NoteList.min.js:153
#, fuzzy
msgid "Do it now"
msgstr "Λήψη τώρα:"
msgstr "Κάνε το τώρα"
#: ElectronClient/gui/NoteList.min.js:452
#: ElectronClient/gui/NoteList/NoteList.min.js:425
@ -1870,14 +1873,13 @@ msgid "Viewer"
msgstr "Εμφάνιση"
#: ElectronClient/gui/NoteContentPropertiesDialog.js:107
#, fuzzy
msgid "Statistics"
msgstr "Κατάσταση"
msgstr "Στατιστικά"
#: ElectronClient/gui/NoteContentPropertiesDialog.js:111
#, javascript-format
msgid "Read time: %s min"
msgstr ""
msgstr "Χρόνος ανάγνωσης: %s min"
#: ElectronClient/gui/NoteContentPropertiesDialog.js:112
#: ElectronClient/gui/ShareNoteDialog.js:175
@ -2102,7 +2104,7 @@ msgstr "Έκδοση προφίλ: %s"
#: ElectronClient/app.js:608
#, javascript-format
msgid "Keychain Supported: %s"
msgstr ""
msgstr "Υποστηριζόμενη κλειδοθήκη: %s"
#: ElectronClient/app.js:630 ElectronClient/app.js:706
msgid "&File"
@ -2166,9 +2168,8 @@ msgid "Zoom Out"
msgstr "Σμίκρινση"
#: ElectronClient/app.js:860
#, fuzzy
msgid "&Note"
msgstr "Σημείωση"
msgstr "&Σημείωση"
#: ElectronClient/app.js:870
msgid "&Tools"
@ -2230,7 +2231,6 @@ msgstr ""
"δεδομένα. Δεν πρόκειται να κοινοποιηθούν δεδομένα σε τρίτους."
#: ReactNativeClient/lib/registry.js:156
#, fuzzy
msgid ""
"Could not synchronise with OneDrive.\n"
"\n"
@ -2241,10 +2241,10 @@ msgid ""
msgstr ""
"Δεν ήταν δυνατός ο συγχρονισμός με το OneDrive.\n"
"\n"
"Αυτό το σφάλμα συμβαίνει συχνά όταν χρησιμοποιείτε το OneDrive for Business, "
"το οποίο δυστυχώς δεν υποστηρίζεται.\n"
"Αυτό το σφάλμα συμβαίνει συχνά όταν χρησιμοποιείτε το OneDrive για "
"επιχειρήσεις, το οποίο δυστυχώς δεν μπορεί να υποστηριχθεί.\n"
"\n"
"Παρακαλώ σκεφτείτε να χρησιμοποιήσετε έναν κανονικό λογαριασμό OneDrive."
"Εξετάστε το ενδεχόμενο να χρησιμοποιήσετε έναν κανονικό λογαριασμό OneDrive."
#: ReactNativeClient/lib/logger.js:178
#, javascript-format
@ -2258,7 +2258,7 @@ msgstr "Άγνωστο level ID: %s"
#: ReactNativeClient/lib/SyncTargetAmazonS3.js:28
msgid "AWS S3"
msgstr ""
msgstr "AWS S3"
#: ReactNativeClient/lib/SyncTargetDropbox.js:25
msgid "Dropbox"
@ -2404,15 +2404,15 @@ msgstr "WebDAV password"
#: ReactNativeClient/lib/models/Setting.js:189
msgid "AWS S3 bucket"
msgstr ""
msgstr "AWS S3 bucket"
#: ReactNativeClient/lib/models/Setting.js:200
msgid "AWS key"
msgstr ""
msgstr "AWS key"
#: ReactNativeClient/lib/models/Setting.js:210
msgid "AWS secret"
msgstr ""
msgstr "AWS secret"
#: ReactNativeClient/lib/models/Setting.js:224
msgid "Attachment download behaviour"
@ -2463,15 +2463,15 @@ msgstr "Θέμα"
#: ReactNativeClient/lib/models/Setting.js:315
msgid "Automatically switch theme to match system theme"
msgstr ""
msgstr "Αυτόματη εναλλαγή θέματος ώστε να ταιριάζει με το θέμα συστήματος"
#: ReactNativeClient/lib/models/Setting.js:327
msgid "Preferred light theme"
msgstr ""
msgstr "Προτιμώμενο φωτεινό θέμα"
#: ReactNativeClient/lib/models/Setting.js:341
msgid "Preferred dark theme"
msgstr ""
msgstr "Προτιμώμενο σκοτεινό θέμα"
#: ReactNativeClient/lib/models/Setting.js:346
msgid "Show note counts"
@ -2510,6 +2510,7 @@ msgstr "Αυτόματη-σύζευξη αγκίστρων, παρενθέσεω
#: ReactNativeClient/lib/models/Setting.js:395
msgid "Use CodeMirror as the code editor (WARNING: BETA)."
msgstr ""
"Χρησιμοποιείστε το CodeMirror ως τον επεξεργαστή κώδικα (ΠΡΟΣΟΧΗ: BETA)"
#: ReactNativeClient/lib/models/Setting.js:397
#: ReactNativeClient/lib/models/Setting.js:415
@ -2637,15 +2638,14 @@ msgid "Editor font family"
msgstr "Οικογένεια γραμματοσειράς editor"
#: ReactNativeClient/lib/models/Setting.js:550
#, fuzzy
msgid ""
"This should be a *monospace* font or some elements will render incorrectly. "
"If the font is incorrect or empty, it will default to a generic monospace "
"font."
msgstr ""
"H γραμματοσειρά πρέπει να είναι *monospace* αλλιώς δεν θα λειτουργήσει "
"σωστά. Εάν η γραμματοσειρά είναι εσφαλμένη ή κενή, θα προεπιλεγεί μια γενική "
"γραμματοσειρά *monospace*."
"H γραμματοσειρά πρέπει να είναι *monospace* αλλιώς κάποια στοιχεία δεν θα "
"εμφανιστούν σωστά. Εάν η γραμματοσειρά είναι εσφαλμένη ή κενή, θα "
"προεπιλεγεί μια γενική γραμματοσειρά *monospace*."
#: ReactNativeClient/lib/models/Setting.js:589
msgid "Custom stylesheet for Joplin-wide app styles"
@ -2816,7 +2816,7 @@ msgstr "Διατήρηση ιστορικού σημειώσεων για"
#: ReactNativeClient/lib/models/Setting.js:741
msgid "Notebook list growth factor"
msgstr ""
msgstr "Συντελεστής ανάπτυξης λίστας σημειωματάριων"
#: ReactNativeClient/lib/models/Setting.js:743
#: ReactNativeClient/lib/models/Setting.js:756
@ -2827,14 +2827,19 @@ msgid ""
"item with a factor of 2 will take twice as much space as an item with a "
"factor of 1.Restart app to see changes."
msgstr ""
"Ο συντελεστής ανάπτυξης καθορίζει το πόσο θα αναπτυχθεί ή θα συρρίκνωθεί το "
"αντικειμένο ώστε να χωρά στον διαθέσιμο χώρο σε σχέση με τα άλλα "
"αντικείμενα. Έτσι, ένα στοιχείο με συντελεστή 2 θα πάρει διπλάσιο χώρο από "
"ένα στοιχείο με συντελεστή 1. Επανεκκινήστε την εφαρμογή για να δείτε "
"αλλαγές."
#: ReactNativeClient/lib/models/Setting.js:754
msgid "Note list growth factor"
msgstr ""
msgstr "Συντελεστής ανάπτυξης λίστας σημειώσεων"
#: ReactNativeClient/lib/models/Setting.js:767
msgid "Note area growth factor"
msgstr ""
msgstr "Συντελεστής ανάπτυξης περιοχής σημειώσεων"
#: ReactNativeClient/lib/models/Setting.js:919
#, javascript-format
@ -2910,30 +2915,32 @@ msgid "Downloaded"
msgstr "Έχουν ληφθεί"
#: ReactNativeClient/lib/models/Resource.js:371
#, fuzzy, javascript-format
#, javascript-format
msgid "Attachment conflict: \"%s\""
msgstr "Επισυναπτόμενα"
msgstr "Διένεξη συνημμένου: \"%s\""
#: ReactNativeClient/lib/models/Resource.js:372
#, fuzzy, javascript-format
#, javascript-format
msgid ""
"There was a [conflict](%s) on the attachment below.\n"
"\n"
"%s"
msgstr "Παρουσιάστηκε σφάλμα κατά τη λήψη αυτού του συνημμένου:"
msgstr ""
"Υπήρξε μια [διένεξη](%s) στο παρακάτω συνημμένο.\n"
"\n"
"%s"
#: ReactNativeClient/lib/models/Tag.js:384
#, fuzzy
msgid "Cannot move tag to this location."
msgstr "Δεν είναι δυνατή η μετακίνηση του σημειωματάριου σε αυτήν τη θέση"
msgstr "Δεν είναι δυνατή η μετακίνηση της ετικέτας σε αυτήν τη θέση."
#: ReactNativeClient/lib/models/Tag.js:429
msgid "Tag name cannot start or end with a `/`."
msgstr ""
msgstr "Το όνομα της ετικέτας δεν μπορεί να ξεκινάει ή να τελειώνει με `/`."
#: ReactNativeClient/lib/models/Tag.js:431
msgid "Tag name cannot contain `//`."
msgstr ""
msgstr "Το όνομα της ετικέτας δεν μπορεί να περιέχει `//`."
#: ReactNativeClient/lib/models/Tag.js:482
#, javascript-format
@ -2946,7 +2953,7 @@ msgstr "ημερομηνία δημιουργίας"
#: ReactNativeClient/lib/models/Note.js:28
msgid "custom order"
msgstr ""
msgstr "προσαρμοσμένη σειρά"
#: ReactNativeClient/lib/models/Note.js:92
msgid "This note does not have geolocation information."
@ -3564,12 +3571,11 @@ msgstr "File system"
#: ReactNativeClient/lib/commands/historyForward.js:16
msgid "Forward"
msgstr ""
msgstr "Μπροστά"
#: ReactNativeClient/lib/commands/synchronize.js:17
#, fuzzy
msgid "Synchronize"
msgstr "Συγχρονισμός"
msgstr "Συγχρόνισε"
#: ReactNativeClient/lib/services/InteropService_Exporter_Jex.js:29
msgid "There is no data to export."
@ -3737,9 +3743,10 @@ msgid "Directory"
msgstr "Φάκελος"
#: ReactNativeClient/lib/services/InteropService.js:174
#, fuzzy, javascript-format
#, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\" and output \"%s\""
msgstr "Δεν είναι δυνατή η φόρτωση του module \"%s\" για τη μορφή \"%s\""
msgstr ""
"Δεν είναι δυνατή η φόρτωση του \"%s\" module με μορφή \"%s\" και έξοδο \"%s\""
#: ReactNativeClient/lib/services/InteropService.js:232
#, javascript-format

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,10 @@ require('app-module-path').addPath(__dirname);
const filterParser = require('lib/services/searchengine/filterParser.js').default;
// import filterParser from 'lib/services/searchengine/filterParser.js';
const makeTerm = (name, value, negated, quoted = false) => {
if (name !== 'text') { return { name, value, negated }; } else { return { name, value, negated, quoted }; }
const makeTerm = (name, value, negated, quoted = false, wildcard = false) => {
if (name === 'text') { return { name, value, negated, quoted, wildcard }; }
if (name === 'title' | name === 'body') { return { name, value, negated, wildcard }; }
return { name, value, negated };
};
describe('filterParser should be correct filter for keyword', () => {
@ -108,6 +110,9 @@ describe('filterParser should be correct filter for keyword', () => {
searchString = 'tag:bl*sphemy';
expect(filterParser(searchString)).toContain(makeTerm('tag', 'bl%sphemy', false));
searchString = 'tag:"space travel"';
expect(filterParser(searchString)).toContain(makeTerm('tag', 'space travel', false));
});
it('wildcard notebooks', () => {

View File

@ -268,7 +268,7 @@ describe('services_SearchFilter', function() {
await Tag.setNoteTagsByTitles(n1.id, ['tag1', 'tag2']);
await Tag.setNoteTagsByTitles(n2.id, ['tag2', 'tag3']);
await Tag.setNoteTagsByTitles(n3.id, ['tag3', 'tag4']);
await Tag.setNoteTagsByTitles(n3.id, ['tag3', 'tag4', 'space travel']);
await engine.syncTables();
@ -304,6 +304,10 @@ describe('services_SearchFilter', function() {
expect(rows.length).toBe(2);
expect(ids(rows)).toContain(n1.id);
expect(ids(rows)).toContain(n3.id);
rows = await engine.search('tag:"space travel"');
expect(rows.length).toBe(1);
expect(ids(rows)).toContain(n3.id);
}));
it('should support filtering by notebook', asyncTest(async () => {

View File

@ -141,5 +141,23 @@ describe('services_SearchFuzzy', function() {
expect(rows.map(r=>r.id)).toContain(n5.id);
}));
it('should leave wild card searches alone', asyncTest(async () => {
let rows;
const n1 = await Note.save({ title: 'abc def' });
const n2 = await Note.save({ title: 'abcc ghi' });
const n3 = await Note.save({ title: 'abccc ghi' });
const n4 = await Note.save({ title: 'abcccc ghi' });
const n5 = await Note.save({ title: 'wxy zzz' });
await engine.syncTables();
rows = await engine.search('abc*', { fuzzy: true });
expect(rows.length).toBe(4);
expect(rows.map(r=>r.id)).toContain(n1.id);
expect(rows.map(r=>r.id)).toContain(n2.id);
expect(rows.map(r=>r.id)).toContain(n3.id);
expect(rows.map(r=>r.id)).toContain(n4.id);
}));
});

View File

@ -1,5 +1,6 @@
import LockHandler from 'lib/services/synchronizer/LockHandler';
import MigrationHandler from 'lib/services/synchronizer/MigrationHandler';
import { Dirnames } from 'lib/services/synchronizer/utils/types';
// To create a sync target snapshot for the current syncVersion:
// - In test-utils, set syncTargetName_ to "filesystem"
@ -70,6 +71,14 @@ describe('synchronizer_MigrationHandler', function() {
done();
});
it('should init a new sync target', asyncTest(async () => {
// Check that basic folders "locks" and "temp" are created for new sync targets.
await migrationHandler().upgrade(1);
const result = await fileApi().list();
expect(result.items.filter((i:any) => i.path === Dirnames.Locks).length).toBe(1);
expect(result.items.filter((i:any) => i.path === Dirnames.Temp).length).toBe(1);
}), specTimeout);
it('should not allow syncing if the sync target is out-dated', asyncTest(async () => {
await synchronizer().start();
await fileApi().put('info.json', `{"version":${Setting.value('syncVersion') - 1}}`);

View File

@ -50,6 +50,7 @@ const KeychainServiceDriver = require('lib/services/keychain/KeychainServiceDriv
const KeychainServiceDriverDummy = require('lib/services/keychain/KeychainServiceDriver.dummy').default;
const md5 = require('md5');
const S3 = require('aws-sdk/clients/s3');
const { Dirnames } = require('lib/services/synchronizer/utils/types');
const databases_ = [];
let synchronizers_ = [];
@ -438,6 +439,7 @@ async function initFileApi() {
fileApi.setLogger(logger);
fileApi.setSyncTargetId(syncTargetId_);
fileApi.setTempDirName(Dirnames.Temp);
fileApi.requestRepeatCount_ = isNetworkSyncTarget_ ? 1 : 0;
fileApis_[syncTargetId_] = fileApi;

View File

@ -105,7 +105,17 @@ class ElectronAppWrapper {
// Waiting for one of the ready events might work but they might not be triggered if there's an error, so
// the easiest is to use a timeout. Keep in mind that if you get a white window on Windows it might be due
// to this line though.
if (debugEarlyBugs) setTimeout(() => this.win_.webContents.openDevTools(), 3000);
if (debugEarlyBugs) {
setTimeout(() => {
try {
this.win_.webContents.openDevTools();
} catch (error) {
// This will throw an exception "Object has been destroyed" if the app is closed
// in less that the timeout interval. It can be ignored.
console.warn('Error opening dev tools', error);
}
}, 3000);
}
this.win_.on('close', (event) => {
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)

View File

@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "1.1.244",
"version": "1.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "1.1.244",
"version": "1.1.0",
"description": "Joplin for Desktop",
"main": "main.js",
"scripts": {

View File

@ -177,8 +177,9 @@ class Dialog extends React.PureComponent {
return output.join(' ');
}
keywords() {
return this.props.highlightedWords;
async keywords(searchQuery) {
const parsedQuery = await SearchEngine.instance().parseQuery(searchQuery, false);
return SearchEngine.instance().allParsedQueryTerms(parsedQuery);
}
markupToHtml() {
@ -226,7 +227,7 @@ class Dialog extends React.PureComponent {
}
} else {
const limit = 20;
const searchKeywords = this.keywords();
const searchKeywords = await this.keywords(searchQuery);
const notes = await Note.byIds(results.map(result => result.id).slice(0, limit), { fields: ['id', 'body', 'markup_language', 'is_todo', 'todo_completed'] });
const notesById = notes.reduce((obj, { id, body, markup_language }) => ((obj[[id]] = { id, body, markup_language }), obj), {});
@ -282,7 +283,7 @@ class Dialog extends React.PureComponent {
this.setState({
listType: listType,
results: results,
keywords: this.keywords(),
keywords: await this.keywords(searchQuery),
selectedItemId: results.length === 0 ? null : results[0].id,
resultsInBody: resultsInBody,
});

View File

@ -44,6 +44,9 @@ const KvStore = require('lib/services/KvStore');
const MigrationService = require('lib/services/MigrationService');
const { toSystemSlashes } = require('lib/path-utils.js');
// const ntpClient = require('lib/vendor/ntp-client');
// ntpClient.dgram = require('dgram');
class BaseApplication {
constructor() {
this.logger_ = new Logger();
@ -673,6 +676,7 @@ class BaseApplication {
reg.dispatch = () => {};
BaseService.logger_ = this.logger_;
// require('lib/ntpDate').setLogger(reg.logger());
this.dbLogger_.addTarget('file', { path: `${profileDir}/log-database.txt` });
this.dbLogger_.setLevel(initArgs.logLevel);

View File

@ -44,7 +44,7 @@ class FileApiDriverDropbox {
metadataToStat_(md, path) {
const output = {
path: path,
updated_time: md.server_modified ? new Date(md.server_modified) : new Date(),
updated_time: md.server_modified ? (new Date(md.server_modified)).getTime() : Date.now(),
isDir: md['.tag'] === 'folder',
};

View File

@ -6,6 +6,7 @@ const JoplinError = require('lib/JoplinError');
const ArrayUtils = require('lib/ArrayUtils');
const { time } = require('lib/time-utils.js');
const { sprintf } = require('sprintf-js');
const Mutex = require('async-mutex').Mutex;
function requestCanBeRepeated(error) {
const errorCode = typeof error === 'object' && error.code ? error.code : null;
@ -57,6 +58,65 @@ class FileApi {
this.tempDirName_ = null;
this.driver_.fileApi_ = this;
this.requestRepeatCount_ = null; // For testing purpose only - normally this value should come from the driver
this.remoteDateOffset_ = 0;
this.remoteDateNextCheckTime_ = 0;
this.remoteDateMutex_ = new Mutex();
}
async fetchRemoteDateOffset_() {
const tempFile = `${this.tempDirName()}/timeCheck${Math.round(Math.random() * 1000000)}.txt`;
const startTime = Date.now();
await this.put(tempFile, 'timeCheck');
// Normally it should be possible to read the file back immediately but
// just in case, read it in a loop.
const loopStartTime = Date.now();
let stat = null;
while (Date.now() - loopStartTime < 5000) {
stat = await this.stat(tempFile);
if (stat) break;
await time.msleep(200);
}
if (!stat) throw new Error('Timed out trying to get sync target clock time');
this.delete(tempFile); // No need to await for this call
const endTime = Date.now();
const expectedTime = Math.round((endTime + startTime) / 2);
return stat.updated_time - expectedTime;
}
// Approximates the current time on the sync target. It caches the time offset to
// improve performance.
async remoteDate() {
const shouldSyncTime = () => {
return !this.remoteDateNextCheckTime_ || Date.now() > this.remoteDateNextCheckTime_;
};
if (shouldSyncTime()) {
const release = await this.remoteDateMutex_.acquire();
try {
// Another call might have refreshed the time while we were waiting for the mutex,
// so check again if we need to refresh.
if (shouldSyncTime()) {
this.remoteDateOffset_ = await this.fetchRemoteDateOffset_();
// The sync target clock should rarely change but the device one might,
// so we need to refresh relatively frequently.
this.remoteDateNextCheckTime_ = Date.now() + 10 * 60 * 1000;
}
} catch (error) {
this.logger().warn('Could not retrieve remote date - defaulting to device date:', error);
this.remoteDateOffset_ = 0;
this.remoteDateNextCheckTime_ = Date.now() + 60 * 1000;
} finally {
release();
}
}
return new Date(Date.now() + this.remoteDateOffset_);
}
// Ideally all requests repeating should be done at the FileApi level to remove duplicate code in the drivers, but

View File

@ -422,7 +422,7 @@ class Setting extends BaseModel {
value: false,
type: Setting.TYPE_BOOL,
section: 'note',
public: true,
public: mobilePlatform === 'ios',
appTypes: ['mobile'],
label: () => 'Opt-in to the editor beta',
description: () => 'This beta adds list continuation, Markdown preview, and Markdown shortcuts. If you find bugs, please report them in the Discourse forum.',

View File

@ -0,0 +1,59 @@
const ntpClient = require('lib/vendor/ntp-client');
const { Logger } = require('lib/logger.js');
const Mutex = require('async-mutex').Mutex;
let nextSyncTime = 0;
let timeOffset = 0;
let logger = new Logger();
const fetchingTimeMutex = new Mutex();
const server = {
domain: 'pool.ntp.org',
port: 123,
};
async function networkTime():Promise<Date> {
return new Promise(function(resolve:Function, reject:Function) {
ntpClient.getNetworkTime(server.domain, server.port, function(error:any, date:Date) {
if (error) {
reject(error);
return;
}
resolve(date);
});
});
}
function shouldSyncTime() {
return !nextSyncTime || Date.now() > nextSyncTime;
}
export function setLogger(v:any) {
logger = v;
}
export default async function():Promise<Date> {
if (shouldSyncTime()) {
const release = await fetchingTimeMutex.acquire();
try {
if (shouldSyncTime()) {
const date = await networkTime();
nextSyncTime = Date.now() + 60 * 1000;
timeOffset = date.getTime() - Date.now();
}
} catch (error) {
logger.warn('Could not get NTP time - falling back to device time:', error);
// Fallback to device time since
// most of the time it's actually correct
nextSyncTime = Date.now() + 20 * 1000;
timeOffset = 0;
} finally {
release();
}
}
return new Date(Date.now() + timeOffset);
}

View File

@ -434,7 +434,6 @@ class SearchEngine {
}
async parseQuery(query, fuzzy = false) {
// fuzzy = false;
const trimQuotes = (str) => str.startsWith('"') ? str.substr(1, str.length - 2) : str;
let allTerms = [];
@ -453,18 +452,22 @@ class SearchEngine {
const fuzzyScore = [];
let numFuzzyMatches = [];
let terms = null;
if (fuzzy) {
const fuzzyText = await this.fuzzifier(textTerms.filter(x => !x.quoted).map(x => trimQuotes(x.value)));
const fuzzyTitle = await this.fuzzifier(titleTerms.map(x => trimQuotes(x.value)));
const fuzzyBody = await this.fuzzifier(bodyTerms.map(x => trimQuotes(x.value)));
const phraseSearches = textTerms.filter(x => x.quoted).map(x => x.value);
// Save number of matches we got for each word
// fuzzifier() is currently set to return at most 3 matches)
if (fuzzy) {
const fuzzyText = await this.fuzzifier(textTerms.filter(x => !(x.quoted || x.wildcard)).map(x => trimQuotes(x.value)));
const fuzzyTitle = await this.fuzzifier(titleTerms.filter(x => !x.wildcard).map(x => trimQuotes(x.value)));
const fuzzyBody = await this.fuzzifier(bodyTerms.filter(x => !x.wildcard).map(x => trimQuotes(x.value)));
const phraseTextSearch = textTerms.filter(x => x.quoted);
const wildCardSearch = textTerms.concat(titleTerms).concat(bodyTerms).filter(x => x.wildcard);
// Save number of fuzzy matches we got for each word
// fuzzifier() is currently set to return at most 3 matches
// We need to know which fuzzy words go together so that we can filter out notes that don't contain a required word.
numFuzzyMatches = fuzzyText.concat(fuzzyTitle).concat(fuzzyBody).map(x => x.length);
for (let i = 0; i < phraseSearches.length; i++) {
numFuzzyMatches.push(1); // Phrase searches are preserved without fuzzification
for (let i = 0; i < phraseTextSearch.length + wildCardSearch.length; i++) {
// Phrase searches and wildcard searches are preserved without fuzzification (A single match)
numFuzzyMatches.push(1);
}
const mergedFuzzyText = [].concat.apply([], fuzzyText);
@ -474,18 +477,33 @@ class SearchEngine {
const fuzzyTextTerms = mergedFuzzyText.map(x => { return { name: 'text', value: x.word, negated: false, score: x.score }; });
const fuzzyTitleTerms = mergedFuzzyTitle.map(x => { return { name: 'title', value: x.word, negated: false, score: x.score }; });
const fuzzyBodyTerms = mergedFuzzyBody.map(x => { return { name: 'body', value: x.word, negated: false, score: x.score }; });
const phraseTextTerms = phraseSearches.map(x => { return { name: 'text', value: x, negated: false, score: 0 }; });
// Remove previous text, title and body and replace with fuzzy versions
allTerms = allTerms.filter(x => (x.name !== 'text' && x.name !== 'title' && x.name !== 'body'));
allFuzzyTerms = allTerms.concat(fuzzyTextTerms).concat(fuzzyTitleTerms).concat(fuzzyBodyTerms).concat(phraseTextTerms);
// The order matters here!
// The text goes first, then title, then body, then phrase and finally wildcard
// This is because it needs to match with numFuzzyMathches.
allFuzzyTerms = allTerms.concat(fuzzyTextTerms).concat(fuzzyTitleTerms).concat(fuzzyBodyTerms).concat(phraseTextSearch).concat(wildCardSearch);
const allTextTerms = allFuzzyTerms.filter(x => x.name === 'title' || x.name === 'body' || x.name === 'text');
for (let i = 0; i < allTextTerms.length; i++) {
// Phrase searches and wildcard searches will get a fuzziness score of zero.
// This means that they will go first in the sort order (Even if there are other words with matches in the title)
// Undesirable?
fuzzyScore.push(allFuzzyTerms[i].score ? allFuzzyTerms[i].score : 0);
}
terms = { _: fuzzyTextTerms.concat(phraseTextTerms).map(x =>trimQuotes(x.value)), 'title': fuzzyTitleTerms.map(x =>trimQuotes(x.value)), 'body': fuzzyBodyTerms.map(x =>trimQuotes(x.value)) };
const wildCardTextTerms = wildCardSearch.filter(x => x.name === 'text').map(x =>trimQuotes(x.value));
const wildCardTitleTerms = wildCardSearch.filter(x => x.name === 'title').map(x =>trimQuotes(x.value));
const wildCardBodyTerms = wildCardSearch.filter(x => x.name === 'body').map(x =>trimQuotes(x.value));
const phraseTextTerms = phraseTextSearch.map(x => trimQuotes(x.value));
terms = {
_: fuzzyTextTerms.map(x => trimQuotes(x.value)).concat(phraseTextTerms).concat(wildCardTextTerms),
title: fuzzyTitleTerms.map(x => trimQuotes(x.value)).concat(wildCardTitleTerms),
body: fuzzyBodyTerms.map(x => trimQuotes(x.value)).concat(wildCardBodyTerms),
};
} else {
const nonNegatedTextTerms = textTerms.length + titleTerms.length + bodyTerms.length;
for (let i = 0; i < nonNegatedTextTerms; i++) {

View File

@ -4,6 +4,7 @@ interface Term {
value: string
negated: boolean
quoted?: boolean
wildcard?: boolean
}
const makeTerm = (name: string, value: string): Term => {
@ -82,13 +83,13 @@ const parseQuery = (query: string): Term[] => {
}
if (name === 'tag' || name === 'notebook' || name === 'resource' || name === 'sourceurl') {
result.push({ name, value: value.replace(/[*]/g, '%'), negated }); // for wildcard search
result.push({ name, value: trimQuotes(value.replace(/[*]/g, '%')), negated }); // for wildcard search
} else if (name === 'title' || name === 'body') {
// Trim quotes since we don't support phrase query here
// eg. Split title:"hello world" to title:hello title:world
const values = trimQuotes(value).split(/[\s-_]+/);
values.forEach(value => {
result.push({ name, value, negated });
result.push({ name, value, negated, wildcard: value.indexOf('*') >= 0 });
});
} else {
result.push({ name, value, negated });
@ -97,9 +98,21 @@ const parseQuery = (query: string): Term[] => {
// Every word is quoted if not already.
// By quoting the word, FTS match query will take care of removing dashes and other word seperators.
if (value.startsWith('-')) {
result.push({ name: 'text', value: quote(value.slice(1)) , negated: true, quoted: quoted(value) });
result.push({
name: 'text',
value: quote(value.slice(1)),
negated: true,
quoted: quoted(value),
wildcard: value.indexOf('*') >= 0,
});
} else {
result.push({ name: 'text', value: quote(value), negated: false, quoted: quoted(value) });
result.push({
name: 'text',
value: quote(value),
negated: false,
quoted: quoted(value),
wildcard: value.indexOf('*') >= 0,
});
}
}
}

View File

@ -1,4 +1,5 @@
import { Dirnames } from './utils/types';
const JoplinError = require('lib/JoplinError');
const { time } = require('lib/time-utils');
const { fileExtension, filename } = require('lib/path-utils.js');
@ -98,8 +99,8 @@ export default class LockHandler {
return output;
}
private lockIsActive(lock:Lock):boolean {
return Date.now() - lock.updatedTime < this.lockTtl;
private lockIsActive(lock:Lock, currentDate:Date):boolean {
return currentDate.getTime() - lock.updatedTime < this.lockTtl;
}
async hasActiveLock(lockType:LockType, clientType:string = null, clientId:string = null) {
@ -112,11 +113,12 @@ export default class LockHandler {
// of that type instead.
async activeLock(lockType:LockType, clientType:string = null, clientId:string = null) {
const locks = await this.locks(lockType);
const currentDate = await this.api_.remoteDate();
if (lockType === LockType.Exclusive) {
const activeLocks = locks
.slice()
.filter((lock:Lock) => this.lockIsActive(lock))
.filter((lock:Lock) => this.lockIsActive(lock, currentDate))
.sort((a:Lock, b:Lock) => {
if (a.updatedTime === b.updatedTime) {
return a.clientId < b.clientId ? -1 : +1;
@ -134,7 +136,7 @@ export default class LockHandler {
for (const lock of locks) {
if (clientType && lock.clientType !== clientType) continue;
if (clientId && lock.clientId !== clientId) continue;
if (this.lockIsActive(lock)) return lock;
if (this.lockIsActive(lock, currentDate)) return lock;
}
return null;
}
@ -290,6 +292,12 @@ export default class LockHandler {
if (!this.refreshTimers_[handle]) return defer(); // Timeout has been cleared
if (!hasActiveLock) {
// If the previous lock has expired, we shouldn't try to acquire a new one. This is because other clients might have performed
// in the meantime operations that invalidates the current operation. For example, another client might have upgraded the
// sync target in the meantime, so any active operation should be cancelled here. Or if the current client was upgraded
// the sync target, another client might have synced since then, making any cached data invalid.
// In some cases it should be safe to re-acquire a lock but adding support for this would make the algorithm more complex
// without much benefits.
error = new JoplinError('Lock has expired', 'lockExpired');
} else {
try {

View File

@ -90,9 +90,11 @@ export default class MigrationHandler extends BaseService {
// it the lock handler will break. So we create the directory now.
// Also if the sync target version is 0, it means it's a new one so we need the
// lock folder first before doing anything else.
// Temp folder is needed too to get remoteDate() call to work.
if (syncTargetInfo.version === 0 || syncTargetInfo.version === 1) {
this.logger().info('MigrationHandler: Sync target version is 0 or 1 - creating "locks" directory:', syncTargetInfo);
this.logger().info('MigrationHandler: Sync target version is 0 or 1 - creating "locks" and "temp" directory:', syncTargetInfo);
await this.api_.mkdir(Dirnames.Locks);
await this.api_.mkdir(Dirnames.Temp);
}
this.logger().info('MigrationHandler: Acquiring exclusive lock');

View File

@ -305,6 +305,8 @@ class Synchronizer {
let syncLock = null;
try {
this.api().setTempDirName(Dirnames.Temp);
try {
const syncTargetInfo = await this.migrationHandler().checkCanSync();
@ -321,8 +323,6 @@ class Synchronizer {
throw error;
}
this.api().setTempDirName(Dirnames.Temp);
syncLock = await this.lockHandler().acquireLock('sync', this.appType_, this.clientId_);
this.lockHandler().startAutoLockRefresh(syncLock, (error) => {

View File

@ -0,0 +1,155 @@
/*
* ntp-client
* https://github.com/moonpyk/node-ntp-client
*
* Copyright (c) 2013 Clément Bourgeois
* Licensed under the MIT license.
*/
// ----------------------------------------------------------------------------------------
// 2020-08-09: We vendor the package because although it works
// it has several bugs and is currently unmaintained
// ----------------------------------------------------------------------------------------
const Buffer = require('buffer').Buffer;
(function (exports) {
"use strict";
exports.defaultNtpPort = 123;
exports.defaultNtpServer = "pool.ntp.org";
exports.dgram = null;
/**
* Amount of acceptable time to await for a response from the remote server.
* Configured default to 10 seconds.
*/
exports.ntpReplyTimeout = 10000;
/**
* Fetches the current NTP Time from the given server and port.
* @param {string} server IP/Hostname of the remote NTP Server
* @param {number} port Remote NTP Server port number
* @param {function(Object, Date)} callback(err, date) Async callback for
* the result date or eventually error.
*/
exports.getNetworkTime = function (server, port, callback) {
if (callback === null || typeof callback !== "function") {
return;
}
server = server || exports.defaultNtpServer;
port = port || exports.defaultNtpPort;
if (!exports.dgram) throw new Error('dgram package has not been set!!');
var client = exports.dgram.createSocket("udp4"),
ntpData = new Buffer(48);
// RFC 2030 -> LI = 0 (no warning, 2 bits), VN = 3 (IPv4 only, 3 bits), Mode = 3 (Client Mode, 3 bits) -> 1 byte
// -> rtol(LI, 6) ^ rotl(VN, 3) ^ rotl(Mode, 0)
// -> = 0x00 ^ 0x18 ^ 0x03
ntpData[0] = 0x1B;
for (var i = 1; i < 48; i++) {
ntpData[i] = 0;
}
// Some errors can happen before/after send() or cause send() to was impossible.
// Some errors will also be given to the send() callback.
// We keep a flag, therefore, to prevent multiple callbacks.
// NOTE : the error callback is not generalised, as the client has to lose the connection also, apparently.
var errorFired = false;
function closeClient(client) {
try {
client.close();
} catch (error) {
// Doesn't mater if it could not be closed
}
}
var timeout = setTimeout(function () {
closeClient(client);
if (errorFired) {
return;
}
callback(new Error("Timeout waiting for NTP response."), null);
errorFired = true;
}, exports.ntpReplyTimeout);
client.on('error', function (err) {
clearTimeout(timeout);
if (errorFired) {
return;
}
callback(err, null);
errorFired = true;
});
// NOTE: To make it work in React Native (Android), a port need to be bound
// before calling client.send()
// client.bind(5555, '0.0.0.0', function() {
client.send(ntpData, 0, ntpData.length, port, server, function (err) {
if (err) {
clearTimeout(timeout);
if (errorFired) {
return;
}
callback(err, null);
errorFired = true;
closeClient(client);
return;
}
client.once('message', function (msg) {
clearTimeout(timeout);
closeClient(client);
// Offset to get to the "Transmit Timestamp" field (time at which the reply
// departed the server for the client, in 64-bit timestamp format."
var offsetTransmitTime = 40,
intpart = 0,
fractpart = 0;
// Get the seconds part
for (var i = 0; i <= 3; i++) {
intpart = 256 * intpart + msg[offsetTransmitTime + i];
}
// Get the seconds fraction
for (i = 4; i <= 7; i++) {
fractpart = 256 * fractpart + msg[offsetTransmitTime + i];
}
var milliseconds = (intpart * 1000 + (fractpart * 1000) / 0x100000000);
// **UTC** time
var date = new Date("Jan 01 1900 GMT");
date.setUTCMilliseconds(date.getUTCMilliseconds() + milliseconds);
callback(null, date);
});
});
// });
};
exports.demo = function (argv) {
exports.getNetworkTime(
exports.defaultNtpServer,
exports.defaultNtpPort,
function (err, date) {
if (err) {
console.error(err);
return;
}
console.log(date);
});
};
}(exports));

View File

@ -401,6 +401,7 @@ async function initialize(dispatch, messageHandler) {
reg.setShowErrorMessageBoxHandler((message) => { alert(message); });
BaseService.logger_ = mainLogger;
// require('lib/ntpDate').setLogger(reg.logger());
reg.logger().info('====================================');
reg.logger().info(`Starting application ${Setting.value('appId')} (${Setting.value('env')})`);

View File

@ -13,6 +13,10 @@ For example, if a client is currently syncing, it must stop doing so if it could
For example, if a client is upgrading a target, it must stop doing so if it couldn't refresh the lock within Y seconds.
If the previous lock has expired, we shouldn't try to acquire a new one. This is because other clients, seeing no active lock, might have performed in the meantime operations that invalidates the current operation. For example, another client might have upgraded the sync target, so any active sync with an expired lock should be cancelled. Or if the current client was upgrading the sync target, another client might have synced since then, making any cached data invalid.
In some cases it should be safe to re-acquire a lock but adding support for this would make the algorithm more complex without much benefits.
# Acquiring a SYNC lock
- The client check if there is a valid EXCLUSIVE lock on the target