fix(core): onenote import (#15332)
#### PR Dependency Tree * **PR #15332** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added improved Windows support for importing OneNote files, including `.one`, `.onetoc2`, and `.onepkg` formats. * Enabled reliable handling of Windows paths, including UNC and verbatim paths. * Added filesystem operations for reading, writing, discovering, and opening OneNote content on Windows. * **Bug Fixes** * Improved file access and path resolution during OneNote imports on Windows. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -165,6 +165,7 @@ dependencies = [
|
|||||||
"affine_nbstore",
|
"affine_nbstore",
|
||||||
"affine_preview",
|
"affine_preview",
|
||||||
"affine_sqlite_v1",
|
"affine_sqlite_v1",
|
||||||
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"hex",
|
"hex",
|
||||||
"infer",
|
"infer",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ resolver = "3"
|
|||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
base64-simd = "0.8"
|
base64-simd = "0.8"
|
||||||
block2 = "0.6"
|
block2 = "0.6"
|
||||||
|
bytes = "1"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
core-foundation = "0.10"
|
core-foundation = "0.10"
|
||||||
coreaudio-rs = "0.12"
|
coreaudio-rs = "0.12"
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ typed-path = { workspace = true }
|
|||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
affine_preview = { workspace = true, features = ["mermaid", "typst"] }
|
affine_preview = { workspace = true, features = ["mermaid", "typst"] }
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
bytes = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||||
mimalloc = { workspace = true }
|
mimalloc = { workspace = true }
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
mod content;
|
mod content;
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod windows_fs;
|
||||||
|
|
||||||
use affine_importer::{
|
use affine_importer::{
|
||||||
FolderHierarchyDelta, ImportBatch, ImportBatchLimits, ImportCursor, ImportError, ImportOptions, ImportProgress,
|
FolderHierarchyDelta, ImportBatch, ImportBatchLimits, ImportCursor, ImportError, ImportOptions, ImportProgress,
|
||||||
@@ -19,6 +21,8 @@ use onenote_parser::{
|
|||||||
section::{Section, SectionEntry, SectionGroup},
|
section::{Section, SectionEntry, SectionGroup},
|
||||||
};
|
};
|
||||||
use typed_path::TypedPath;
|
use typed_path::TypedPath;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use windows_fs::WindowsFs;
|
||||||
|
|
||||||
pub struct OneNoteImportProvider;
|
pub struct OneNoteImportProvider;
|
||||||
|
|
||||||
@@ -81,7 +85,7 @@ impl OneNotePlanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn read_file(&mut self, path: &Path) -> ImportResult<()> {
|
fn read_file(&mut self, path: &Path) -> ImportResult<()> {
|
||||||
let parser = Parser::new();
|
let parser = parser();
|
||||||
match extension(path).as_deref() {
|
match extension(path).as_deref() {
|
||||||
Some("one") => {
|
Some("one") => {
|
||||||
let source_path = path.to_string_lossy();
|
let source_path = path.to_string_lossy();
|
||||||
@@ -121,7 +125,7 @@ impl OneNotePlanner {
|
|||||||
path.to_string_lossy()
|
path.to_string_lossy()
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
let parser = Parser::new();
|
let parser = parser();
|
||||||
let toc_path = toc.to_string_lossy();
|
let toc_path = toc.to_string_lossy();
|
||||||
let notebook = parser
|
let notebook = parser
|
||||||
.parse_notebook(TypedPath::derive(toc_path.as_ref()))
|
.parse_notebook(TypedPath::derive(toc_path.as_ref()))
|
||||||
@@ -210,6 +214,16 @@ impl OneNotePlanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn parser() -> Parser {
|
||||||
|
Parser::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn parser() -> Parser<WindowsFs> {
|
||||||
|
Parser::new_with_fs(WindowsFs)
|
||||||
|
}
|
||||||
|
|
||||||
struct OneNoteImportCursor {
|
struct OneNoteImportCursor {
|
||||||
docs: Vec<OneNoteDoc>,
|
docs: Vec<OneNoteDoc>,
|
||||||
folders: Vec<FolderHierarchyDelta>,
|
folders: Vec<FolderHierarchyDelta>,
|
||||||
|
|||||||
152
packages/frontend/native/src/import/onenote/windows_fs.rs
Normal file
152
packages/frontend/native/src/import/onenote/windows_fs.rs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
use std::{
|
||||||
|
ffi::OsString,
|
||||||
|
fs::{self, File},
|
||||||
|
io::{Error, Read},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use onenote_parser::{
|
||||||
|
FileSystem,
|
||||||
|
fs::{FileSource, file_source::CachedFileSource},
|
||||||
|
};
|
||||||
|
use typed_path::{TypedPath, TypedPathBuf};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(super) struct WindowsFs;
|
||||||
|
|
||||||
|
impl FileSystem for WindowsFs {
|
||||||
|
fn is_directory(&self, path: TypedPath) -> Result<bool, Error> {
|
||||||
|
match fs::metadata(resolve_path(path)?) {
|
||||||
|
Ok(meta) => Ok(meta.is_dir()),
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_dir(&self, path: TypedPath) -> Result<Vec<TypedPathBuf>, Error> {
|
||||||
|
fs::read_dir(resolve_path(path)?)?
|
||||||
|
.map(|entry| entry.and_then(|entry| as_typed_path(&entry.path())))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_file(&self, path: TypedPath) -> Result<Vec<u8>, Error> {
|
||||||
|
fs::read(resolve_path(path)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_file(&self, path: TypedPath, data: &[u8]) -> Result<(), Error> {
|
||||||
|
fs::write(resolve_path(path)?, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stream_to_file(&self, path: TypedPath, reader: &mut dyn Read) -> Result<(), Error> {
|
||||||
|
let mut file = File::create(resolve_path(path)?)?;
|
||||||
|
std::io::copy(reader, &mut file)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_dir(&self, path: TypedPath) -> Result<(), Error> {
|
||||||
|
fs::create_dir_all(resolve_path(path)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn canonicalize(&self, path: TypedPath) -> Result<TypedPathBuf, Error> {
|
||||||
|
as_typed_path(&fs::canonicalize(resolve_path(path)?)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exists(&self, path: TypedPath) -> Result<bool, Error> {
|
||||||
|
fs::exists(resolve_path(path)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_file(&self, path: TypedPath) -> Result<Arc<dyn FileSource>, Error> {
|
||||||
|
let file = File::open(resolve_path(path)?)?;
|
||||||
|
let byte_length = file.metadata()?.len();
|
||||||
|
Ok(Arc::new(CachedFileSource::new(WindowsFileSource { file, byte_length })))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_path(path: TypedPath) -> Result<PathBuf, Error> {
|
||||||
|
let path = if path.is_windows() {
|
||||||
|
path.to_path_buf()
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
.with_windows_encoding_checked()
|
||||||
|
.map_err(|error| Error::new(std::io::ErrorKind::InvalidInput, error))?
|
||||||
|
};
|
||||||
|
let path = path
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| Error::new(std::io::ErrorKind::InvalidData, "path bytes are not valid UTF-8"))?;
|
||||||
|
to_verbatim(PathBuf::from(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_typed_path(path: &Path) -> Result<TypedPathBuf, Error> {
|
||||||
|
let path = path
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| Error::new(std::io::ErrorKind::InvalidData, "path is not valid UTF-8"))?;
|
||||||
|
Ok(TypedPath::windows(path.as_bytes()).to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_verbatim(path: PathBuf) -> Result<PathBuf, Error> {
|
||||||
|
let path = std::path::absolute(path)?.to_string_lossy().replace('/', "\\");
|
||||||
|
if path.starts_with(r"\\?\") {
|
||||||
|
return Ok(PathBuf::from(path));
|
||||||
|
}
|
||||||
|
let mut verbatim = OsString::new();
|
||||||
|
if let Some(path) = path.strip_prefix(r"\\") {
|
||||||
|
verbatim.push(r"\\?\UNC\");
|
||||||
|
verbatim.push(path);
|
||||||
|
} else {
|
||||||
|
verbatim.push(r"\\?\");
|
||||||
|
verbatim.push(path);
|
||||||
|
}
|
||||||
|
Ok(PathBuf::from(verbatim))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WindowsFileSource {
|
||||||
|
file: File,
|
||||||
|
byte_length: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileSource for WindowsFileSource {
|
||||||
|
fn byte_length(&self) -> u64 {
|
||||||
|
self.byte_length
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_at(&self, offset: u64, len: usize) -> Result<Bytes, Error> {
|
||||||
|
use std::os::windows::fs::FileExt;
|
||||||
|
|
||||||
|
let mut bytes = vec![0; len];
|
||||||
|
let mut read = 0;
|
||||||
|
while read < len {
|
||||||
|
let count = self.file.seek_read(&mut bytes[read..], offset + read as u64)?;
|
||||||
|
if count == 0 {
|
||||||
|
return Err(Error::new(
|
||||||
|
std::io::ErrorKind::UnexpectedEof,
|
||||||
|
"unexpected end of OneNote file",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
read += count;
|
||||||
|
}
|
||||||
|
Ok(Bytes::from(bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preserves_windows_absolute_path_prefixes() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_path(TypedPath::derive(r"C:\Users\Alice\Notebook.one")).unwrap(),
|
||||||
|
PathBuf::from(r"\\?\C:\Users\Alice\Notebook.one")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_path(TypedPath::derive(r"\\server\share\Notebook.one")).unwrap(),
|
||||||
|
PathBuf::from(r"\\?\UNC\server\share\Notebook.one")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_path(TypedPath::derive(r"\\?\D:\Notebook.one")).unwrap(),
|
||||||
|
PathBuf::from(r"\\?\D:\Notebook.one")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user