diff --git a/README.md b/README.md index b2e444a..da333ef 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,13 @@ # wls_vfs -windows linux文件共享,使用fuse + grpc + protobuffer -语言:rust \ No newline at end of file +Windows/Linux 文件共享服务端(gRPC + Protobuf)。 + +本仓库已升级至协议 v2(统一错误模型): + +- 所有 RPC 使用显式的 `Request`/`Response` 类型; +- 响应统一携带 `RpcStatus { ok, error }`; +- 删除历史 `ret` 字段;失败信息通过 `FsError` 完整承载(`code`/`sys_msg`/`operation`/`paths`/`context` 等)。 + +协议详情见:`proto/lws.proto` 与 `docs/protocol_v2.md`。 + +注意:本项目主要在 Windows 上构建运行(依赖 `winapi`)。在非 Windows 平台构建仅用于类型检查,可能失败。 diff --git a/docs/protocol_v2.md b/docs/protocol_v2.md new file mode 100644 index 0000000..91a5094 --- /dev/null +++ b/docs/protocol_v2.md @@ -0,0 +1,14 @@ +# LWS 协议 v2(统一错误模型) + +- RpcStatus { ok, error } +- FsError { code, message, sys_msg, grpc_code, grpc_message, operation(FsOp), paths[], context{...}, causes[], server, timestamp_ms, retriable } + +成功:ok = true;失败:ok = false 且 error.code 为正数 POSIX errno。 + +常用 context keys: flags, offset, size, uid, gid, mode, fh, mask, xattr_name, xattr_size。 + +FsOp 与 RPC 映射:与客户端 docs 相同(参见 proto/lws.proto 中的 service 定义)。 + +服务端返回建议: +- 失败时设置 error.code 与 sys_msg,并补充 operation/paths/context。 +- 传输层错误由客户端据 gRPC `Status` 统一映射。 diff --git a/proto/lws.proto b/proto/lws.proto index 895e00e..4eac9b4 100644 --- a/proto/lws.proto +++ b/proto/lws.proto @@ -16,43 +16,62 @@ syntax = "proto3"; package lws_vfs; -service LwsVfs { - // Sends a greeting - rpc SayHello(HelloRequest) returns (HelloReply) {} - rpc GetConfig(get_config) returns (get_config) {} - - rpc fgetattr(getattr) returns (getattr) {} - rpc fsetxattr(setxattr) returns (setxattr) {} - rpc faccess(access) returns (access) {} - rpc freaddir(readdir) returns (readdir) {} - rpc fread(read) returns (read) {} - rpc fopen(open) returns (open) {} - rpc fwrite(write) returns (write) {} - rpc fgetxattr(getxattr) returns (getxattr) {} - rpc ftruncate(truncate) returns (truncate) {} - rpc futimens(utimens) returns (utimens) {} - rpc fchown(chown) returns (chown) {} - rpc frelease(release) returns (release) {} - rpc fmkdir(mkdir) returns (mkdir) {} - rpc frmdir(rmdir) returns (rmdir) {} - rpc fflush(flush) returns (flush) {} - rpc fopendir(opendir) returns (opendir) {} - rpc freleasedir(releasedir) returns (releasedir) {} - rpc fcreate(create) returns (create) {} - rpc funlink(unlink) returns (unlink) {} - rpc frename(rename) returns (rename) {} +// Filesystem operation types for diagnostics and observability. +enum FsOp { + FSOP_UNKNOWN = 0; + FSOP_GETATTR = 1; + FSOP_SETXATTR = 2; + FSOP_ACCESS = 3; + FSOP_READDIR = 4; + FSOP_READ = 5; + FSOP_OPEN = 6; + FSOP_WRITE = 7; + FSOP_GETXATTR = 8; + FSOP_TRUNCATE = 9; + FSOP_UTIMENS = 10; + FSOP_CHOWN = 11; + FSOP_RELEASE = 12; + FSOP_MKDIR = 13; + FSOP_RMDIR = 14; + FSOP_FLUSH = 15; + FSOP_OPENDIR = 16; + FSOP_RELEASEDIR = 17; + FSOP_CREATE = 18; + FSOP_UNLINK = 19; + FSOP_RENAME = 20; + FSOP_HELLO = 21; + FSOP_GET_CONFIG = 22; } -// The request message containing the user's name. -message HelloRequest { string name = 1; } - -// The response message containing the greetings -message HelloReply { string message = 1; } - -message get_config { - string config = 1; +// Optional chained cause for nested errors. +message ErrorCause { + int32 code = 1; // POSIX errno if available + string message = 2; // human-readable message + string type = 3; // error type/category from server implementation } +// Rich error payload carried by all responses. +message FsError { + int32 code = 1; // POSIX errno (>0) + string message = 2; // application error message + string sys_msg = 3; // strerror(code) or OS message + uint32 grpc_code = 4; // gRPC status code (if applicable) + string grpc_message = 5; // gRPC status message + FsOp operation = 6; // which fs op failed + repeated string paths = 7; // involved paths (e.g. [from, to]) + map context = 8; // flags/size/offset/uid/gid ... + repeated ErrorCause causes = 9; // nested causes + string server = 10; // server id/hostname + uint64 timestamp_ms = 11; // server time when error occurred + bool retriable = 12; // whether retry may succeed +} + +message RpcStatus { + bool ok = 1; // true if the call succeeded + FsError error = 2; // populated when ok == false +} + +// Common structs message file_info { uint32 flags = 1; uint32 fh_old = 2; @@ -77,34 +96,9 @@ message fstat { uint64 fst_blocks = 13; } -// fuse api function message define: -message getattr { - string path = 1; - fstat stat = 2; - file_info fi = 3; - int32 ret = 15; -} - -message setxattr { - string path = 1; - string name = 2; - bytes value = 3; - int64 size = 4; - uint32 flags = 5; - int32 ret = 15; -} -message getxattr { - string path = 1; - string name = 2; - bytes value = 3; - int64 size = 4; - int32 ret = 15; -} - -message access { - string path = 1; - uint32 mask = 2; - int32 ret = 15; +message timespec { + int32 tv_sec = 1; + int64 tv_nsec = 2; } message direntry { @@ -112,112 +106,100 @@ message direntry { uint32 kind = 2; } -message readdir { - string path = 1; - repeated direntry dirs = 2; - uint32 offset = 3; - file_info fi = 4; - int32 ret = 15; +// Service and RPCs +service LwsVfs { + rpc SayHello(HelloRequest) returns (HelloReply) {} + rpc GetConfig(GetConfigRequest) returns (GetConfigResponse) {} + + rpc fgetattr(GetattrRequest) returns (GetattrResponse) {} + rpc fsetxattr(SetxattrRequest) returns (EmptyResponse) {} + rpc faccess(AccessRequest) returns (EmptyResponse) {} + rpc freaddir(ReaddirRequest) returns (ReaddirResponse) {} + rpc fread(ReadRequest) returns (ReadResponse) {} + rpc fopen(OpenRequest) returns (OpenResponse) {} + rpc fwrite(WriteRequest) returns (WriteResponse) {} + rpc fgetxattr(GetxattrRequest) returns (GetxattrResponse) {} + rpc ftruncate(TruncateRequest) returns (EmptyResponse) {} + rpc futimens(UtimensRequest) returns (EmptyResponse) {} + rpc fchown(ChownRequest) returns (EmptyResponse) {} + rpc frelease(ReleaseRequest) returns (EmptyResponse) {} + rpc fmkdir(MkdirRequest) returns (EmptyResponse) {} + rpc frmdir(RmdirRequest) returns (EmptyResponse) {} + rpc fflush(FlushRequest) returns (EmptyResponse) {} + rpc fopendir(OpendirRequest) returns (OpendirResponse) {} + rpc freleasedir(ReleasedirRequest) returns (EmptyResponse) {} + rpc fcreate(CreateRequest) returns (CreateResponse) {} + rpc funlink(UnlinkRequest) returns (EmptyResponse) {} + rpc frename(RenameRequest) returns (EmptyResponse) {} } -message open { - string path = 1; - file_info fi = 2; - int32 ret = 15; -} +// Hello +message HelloRequest { string name = 1; } +message HelloReply { string message = 1; RpcStatus status = 2; } -message read { - string path = 1; - bytes buff = 2; - int64 size = 3; - uint64 offset = 4; - file_info fi = 5; - int32 ret = 15; -} +// Config +message GetConfigRequest {} +message GetConfigResponse { string config = 1; RpcStatus status = 2; } -message write { - string path = 1; - bytes buff = 2; - uint64 size = 3; - uint64 offset = 4; - file_info fi = 5; - int32 ret = 15; -} +// getattr +message GetattrRequest { string path = 1; file_info fi = 2; } +message GetattrResponse { fstat stat = 1; RpcStatus status = 2; } -message truncate { - string path = 1; - int64 size = 3; - int32 ret = 15; -} +// xattr +message SetxattrRequest { string path = 1; string name = 2; bytes value = 3; int64 size = 4; uint32 flags = 5; } +message GetxattrRequest { string path = 1; string name = 2; int64 size = 3; } +message GetxattrResponse { bytes value = 1; RpcStatus status = 2; } -message timespec { - int32 tv_sec = 1; - int64 tv_nsec = 2; -} -message utimens { - string path = 1; - repeated timespec ts = 2; // const struct timespec ts[2] - int32 ret = 15; -} +// access +message AccessRequest { string path = 1; uint32 mask = 2; } -message chown { - string path = 1; - int32 uid = 2; - int32 gid = 3; - int32 ret = 15; -} +// readdir +message ReaddirRequest { string path = 1; uint32 offset = 2; file_info fi = 3; } +message ReaddirResponse { repeated direntry dirs = 1; RpcStatus status = 2; } -message release { - string path = 1; - file_info fi = 2; - uint32 flush = 3; - int32 ret = 15; -} +// open +message OpenRequest { string path = 1; file_info fi = 2; } +message OpenResponse { file_info fi = 1; RpcStatus status = 2; } -message mkdir { - string path = 1; - file_info fi = 2; - uint32 mode = 3; - int32 ret = 15; -} +// read +message ReadRequest { string path = 1; uint64 offset = 2; int64 size = 3; file_info fi = 4; } +message ReadResponse { bytes data = 1; RpcStatus status = 2; } -message rmdir { - string path = 1; - int32 ret = 15; -} +// write +message WriteRequest { string path = 1; uint64 offset = 2; bytes data = 3; file_info fi = 4; } +message WriteResponse { uint64 written = 1; RpcStatus status = 2; } -message flush { - string path = 1; - file_info fi = 2; - int32 ret = 15; -} +// truncate +message TruncateRequest { string path = 1; int64 size = 2; } -message opendir { - string path = 1; - file_info fi = 2; - int32 ret = 15; -} +// utimens +message UtimensRequest { string path = 1; repeated timespec ts = 2; } -message releasedir { - string path = 1; - file_info fi = 2; - int32 ret = 15; -} +// chown +message ChownRequest { string path = 1; int32 uid = 2; int32 gid = 3; } -message create { - string path = 1; - uint32 mode = 2; - file_info fi = 3; - int32 ret = 15; -} +// release/flush +message ReleaseRequest { string path = 1; file_info fi = 2; uint32 flush = 3; } +message FlushRequest { string path = 1; file_info fi = 2; } -message unlink { - string path = 1; - int32 ret = 15; -} +// mkdir/rmdir +message MkdirRequest { string path = 1; uint32 mode = 2; } +message RmdirRequest { string path = 1; } -message rename { - string path = 1; - string new = 2; - int32 ret = 15; -} \ No newline at end of file +// opendir/releasedir +message OpendirRequest { string path = 1; } +message OpendirResponse { file_info fi = 1; RpcStatus status = 2; } +message ReleasedirRequest { string path = 1; file_info fi = 2; } + +// create +message CreateRequest { string path = 1; uint32 mode = 2; } +message CreateResponse { file_info fi = 1; RpcStatus status = 2; } + +// unlink +message UnlinkRequest { string path = 1; } + +// rename +message RenameRequest { string path = 1; string new = 2; } + +// Empty response with status only +message EmptyResponse { RpcStatus status = 1; } diff --git a/src/lib.rs b/src/lib.rs index 1eb4b23..4a370a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,580 +1,471 @@ - -use lws_vfs::lws_vfs_server::LwsVfs; -use lws_vfs::{ - Access, Chown, FileInfo, Flush, Fstat, Getattr, Getxattr, HelloReply, HelloRequest, Mkdir, - Open, Read, Readdir, Release, Rmdir, Setxattr, Truncate, Utimens, Write, Create, Unlink, Opendir, Releasedir, Rename, GetConfig, -}; - -use self::fs_impl::FSImpl; -use serde_json::{self, Value}; -use serde::Serialize; -use std::error::Error; -use std::fs::File; -use std::io::Read as _; -use tonic::{Request, Response, Status}; -use std::collections::HashMap; -extern crate log; - -use std::path::{Path, PathBuf}; - -mod fs_impl; -mod disk; -pub mod lws_vfs { - tonic::include_proto!("lws_vfs"); -} - -// config - -// { -// "mount": { -// "c:\\": "/l3c", -// "w:\\": "/l0e", -// "y:\\": "/l0d" -// }, -// "port":5001 -// } - -#[derive(Debug, Serialize, Default)] -pub struct Config { - port: u16, - mount_map: HashMap, -} -impl Config { - pub fn new(json: &str) -> Result> { - let mut file = File::open(json)?; - let mut buffer = String::new(); - file.read_to_string(&mut buffer)?; - let json: Value = serde_json::from_str(buffer.as_ref())?; - let port: u16 = match json.get("port") { - Some(port) => port.as_u64().expect("expect port is a number but its not") as u16, - None => 5001, - }; - let mounts = match json.get("mount") { - Some(mounts) => mounts.as_object().unwrap(), - None => { - return Err(Box::new(std::io::Error::new( - std::io::ErrorKind::Other, - "no mount map", - ))) - } - }; - let mut mount_map: HashMap = mounts.iter().filter(|(key, _)| { - match disk::get_disk_info(&key) { - None => false, - Some(_) => true, - } - }).map(|(key, value)| (key.to_string(), value.as_str().unwrap().to_string())).collect(); - log::debug!("mounts is {:?}", mounts); - - let mut volume_names = HashMap::new(); - - // 获取所有的盘符对应的卷标便于后续进行比对 - for drive in 'C'..='Z' { - let drive = format!("{}:\\", drive); - if let Some((volume, _, _)) = disk::get_disk_info(&drive) { - //TODO: 这里如果存在同名的卷标会出现问题 - volume_names.insert(volume, drive); - } - } - log::info!("local disk info: {:?}", volume_names); - - // 遍历配置的盘符和卷标,将配置的盘符和卷标映射到实际盘符上 - for (key, value) in mounts.iter() { - // 直接看下是不是盘符 - if let Some((drive, _, _)) = disk::get_disk_info(&key) { - mount_map.insert(drive.to_string(), value.as_str().unwrap().to_string()); - } else { - // 看下是不是卷标 - if let Some(drive) = volume_names.get(key.as_str()) { - // log::info!("value is {:?} {}", value, value.as_str().unwrap()); - mount_map.insert(drive.to_string(), value.as_str().unwrap().to_string()); - } else { - log::error!("mount volume name [{}] not found in disk list", key); - return Err(Box::new(std::io::Error::new( - std::io::ErrorKind::Other, - "mount map unavailable", - ))) - } - - } - } - log::info!("after local search&match: {:?}", mount_map); - Ok(Config { port, mount_map }) - } - - pub fn get_port(&self) -> u16 { - self.port - } -} - -/// server 的基础结构 -#[derive(Debug, Default)] -pub struct LwsVfsIns { - pub config: Config, - pub fs: FSImpl, -} - -fn linux_to_windows_path(linux_path: &str) -> String { - let path: PathBuf = Path::new(linux_path) - .components() - // .filter(|component| component.as_os_str() != Component::RootDir.as_os_str()) - // .map(|component| component.as_os_str().to_string_lossy().into_owned()) - .collect(); - path.to_string_lossy().to_string() -} - -impl LwsVfsIns { - pub fn new(json: &str) -> Result> { - Ok(LwsVfsIns { - config: Config::new(json)?, - fs: FSImpl::new(), - }) - } - fn lpath(&self, path: &String) -> String { - let mut ret = String::new(); - // log::trace!("try to convert {}", path); - // /l0e -> w: - for (k, v) in &self.config.mount_map { - if path.starts_with(v) { - ret = path.replace(v, k); - break; - } - } - linux_to_windows_path(&ret) - } -} - -/// 将result 转换成 i32返回给rpc使用 -macro_rules! result2ret { - ($fun_name:expr, $path:expr, $ret:expr) => { - match $ret { - Ok(ret) => ret, - Err(e) => { - log::error!("{} [{}]: {:?}", $fun_name, $path, e); - if let Some(e) = e.downcast_ref::() { - e.raw_os_error().unwrap_or(-1) as i32 - } else { - -1 - } - } - } - }; -} - // match e.downcast_ref::() { - // Some(e) => match e.kind() { - // std::io::ErrorKind::NotFound => libc::ENOENT, - // std::io::ErrorKind::PermissionDenied => libc::EACCES, - // std::io::ErrorKind::AlreadyExists => libc::EEXIST, - // } - // } - -#[tonic::async_trait] -impl LwsVfs for LwsVfsIns { - async fn say_hello( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - - let reply = HelloReply { - message: format!("Hello {}!", request.name), - }; - - Ok(Response::new(reply)) - } - async fn get_config(&self, _req: Request) -> Result, Status> { - log::info!("{:?}",self.config); - let config = serde_json::to_string(&self.config).unwrap(); - log::info!("reply config: {}", config); - let reply = GetConfig { - config, - }; - Ok(Response::new(reply)) - } - async fn fgetattr(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let mut fstat = Fstat::default(); - let mut fi = FileInfo::default(); - let ret = self.fs.fgetattr(path, &mut fstat, &mut fi); - let ret = result2ret!("fgetattr", path, ret); - let reply = Getattr { - path: path.to_string(), - stat: Some(fstat), - ret, - ..Getattr::default() - }; - Ok(Response::new(reply)) - } - async fn fopen(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let mut fi = match request.fi{ - Some(fi) => fi, - None => FileInfo::default(), - }; - let ret = self.fs.fopen(path, &mut fi); - let ret = result2ret!("fopen", path, ret); - let reply = Open { - path: path.to_string(), - fi: Some(fi), - ret, - }; - Ok(Response::new(reply)) - } - async fn fread(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - let mut fi = match request.fi{ - Some(fi) => fi, - None => FileInfo::default(), - }; - let path = &self.lpath(&request.path); - let mut size: usize = request.size as usize; - let mut offset: usize = request.offset as usize; - let mut buff: Vec = Vec::with_capacity(size); - unsafe { - buff.set_len(size); - } - log::trace!("Got a request: fread {}, size {} offset {}", path, request.size, offset); - let ret = self.fs.fread(path, &mut buff, &mut size, offset, &mut fi); - let ret = result2ret!("fread", path, ret); - if ret != 0 { - buff = Vec::new(); - offset = 0; - } - Ok(Response::new(Read { - path: request.path, - size: buff.len() as i64, - buff, - offset: offset as u64, - fi: None, - ret, - })) - } - - async fn fwrite(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - let mut fi = match request.fi{ - Some(fi) => fi, - None => FileInfo::default(), - }; - let path = &self.lpath(&request.path); - let buff = request.buff; - let mut size = request.size as usize; - let offset = request.offset as usize; - log::trace!("Got a request: Write {}, size {} offset {}", path, request.size, offset); - let ret = self.fs.fwrite(path, &buff, &mut size, offset, &mut fi); - let ret = result2ret!("fwrite", path, ret); - let reply = Write { - ret, - path: path.to_string(), - size: size as u64, - ..Default::default() - }; - Ok(Response::new(reply)) - } - async fn faccess(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let mask = request.mask; - let ret = self.fs.faccess(path, mask); - let ret = result2ret!("faccess", path, ret); - let reply = Access { - ret, - path: path.to_string(), - mask, - }; - Ok(Response::new(reply)) - } - - async fn fsetxattr(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let name = request.name.as_ref(); - let value = request.value; - let size = request.size as usize; - let flags = request.flags; - let ret = self.fs.fsetxattr(path, name, &value, size, flags); - let ret = result2ret!("fsetxattr", path, ret); - let reply = Setxattr { - ret: ret, - ..Setxattr::default() - }; - Ok(Response::new(reply)) - } - async fn fgetxattr(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let name = request.name.as_ref(); - let size = request.size as usize; - let ret = self.fs.fgetxattr(path, name, size); - let ret = result2ret!("fgetxattr", path, ret); - let reply = Getxattr { - ret, - ..Getxattr::default() - }; - Ok(Response::new(reply)) - } - - async fn freaddir(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let mut fi = match request.fi{ - Some(fi) => fi, - None => FileInfo::default(), - }; - let path = &self.lpath(&request.path); - let offset = request.offset as usize; - let mut dirs = vec![]; - let size = 0; - let ret = self.fs.freaddir(path, &mut dirs, size, offset, &mut fi); - let ret = result2ret!("freaddir", path, ret); - let reply = Readdir { - ret, - dirs, - ..Readdir::default() - }; - Ok(Response::new(reply)) - } - async fn fmkdir(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let mode = request.mode; - let ret = self.fs.fmkdir(&path, mode); - let ret = result2ret!("fmkdir", path, ret); - let reply = Mkdir { - ret, - ..Mkdir::default() - }; - Ok(Response::new(reply)) - } - async fn ftruncate(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let size = request.size as u64; - let ret = self.fs.ftruncate(path, size); - let ret = result2ret!("ftruncate", path, ret); - let reply = Truncate { - ret, - ..Truncate::default() - }; - Ok(Response::new(reply)) - } - - async fn futimens(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let a_time = vec![request.ts[0].tv_sec as u64, request.ts[0].tv_nsec as u64]; - let m_time = vec![request.ts[1].tv_sec as u64, request.ts[1].tv_nsec as u64]; - let ret = self.fs.futimens(path, &a_time, &m_time); - let ret = result2ret!("futimens", path, ret); - let reply = Utimens { - ret, - ..Utimens::default() - }; - Ok(Response::new(reply)) - } - - async fn fchown(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let uid = request.uid as u32; - let gid = request.gid as u32; - let ret = self.fs.fchown(path, uid, gid); - let ret = result2ret!("fchown", path, ret); - let reply = Chown { - ret, - ..Chown::default() - }; - Ok(Response::new(reply)) - } - async fn frelease(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let mut fi = match request.fi{ - Some(fi) => fi, - None => FileInfo::default(), - }; - let ret = self.fs.frelease(path, &mut fi, request.flush != 0); - let ret = result2ret!("frelease", path, ret); - let reply = Release { - ret, - ..Release::default() - }; - Ok(Response::new(reply)) - } - async fn frmdir(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let ret = self.fs.frmdir(path); - let ret = result2ret!("frmdir", path, ret); - let reply = Rmdir { - ret, - path: request.path, - }; - Ok(Response::new(reply)) - } - - async fn fflush(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let mut fi = match request.fi{ - Some(fi) => fi, - None => FileInfo::default(), - }; - let ret = self.fs.fflush(path, &mut fi); - let ret = result2ret!("fflush", path, ret); - let reply = Flush { - ret, - ..Flush::default() - }; - Ok(Response::new(reply)) - } - - async fn fcreate(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let mode = request.mode as u32; - let mut fi = match request.fi{ - Some(fi) => fi, - None => FileInfo::default(), - }; - let ret = self.fs.fcreate(path, mode, &mut fi); - let ret = result2ret!("fcreate", path, ret); - let reply = Create { - ret, - fi:Some(fi), - ..Create::default() - }; - Ok(Response::new(reply)) - } - - async fn funlink(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let ret = self.fs.funlink(path); - let ret = result2ret!("funlink", path, ret); - let reply = Unlink { - ret, - path: request.path, - }; - Ok(Response::new(reply)) - } - - async fn fopendir(&self,request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let mut fi = match request.fi{ - Some(fi) => fi, - None => FileInfo::default(), - }; - let ret = self.fs.fopendir(path, &mut fi); - let ret = result2ret!("fopendir", path, ret); - let reply = Opendir { - ret, - fi: Some(fi), - ..Opendir::default() - }; - Ok(Response::new(reply)) - } - - async fn freleasedir(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let path = &self.lpath(&request.path); - let mut fi = match request.fi{ - Some(fi) => fi, - None => FileInfo::default(), - }; - let ret = self.fs.freleasedir(path, &mut fi); - let ret = result2ret!("freleasedir", path, ret); - let reply = Releasedir { - ret, - ..Releasedir::default() - }; - Ok(Response::new(reply)) - } - - async fn frename(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - log::trace!("Got a request: {:?}", request); - let from = &self.lpath(&request.path); - let to = &self.lpath(&request.new); - let ret = self.fs.frename(from, to); - let ret = result2ret!("frename", from, ret); - let reply = Rename { - ret, - ..Rename::default() - }; - Ok(Response::new(reply)) - } -} - -# [test] -fn test_config_new() { - env_logger::init(); - let _config = Config::new("config.json").unwrap(); - println!("Config: {:#?}", _config); -} - -# [cfg(test)] -mod test_macros { - use std::error::Error; - macro_rules! handle_result { - ($func:expr, $msg:expr) => { - match $func { - Ok(value) => value, - Err(e) => { - eprintln!("错误发生在函数:{},信息:{},错误:{}", stringify!($func), $msg, e); - -1 - } - } - }; - } - - - struct MyStruct; - - impl MyStruct { - fn example_function(&self, _:i32) -> Result> { - // 这里模拟一个可能的错误 - Err("模拟错误".into()) - } - } - - #[test] - fn test_() -> Result<(), Box> { - let my_struct = MyStruct; - - // 使用宏处理返回值 - assert_ne!(handle_result!(my_struct.example_function(32), "调用 example_function 时发生错误"), 32); - Ok(()) - } -} - -# [test] -fn test_path() { - env_logger::init(); - let path = linux_to_windows_path(r"p:\/test/dir/tree"); - assert_eq!(path, r"p:\test\dir\tree"); - let path = linux_to_windows_path(r"p:\/"); - assert_eq!(path, r"p:\"); - let path = linux_to_windows_path(r"p:\"); - assert_eq!(path, r"p:\"); -} \ No newline at end of file +use lws_vfs::lws_vfs_server::LwsVfs; +use lws_vfs::{ + AccessRequest, ChownRequest, CreateRequest, CreateResponse, Direntry, EmptyResponse, FileInfo, + FlushRequest, FsError, FsOp, Fstat, GetConfigRequest, GetConfigResponse, GetattrRequest, GetattrResponse, + GetxattrRequest, GetxattrResponse, HelloReply, HelloRequest, MkdirRequest, OpenRequest, OpenResponse, + OpendirRequest, OpendirResponse, ReadRequest, ReadResponse, ReaddirRequest, ReaddirResponse, ReleaseRequest, ReleasedirRequest, + RenameRequest, RpcStatus, RmdirRequest, SetxattrRequest, TruncateRequest, UnlinkRequest, UtimensRequest, WriteRequest, WriteResponse, +}; + +use self::fs_impl::FSImpl; +use serde_json::{self, Value}; +use serde::Serialize; +use std::error::Error; +use std::fs::File; +use std::io::Read as _; +use tonic::{Request, Response, Status}; +use std::collections::HashMap; +extern crate log; + +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +mod fs_impl; +mod disk; +pub mod lws_vfs { + tonic::include_proto!("lws_vfs"); +} + +// config + +// { +// "mount": { +// "c:\\": "/l3c", +// "w:\\": "/l0e", +// "y:\\": "/l0d" +// }, +// "port":5001 +// } + +#[derive(Debug, Serialize, Default)] +pub struct Config { + port: u16, + mount_map: HashMap, +} +impl Config { + pub fn new(json: &str) -> Result> { + let mut file = File::open(json)?; + let mut buffer = String::new(); + file.read_to_string(&mut buffer)?; + let json: Value = serde_json::from_str(buffer.as_ref())?; + let port: u16 = match json.get("port") { + Some(port) => port.as_u64().expect("expect port is a number but its not") as u16, + None => 5001, + }; + let mounts = match json.get("mount") { + Some(mounts) => mounts.as_object().unwrap(), + None => { + return Err(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + "no mount map", + ))) + } + }; + let mut mount_map: HashMap = mounts + .iter() + .filter(|(key, _)| match disk::get_disk_info(&key) { + None => false, + Some(_) => true, + }) + .map(|(key, value)| (key.to_string(), value.as_str().unwrap().to_string())) + .collect(); + log::debug!("mounts is {:?}", mounts); + + let mut volume_names = HashMap::new(); + + for drive in 'C'..='Z' { + let drive = format!("{}:\\", drive); + if let Some((volume, _, _)) = disk::get_disk_info(&drive) { + volume_names.insert(volume, drive); + } + } + log::info!("local disk info: {:?}", volume_names); + + for (key, value) in mounts.iter() { + if let Some((drive, _, _)) = disk::get_disk_info(&key) { + mount_map.insert(drive.to_string(), value.as_str().unwrap().to_string()); + } else if let Some(drive) = volume_names.get(key.as_str()) { + mount_map.insert(drive.to_string(), value.as_str().unwrap().to_string()); + } else { + log::error!("mount volume name [{}] not found in disk list", key); + return Err(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + "mount map unavailable", + ))); + } + } + log::info!("after local search&match: {:?}", mount_map); + Ok(Config { port, mount_map }) + } + + pub fn get_port(&self) -> u16 { self.port } +} + +/// server 的基础结构 +#[derive(Debug, Default)] +pub struct LwsVfsIns { + pub config: Config, + pub fs: FSImpl, +} + +fn linux_to_windows_path(linux_path: &str) -> String { + let path: PathBuf = Path::new(linux_path).components().collect(); + path.to_string_lossy().to_string() +} + +impl LwsVfsIns { + pub fn new(json: &str) -> Result> { + Ok(LwsVfsIns { config: Config::new(json)?, fs: FSImpl::new() }) + } + fn lpath(&self, path: &String) -> String { + let mut ret = String::new(); + for (k, v) in &self.config.mount_map { + if path.starts_with(v) { ret = path.replace(v, k); break; } + } + linux_to_windows_path(&ret) + } +} + +fn ok_status() -> Option { Some(RpcStatus{ ok: true, error: None }) } + +fn now_ms() -> u64 { + SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0) +} + +fn err_status(op: FsOp, paths: Vec, err: &(dyn std::error::Error)) -> Option { + let (code, sys_msg) = (libc::EIO, format!("{}", err)); + Some(RpcStatus{ + ok: false, + error: Some(FsError{ + code, + message: String::new(), + sys_msg, + grpc_code: 0, + grpc_message: String::new(), + operation: op as i32, + paths, + context: Default::default(), + causes: vec![], + server: std::env::var("HOSTNAME").unwrap_or_default(), + timestamp_ms: now_ms(), + retriable: false, + }) + }) +} + +#[tonic::async_trait] +impl LwsVfs for LwsVfsIns { + async fn say_hello(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + log::trace!("Got a request: {:?}", request); + let reply = HelloReply { message: format!("Hello {}!", request.name), status: ok_status() }; + Ok(Response::new(reply)) + } + + async fn get_config(&self, _req: Request) -> Result, Status> { + log::info!("{:?}", self.config); + let config = serde_json::to_string(&self.config).unwrap(); + log::info!("reply config: {}", config); + Ok(Response::new(GetConfigResponse{ config, status: ok_status() })) + } + + async fn fgetattr(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let mut fstat = Fstat::default(); + let mut fi = FileInfo::default(); + let reply = match self.fs.fgetattr(path, &mut fstat, &mut fi) { + Ok(0) => GetattrResponse { stat: Some(fstat), status: ok_status() }, + Ok(_) => GetattrResponse { stat: None, status: err_status(FsOp::FSOP_GETATTR, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => GetattrResponse { stat: None, status: err_status(FsOp::FSOP_GETATTR, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn fopen(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let mut fi = FileInfo::default(); + let reply = match self.fs.fopen(path, &mut fi) { + Ok(0) => OpenResponse{ fi: Some(fi), status: ok_status() }, + Ok(_) => OpenResponse{ fi: None, status: err_status(FsOp::FSOP_OPEN, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => OpenResponse{ fi: None, status: err_status(FsOp::FSOP_OPEN, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn fread(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let mut fi = FileInfo::default(); + let path = &self.lpath(&request.path); + let mut size: usize = request.size as usize; + let offset: usize = request.offset as usize; + let mut buff: Vec = vec![0; size]; + let reply = match self.fs.fread(path, &mut buff, &mut size, offset, &mut fi) { + Ok(0) => ReadResponse { data: buff, status: ok_status() }, + Ok(_) => ReadResponse { data: Vec::new(), status: err_status(FsOp::FSOP_READ, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => ReadResponse { data: Vec::new(), status: err_status(FsOp::FSOP_READ, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn fwrite(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let mut fi = FileInfo::default(); + let path = &self.lpath(&request.path); + let buff = request.data; + let mut size = buff.len(); + let offset = request.offset as usize; + let reply = match self.fs.fwrite(path, &buff, &mut size, offset, &mut fi) { + Ok(0) => WriteResponse { written: size as u64, status: ok_status() }, + Ok(_) => WriteResponse { written: 0, status: err_status(FsOp::FSOP_WRITE, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => WriteResponse { written: 0, status: err_status(FsOp::FSOP_WRITE, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn faccess(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let mask = request.mask; + let reply = match self.fs.faccess(path, mask) { + Ok(0) => EmptyResponse { status: ok_status() }, + Ok(_) => EmptyResponse { status: err_status(FsOp::FSOP_ACCESS, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => EmptyResponse { status: err_status(FsOp::FSOP_ACCESS, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn fsetxattr(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let name = request.name.as_ref(); + let value = request.value; + let size = request.size as usize; + let flags = request.flags; + let reply = match self.fs.fsetxattr(path, name, &value, size, flags) { + Ok(0) => EmptyResponse { status: ok_status() }, + Ok(_) => EmptyResponse { status: err_status(FsOp::FSOP_SETXATTR, vec![path.to_string()], &std::io::Error::from_raw_os_error(libc::ENOSYS)) }, + Err(e) => EmptyResponse { status: err_status(FsOp::FSOP_SETXATTR, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn fgetxattr(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let name = request.name.as_ref(); + let size = request.size as usize; + let reply = match self.fs.fgetxattr(path, name, size) { + Ok(0) => GetxattrResponse { value: Vec::new(), status: ok_status() }, + Ok(_) => GetxattrResponse { value: Vec::new(), status: err_status(FsOp::FSOP_GETXATTR, vec![path.to_string()], &std::io::Error::from_raw_os_error(libc::ENOSYS)) }, + Err(e) => GetxattrResponse { value: Vec::new(), status: err_status(FsOp::FSOP_GETXATTR, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn freaddir(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let mut fi = FileInfo::default(); + let path = &self.lpath(&request.path); + let offset = request.offset as usize; + let mut dirs: Vec = vec![]; + let size = 0; + let reply = match self.fs.freaddir(path, &mut dirs, size, offset, &mut fi) { + Ok(0) => ReaddirResponse { dirs, status: ok_status() }, + Ok(_) => ReaddirResponse { dirs: vec![], status: err_status(FsOp::FSOP_READDIR, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => ReaddirResponse { dirs: vec![], status: err_status(FsOp::FSOP_READDIR, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn fmkdir(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let mode = request.mode; + let reply = match self.fs.fmkdir(&path, mode) { + Ok(0) => EmptyResponse { status: ok_status() }, + Ok(_) => EmptyResponse { status: err_status(FsOp::FSOP_MKDIR, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => EmptyResponse { status: err_status(FsOp::FSOP_MKDIR, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn ftruncate(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let size = request.size as u64; + let reply = match self.fs.ftruncate(path, size) { + Ok(0) => EmptyResponse { status: ok_status() }, + Ok(_) => EmptyResponse { status: err_status(FsOp::FSOP_TRUNCATE, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => EmptyResponse { status: err_status(FsOp::FSOP_TRUNCATE, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn futimens(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let a_time = vec![request.ts[0].tv_sec as u64, request.ts[0].tv_nsec as u64]; + let m_time = vec![request.ts[1].tv_sec as u64, request.ts[1].tv_nsec as u64]; + let reply = match self.fs.futimens(path, &a_time, &m_time) { + Ok(0) => EmptyResponse { status: ok_status() }, + Ok(_) => EmptyResponse { status: err_status(FsOp::FSOP_UTIMENS, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => EmptyResponse { status: err_status(FsOp::FSOP_UTIMENS, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn fchown(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let uid = request.uid as u32; + let gid = request.gid as u32; + let reply = match self.fs.fchown(path, uid, gid) { + Ok(0) => EmptyResponse { status: ok_status() }, + Ok(_) => EmptyResponse { status: err_status(FsOp::FSOP_CHOWN, vec![path.to_string()], &std::io::Error::from_raw_os_error(libc::ENOSYS)) }, + Err(e) => EmptyResponse { status: err_status(FsOp::FSOP_CHOWN, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn frelease(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let mut fi = FileInfo::default(); + let reply = match self.fs.frelease(path, &mut fi, request.flush != 0) { + Ok(0) => EmptyResponse { status: ok_status() }, + Ok(_) => EmptyResponse { status: err_status(FsOp::FSOP_RELEASE, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => EmptyResponse { status: err_status(FsOp::FSOP_RELEASE, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn frmdir(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let reply = match self.fs.frmdir(path) { + Ok(0) => EmptyResponse { status: ok_status() }, + Ok(_) => EmptyResponse { status: err_status(FsOp::FSOP_RMDIR, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => EmptyResponse { status: err_status(FsOp::FSOP_RMDIR, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn fflush(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let mut fi = FileInfo::default(); + let reply = match self.fs.fflush(path, &mut fi) { + Ok(0) => EmptyResponse { status: ok_status() }, + Ok(_) => EmptyResponse { status: err_status(FsOp::FSOP_FLUSH, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => EmptyResponse { status: err_status(FsOp::FSOP_FLUSH, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn fcreate(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let mode = request.mode as u32; + let mut fi = FileInfo::default(); + let reply = match self.fs.fcreate(path, mode, &mut fi) { + Ok(0) => CreateResponse { fi: Some(fi), status: ok_status() }, + Ok(_) => CreateResponse { fi: None, status: err_status(FsOp::FSOP_CREATE, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => CreateResponse { fi: None, status: err_status(FsOp::FSOP_CREATE, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn funlink(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let reply = match self.fs.funlink(path) { + Ok(0) => EmptyResponse { status: ok_status() }, + Ok(_) => EmptyResponse { status: err_status(FsOp::FSOP_UNLINK, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => EmptyResponse { status: err_status(FsOp::FSOP_UNLINK, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn fopendir(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let mut fi = FileInfo::default(); + let reply = match self.fs.fopendir(path, &mut fi) { + Ok(0) => OpendirResponse { fi: Some(fi), status: ok_status() }, + Ok(_) => OpendirResponse { fi: None, status: err_status(FsOp::FSOP_OPENDIR, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => OpendirResponse { fi: None, status: err_status(FsOp::FSOP_OPENDIR, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn freleasedir(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let path = &self.lpath(&request.path); + let mut fi = FileInfo::default(); + let reply = match self.fs.freleasedir(path, &mut fi) { + Ok(0) => EmptyResponse { status: ok_status() }, + Ok(_) => EmptyResponse { status: err_status(FsOp::FSOP_RELEASEDIR, vec![path.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => EmptyResponse { status: err_status(FsOp::FSOP_RELEASEDIR, vec![path.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } + + async fn frename(&self, request: Request) -> Result, Status> { + let request = request.into_inner(); + let from = &self.lpath(&request.path); + let to = &self.lpath(&request.new); + let reply = match self.fs.frename(from, to) { + Ok(0) => EmptyResponse { status: ok_status() }, + Ok(_) => EmptyResponse { status: err_status(FsOp::FSOP_RENAME, vec![from.to_string(), to.to_string()], &std::io::Error::new(std::io::ErrorKind::Other, "op failed")) }, + Err(e) => EmptyResponse { status: err_status(FsOp::FSOP_RENAME, vec![from.to_string(), to.to_string()], &*e) }, + }; + Ok(Response::new(reply)) + } +} + +#[test] +fn test_config_new() { + let _ = env_logger::try_init(); + let _config = Config::new("config.json").unwrap(); + println!("Config: {:#?}", _config); +} + +#[cfg(test)] +mod test_macros { + use std::error::Error; + macro_rules! handle_result { + ($func:expr, $msg:expr) => { + match $func { + Ok(value) => value, + Err(e) => { + eprintln!("错误发生在函数:{},信息:{},错误:{}", stringify!($func), $msg, e); + -1 + } + } + }; + } + + struct MyStruct; + impl MyStruct { fn example_function(&self, _:i32) -> Result> { Err("模拟错误".into()) } } + + #[test] + fn test_() -> Result<(), Box> { + let _ = env_logger::try_init(); + let my_struct = MyStruct; + assert_ne!(handle_result!(my_struct.example_function(32), "调用 example_function 时发生错误"), 32); + Ok(()) + } +} + +#[test] +fn test_path() { + let _ = env_logger::try_init(); + let path = linux_to_windows_path(r"p:\/test/dir/tree"); + assert_eq!(path, r"p:\test\dir\tree"); + let path = linux_to_windows_path(r"p:\/"); + assert_eq!(path, r"p:\"); + let path = linux_to_windows_path(r"p:\"); + assert_eq!(path, r"p:\"); +}