1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-02-28 09:22:25 +02:00

Compare commits

...

7 Commits

17 changed files with 162 additions and 42 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "3.5.12",
"version": "3.5.13",
"description": "Joplin for Desktop",
"main": "main.bundle.js",
"private": true,

View File

@@ -364,4 +364,17 @@ describe('screens/Note', () => {
unmount();
});
it('should set the initial editor cursor location to the specified hash', async () => {
await openNewNote({ title: 'To be edited', body: 'a test\n\n# Test\n\n# Test 2\n\n# Test 3' });
store.dispatch({ type: 'NAV_GO', noteHash: 'test-2' });
const { unmount } = render(<WrappedNoteScreen />);
await openEditor();
const editor = await getMarkdownEditorControl();
expect(editor.getCursor().line).toBe(4);
unmount();
});
});

View File

@@ -236,12 +236,16 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
multiline: false,
};
const initialCursorLocation = NotePositionService.instance().getCursorPosition(props.noteId, defaultWindowId).markdown;
if (initialCursorLocation) {
this.selection = { start: initialCursorLocation, end: initialCursorLocation };
}
const initialScroll = NotePositionService.instance().getScrollPercent(props.noteId, defaultWindowId);
this.lastBodyScroll = initialScroll;
const initialCursorLocation = NotePositionService.instance().getCursorPosition(props.noteId, defaultWindowId).markdown;
// Ignore the initial scroll and cursor location when there's a note hash. The editor/viewer should jump to
// the hash, rather than the last position.
if (!props.noteHash) {
if (initialCursorLocation) {
this.selection = { start: initialCursorLocation, end: initialCursorLocation };
}
this.lastBodyScroll = initialScroll;
}
this.titleTextFieldRef = React.createRef();

View File

@@ -870,7 +870,7 @@ export default class Synchronizer {
let context = null;
let newDeltaContext = null;
const localFoldersToDelete = [];
const localFoldersToDelete = new Set<string>();
let hasCancelled = false;
if (lastContext.delta) context = lastContext.delta;
@@ -972,6 +972,8 @@ export default class Synchronizer {
reason = 'remote exists but local does not';
content = await loadContent();
ItemClass = content ? BaseItem.itemClass(content) : null;
} else {
reason = 'skipping: the item was deleted';
}
} else {
ItemClass = BaseItem.itemClass(local);
@@ -980,6 +982,11 @@ export default class Synchronizer {
action = SyncAction.DeleteLocal;
reason = 'remote has been deleted';
} else {
if (localFoldersToDelete.has(remoteId)) {
logger.debug('Removing a scheduled folder deletion (', remoteId, '). It was recreated by sync.');
localFoldersToDelete.delete(remoteId);
}
if (this.api().supportsAccurateTimestamp && remote.jop_updated_time === local.updated_time) {
// Nothing to do, and no need to fetch the content
} else {
@@ -1066,7 +1073,12 @@ export default class Synchronizer {
await MasterKey.save(content);
}
} else {
await ItemClass.save(content, options);
const saved = await ItemClass.save(content, options);
// Ensure that the item can be found if another create/update event is received for the same item:
if (!local) {
locals.push(saved);
}
}
if (creatingOrUpdatingResource) this.dispatch({ type: 'SYNC_CREATED_OR_UPDATED_RESOURCE', id: content.id });
@@ -1083,7 +1095,7 @@ export default class Synchronizer {
if (content.encryption_applied) this.dispatch({ type: 'SYNC_GOT_ENCRYPTED_ITEM' });
} else if (action === SyncAction.DeleteLocal) {
if (local.type_ === BaseModel.TYPE_FOLDER) {
localFoldersToDelete.push(local);
localFoldersToDelete.add(local.id);
continue;
}
@@ -1133,12 +1145,12 @@ export default class Synchronizer {
// ------------------------------------------------------------------------
if (!this.cancelling()) {
for (let i = 0; i < localFoldersToDelete.length; i++) {
const item = localFoldersToDelete[i];
const noteIds = await Folder.noteIds(item.id);
for (const folderId of localFoldersToDelete) {
const noteIds = await Folder.noteIds(folderId);
if (noteIds.length) {
logger.warn('Conflict: Folder to be deleted', folderId, 'still contains notes', noteIds);
// CONFLICT
await Folder.markNotesAsConflict(item.id);
await Folder.markNotesAsConflict(folderId);
}
const deletionOptions: DeleteOptions = {
@@ -1147,7 +1159,7 @@ export default class Synchronizer {
changeSource: ItemChange.SOURCE_SYNC,
sourceDescription: 'Sync',
};
await Folder.delete(item.id, deletionOptions);
await Folder.delete(folderId, deletionOptions);
}
}

View File

@@ -94,7 +94,7 @@
"sqlite3": "5.1.6",
"string-padding": "1.0.2",
"string-to-stream": "3.0.1",
"tar": "6.2.1",
"tar": "7.5.8",
"tcp-port-used": "1.0.2",
"uglifycss": "0.0.29",
"url-parse": "1.5.10",

View File

@@ -2,7 +2,7 @@ import { ImportExportResult, ImportModuleOutputFormat, ImportOptions } from './t
import InteropService_Importer_Base from './InteropService_Importer_Base';
import { NoteEntity } from '../database/types';
import { rtrimSlashes } from '../../path-utils';
import { friendlySafeFilename, rtrimSlashes } from '../../path-utils';
import InteropService_Importer_Md from './InteropService_Importer_Md';
import { join, resolve, normalize, sep, dirname, extname, basename, relative } from 'path';
import Logger from '@joplin/utils/Logger';
@@ -337,15 +337,15 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
const originalPath = join(basePath, fileName);
let newPath;
let fixedFileName = Buffer.from(fileName, 'latin1').toString('utf8');
if (fixedFileName !== fileName) {
// In general, the path shouldn't start with "."s or contain path separators.
// However, if it does, these characters might cause import errors, so remove them:
fixedFileName = fixedFileName.replace(/^\.+/, '');
fixedFileName = fixedFileName.replace(/[/\\]/g, ' ');
// Avoid path traversal: Ensure that the file path is contained within the base directory
const newFullPathSafe = shim.fsDriver().resolveRelativePathWithinDir(basePath, fixedFileName);
const fixedFileName = Buffer.from(fileName, 'latin1').toString('utf8');
// If the filename includes the Unicode replacement character, file name correction has failed.
// Use the original (incorrect) filename in that case:
const replacementCharacter = '\uFFFD';
if (fixedFileName !== fileName && !fixedFileName.includes(replacementCharacter)) {
const newFullPathSafe = shim.fsDriver().resolveRelativePathWithinDir(
basePath,
friendlySafeFilename(fixedFileName, 128, true),
);
await shim.fsDriver().move(originalPath, newFullPathSafe);
newPath = newFullPathSafe;

View File

@@ -392,6 +392,7 @@ dependencies = [
"lazy_static",
"parser-macros",
"paste",
"sanitize-filename",
"thiserror",
"uuid",
"wasm-bindgen",
@@ -569,7 +570,6 @@ dependencies = [
"paste",
"percent-encoding",
"regex",
"sanitize-filename",
"thiserror",
"uuid",
"wasm-bindgen",

View File

@@ -11,6 +11,7 @@ widestring = "1.0.2"
uuid = "1.1.2"
lazy_static = "1.4"
wasm-bindgen = "0.2"
sanitize-filename = "0.3.0"
parser-macros = { path = "../parser-macros" }
[dependencies.web-sys]

View File

@@ -28,10 +28,15 @@ function normalizeAndWriteFile(filePath, data) {
fs.writeFileSync(filePath, data);
}
function isWindows() {
return process.platform === 'win32';
}
module.exports = {
mkdirSyncRecursive,
isDirectory,
readDir,
removePrefix,
normalizeAndWriteFile,
isWindows,
};

View File

@@ -1,6 +1,9 @@
use sanitize_filename::{ sanitize_with_options, Options as SanitizeOptions };
pub type ApiResult<T> = std::result::Result<T, std::io::Error>;
pub trait FileApiDriver: Send + Sync {
fn is_windows(&self) -> bool;
fn is_directory(&self, path: &str) -> ApiResult<bool>;
fn read_dir(&self, path: &str) -> ApiResult<Vec<String>>;
fn read_file(&self, path: &str) -> ApiResult<Vec<u8>>;
@@ -19,6 +22,21 @@ pub trait FileApiDriver: Send + Sync {
/// `path_2` is still appended to `path_1`.
fn join(&self, path_1: &str, path_2: &str) -> String;
fn sanitize_file_name(&self, file_name: &str) -> String {
sanitize_with_options(
file_name.trim(),
SanitizeOptions {
// Override "windows". By default, sanitize_filename can
// incorrectly detect the host OS when compiled to WASM.
windows: self.is_windows(),
// Otherwise, match the default sanitize_filename options:
truncate: true,
replacement: "",
},
)
}
/// Splits filename into (base, extension).
fn split_file_name(&self, filename: &str) -> (String, String) {
let ext = self.get_file_extension(filename);

View File

@@ -21,3 +21,32 @@ lazy_static! {
pub fn fs_driver() -> Arc<dyn FileApiDriver> {
FS_DRIVER.clone()
}
#[cfg(test)]
mod test {
use super::fs_driver;
#[test]
fn sanitize_simple() {
assert_eq!(
fs_driver().sanitize_file_name("a.txt"),
"a.txt"
);
assert_eq!(
fs_driver().sanitize_file_name("a/b.txt"),
"a_b.txt"
);
assert_eq!(
fs_driver().sanitize_file_name("a\0a/b.txt"),
"a_a_b.txt"
);
assert_eq!(
fs_driver().sanitize_file_name("a\\b\\.txt "),
"a_b_.txt"
);
assert_eq!(
fs_driver().sanitize_file_name("/"),
"_"
);
}
}

View File

@@ -7,6 +7,16 @@ use std::path::Path;
pub struct FileApiDriverImpl {}
impl FileApiDriver for FileApiDriverImpl {
#[cfg(target_os = "windows")]
fn is_windows(&self) -> bool {
true
}
#[cfg(not(target_os = "windows"))]
fn is_windows(&self) -> bool {
false
}
fn is_directory(&self, path: &str) -> ApiResult<bool> {
let metadata = fs::metadata(path)?;
let file_type = metadata.file_type();

View File

@@ -31,6 +31,9 @@ extern "C" {
#[wasm_bindgen(js_name = readDir, catch)]
fn read_dir_js(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = isWindows)]
fn is_windows() -> bool;
}
#[wasm_bindgen(module = "fs")]
@@ -73,6 +76,10 @@ fn handle_error(error: JsValue, source: &str) -> std::io::Error {
pub struct FileApiDriverImpl {}
impl FileApiDriver for FileApiDriverImpl {
fn is_windows(&self) -> bool {
is_windows()
}
fn is_directory(&self, path: &str) -> ApiResult<bool> {
match is_directory(path) {
Ok(is_dir) => Ok(is_dir),

View File

@@ -17,7 +17,6 @@ mime_guess = "2.0.3"
once_cell = "1.4.1"
palette = "0.5.0"
regex = "1"
sanitize-filename = "0.3.0"
console_error_panic_hook = "0.1.7"
bytes = "1.2.0"
encoding_rs = "0.8.31"

View File

@@ -38,7 +38,7 @@ impl Renderer {
)?));
}
SectionEntry::SectionGroup(group) => {
let dir_name = sanitize_filename::sanitize(group.display_name());
let dir_name = fs_driver().sanitize_file_name(group.display_name());
let section_group_dir =
fs_driver().join(notebook_dir.as_str(), dir_name.as_str());

View File

@@ -28,7 +28,7 @@ impl Renderer {
pub fn render(&mut self, section: &Section, output_dir: String) -> Result<RenderedSection> {
let section_dir = fs_driver().join(
output_dir.as_str(),
sanitize_filename::sanitize(section.display_name()).as_str(),
fs_driver().sanitize_file_name(section.display_name()).as_str(),
);
log!(
"section_dir: {:?} \n output_dir: {:?}",
@@ -168,7 +168,7 @@ impl Renderer {
let filename = filename_base.trim().replace("/", "_");
let mut i = 0;
let mut current_filename =
sanitize_filename::sanitize(format!("{}{}", filename, extension));
fs_driver().sanitize_file_name(&format!("{}{}", filename, extension));
loop {
let current_full_path = fs_driver().join(parent_dir, &current_filename);
@@ -179,7 +179,7 @@ impl Renderer {
i += 1;
current_filename =
sanitize_filename::sanitize(format!("{}_{}{}", filename, i, extension));
fs_driver().sanitize_file_name(&format!("{}_{}{}", filename, i, extension));
}
Ok(current_filename)

View File

@@ -10860,7 +10860,7 @@ __metadata:
sqlite3: "npm:5.1.6"
string-padding: "npm:1.0.2"
string-to-stream: "npm:3.0.1"
tar: "npm:6.2.1"
tar: "npm:7.5.8"
tcp-port-used: "npm:1.0.2"
tesseract.js: "npm:6.0.1"
typescript: "npm:5.8.3"
@@ -38732,6 +38732,15 @@ __metadata:
languageName: node
linkType: hard
"minizlib@npm:^3.1.0":
version: 3.1.0
resolution: "minizlib@npm:3.1.0"
dependencies:
minipass: "npm:^7.1.2"
checksum: 10/f47365cc2cb7f078cbe7e046eb52655e2e7e97f8c0a9a674f4da60d94fb0624edfcec9b5db32e8ba5a99a5f036f595680ae6fe02a262beaa73026e505cc52f99
languageName: node
linkType: hard
"mississippi@npm:^3.0.0":
version: 3.0.0
resolution: "mississippi@npm:3.0.0"
@@ -49589,17 +49598,16 @@ __metadata:
languageName: node
linkType: hard
"tar@npm:6.2.1, tar@npm:^6.2.1":
version: 6.2.1
resolution: "tar@npm:6.2.1"
"tar@npm:7.5.8":
version: 7.5.8
resolution: "tar@npm:7.5.8"
dependencies:
chownr: "npm:^2.0.0"
fs-minipass: "npm:^2.0.0"
minipass: "npm:^5.0.0"
minizlib: "npm:^2.1.1"
mkdirp: "npm:^1.0.3"
yallist: "npm:^4.0.0"
checksum: 10/bfbfbb2861888077fc1130b84029cdc2721efb93d1d1fb80f22a7ac3a98ec6f8972f29e564103bbebf5e97be67ebc356d37fa48dbc4960600a1eb7230fbd1ea0
"@isaacs/fs-minipass": "npm:^4.0.0"
chownr: "npm:^3.0.0"
minipass: "npm:^7.1.2"
minizlib: "npm:^3.1.0"
yallist: "npm:^5.0.0"
checksum: 10/5fddc22e0fd03e73d5e9e922e71d8681f85443dee4f21403059a757e186ae4004abc9a709cdc7f4143d7d75758a2935f7306b3cc193123d46b6f786dd2b99c2a
languageName: node
linkType: hard
@@ -49646,6 +49654,20 @@ __metadata:
languageName: node
linkType: hard
"tar@npm:^6.2.1":
version: 6.2.1
resolution: "tar@npm:6.2.1"
dependencies:
chownr: "npm:^2.0.0"
fs-minipass: "npm:^2.0.0"
minipass: "npm:^5.0.0"
minizlib: "npm:^2.1.1"
mkdirp: "npm:^1.0.3"
yallist: "npm:^4.0.0"
checksum: 10/bfbfbb2861888077fc1130b84029cdc2721efb93d1d1fb80f22a7ac3a98ec6f8972f29e564103bbebf5e97be67ebc356d37fa48dbc4960600a1eb7230fbd1ea0
languageName: node
linkType: hard
"tar@npm:^7.4.3":
version: 7.4.3
resolution: "tar@npm:7.4.3"