From 741cbf80d1ef11ccc7c960325aa27bfe94f97bc5 Mon Sep 17 00:00:00 2001 From: Ho 229 <2189684957@qq.com> Date: Thu, 9 May 2024 06:19:59 +0800 Subject: [PATCH] refactor: mvp (#10) * refactor: mvp --------- Co-authored-by: ok-nick --- Cargo.toml | 6 +- examples/cloud-mirror/.gitignore | 2 + examples/sftp/.gitignore | 2 + examples/sftp/Cargo.toml | 5 +- examples/sftp/src/main.rs | 380 ++++++++++++++++++++++--------- src/command/commands.rs | 16 +- src/command/executor.rs | 2 +- src/ext/file.rs | 14 +- src/filter/ticket.rs | 15 ++ src/lib.rs | 12 +- src/placeholder_file.rs | 80 ++++--- src/request.rs | 74 +----- src/root/sync_root.rs | 4 +- 13 files changed, 380 insertions(+), 232 deletions(-) create mode 100644 examples/cloud-mirror/.gitignore create mode 100644 examples/sftp/.gitignore diff --git a/Cargo.toml b/Cargo.toml index f0cec26..0e76f9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -widestring = "1.0.1" +widestring = "1.0.2" memoffset = "0.6.4" windows = { version = "0.33.0", features = [ "alloc", @@ -38,5 +38,5 @@ globset = { version = "0.4.9", optional = true } globs = ["globset"] # TODO: temporarily ignored -# [workspace] -# members = ["examples/*"] +[workspace] +members = ["examples/sftp"] diff --git a/examples/cloud-mirror/.gitignore b/examples/cloud-mirror/.gitignore new file mode 100644 index 0000000..2c96eb1 --- /dev/null +++ b/examples/cloud-mirror/.gitignore @@ -0,0 +1,2 @@ +target/ +Cargo.lock diff --git a/examples/sftp/.gitignore b/examples/sftp/.gitignore new file mode 100644 index 0000000..2c96eb1 --- /dev/null +++ b/examples/sftp/.gitignore @@ -0,0 +1,2 @@ +target/ +Cargo.lock diff --git a/examples/sftp/Cargo.toml b/examples/sftp/Cargo.toml index cc09161..40d9d4b 100644 --- a/examples/sftp/Cargo.toml +++ b/examples/sftp/Cargo.toml @@ -5,7 +5,8 @@ edition = "2021" [dependencies] wincs = { path = "../../" } -widestring = "0.5.1" -ssh2 = "0.9.3" +widestring = "1.0.2" +ssh2 = "0.9.4" thiserror = "1.0.30" rkyv = "0.7.30" +ctrlc = "3.4.4" diff --git a/examples/sftp/src/main.rs b/examples/sftp/src/main.rs index 9df4c46..e73171e 100644 --- a/examples/sftp/src/main.rs +++ b/examples/sftp/src/main.rs @@ -1,19 +1,24 @@ use std::{ - ffi::OsString, + env, + ffi::OsStr, fs::{self, File}, io::{self, BufWriter, Read, Seek, SeekFrom, Write}, net::TcpStream, + os::windows::fs::OpenOptionsExt, path::Path, + sync::mpsc, }; use rkyv::{Archive, Deserialize, Serialize}; use ssh2::{Session, Sftp}; use thiserror::Error; +use widestring::{u16str, U16String}; use wincs::{ + ext::{ConvertOptions, FileExt}, filter::{info, ticket, SyncFilter}, - logger::ErrorReason, placeholder_file::{Metadata, PlaceholderFile}, request::Request, + CloudErrorKind, PopulationType, Registration, SecurityId, SyncRootIdBuilder, }; // max should be 65536, this is done both in term-scp and sshfs because it's the @@ -22,8 +27,6 @@ const DOWNLOAD_CHUNK_SIZE_BYTES: usize = 4096; // doesn't have to be 4KiB aligned const UPLOAD_CHUNK_SIZE_BYTES: usize = 4096; -const CLIENT_PATH: &str = "C:\\Users\\nicky\\Music\\sftp_client"; - const PROVIDER_NAME: &str = "wincs"; const DISPLAY_NAME: &str = "Sftp"; @@ -33,38 +36,82 @@ pub struct FileBlob { } fn main() { - let tcp = TcpStream::connect("localhost").unwrap(); + let tcp = TcpStream::connect(env::var("SERVER").unwrap()).unwrap(); let mut session = Session::new().unwrap(); session.set_blocking(true); session.set_tcp_stream(tcp); session.handshake().unwrap(); - session.userauth_agent("nicky").unwrap(); + session + .userauth_password( + &env::var("USERNAME").unwrap_or_default(), + &env::var("PASSWORD").unwrap_or_default(), + ) + .unwrap(); let sftp = session.sftp().unwrap(); - // do I need the "."? - for entry in sftp.readdir(Path::new(".")).unwrap() { - // check if it's a file or dir and set metadata/other stuff accordingly - PlaceholderFile::new() - .metadata( - Metadata::default() - // .creation_time() // either the access time or write time, whichever is less - .last_access_time(entry.1.atime.unwrap_or_default()) - .last_write_time(entry.1.mtime.unwrap_or_default()) - .change_time(entry.1.mtime.unwrap_or_default()) - .file_size(entry.1.size.unwrap_or_default()), + let sync_root_id = SyncRootIdBuilder::new(U16String::from_str(PROVIDER_NAME)) + .user_security_id(SecurityId::current_user().unwrap()) + .build(); + + let client_path = get_client_path(); + if !sync_root_id.is_registered().unwrap() { + let u16_display_name = U16String::from_str(DISPLAY_NAME); + Registration::from_sync_root_id(&sync_root_id) + .display_name(&u16_display_name) + .hydration_type(wincs::HydrationType::Full) + .population_type(PopulationType::Full) + .icon( + U16String::from_str("%SystemRoot%\\system32\\charmap.exe"), + 0, ) - .disable_on_demand_population(true) - .mark_in_sync(true) // need this? - .blob::<_, 100>(FileBlob { - relative_path: entry.0.as_os_str().to_owned().to_string_lossy().to_string(), - }) - .unwrap() // when moved to a recursive function change this - .create(Path::new(CLIENT_PATH).join(entry.0.file_name().unwrap())) + .version(u16str!("1.0.0")) + .recycle_bin_uri(u16str!("http://cloudmirror.example.com/recyclebin")) + .register(Path::new(&client_path)) .unwrap(); } - // TODO: Periodically check for changes on the server and check pin state + convert_to_placeholder(Path::new(&client_path)); + + let connection = wincs::Session::new() + .connect(&client_path, Filter { sftp }) + .unwrap(); + + wait_for_ctrlc(); + + connection.disconnect().unwrap(); + sync_root_id.unregister().unwrap(); +} + +fn get_client_path() -> String { + env::var("CLIENT_PATH").unwrap() +} + +fn convert_to_placeholder(path: &Path) { + for entry in path.read_dir().unwrap() { + let entry = entry.unwrap(); + let is_dir = entry.path().is_dir(); + + let mut open_options = File::options(); + open_options.read(true); + if is_dir { + // FILE_FLAG_BACKUP_SEMANTICS, needed to obtain handle to directory + open_options.custom_flags(0x02000000); + } + + let convert_options = if is_dir { + ConvertOptions::default().has_children() + } else { + ConvertOptions::default() + }; + + let file = open_options.open(entry.path()).unwrap(); + file.to_placeholder(convert_options).unwrap(); + + if is_dir { + convert_to_placeholder(&entry.path()); + } + } } pub struct Filter { @@ -130,102 +177,229 @@ impl Filter { // TODO: everything is just forwarded to external functions... This should be // changed in the wrapper api impl SyncFilter for Filter { - type Error = SftpError; - // TODO: handle unwraps - fn fetch_data(&self, request: Request, info: info::FetchData) -> Result<(), Self::Error> { + fn fetch_data(&self, request: Request, ticket: ticket::FetchData, info: info::FetchData) { + println!("fetch_data {:?}", request.file_blob()); // TODO: handle unwrap - let path = - Path::new(unsafe { &request.file_blob::().unwrap().relative_path }); + let path = Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(request.file_blob()) }); let range = info.required_file_range(); let end = range.end; let mut position = range.start; - let mut server_file = self.sftp.open(path)?; - let mut client_file = BufWriter::with_capacity(4096, request.placeholder()); + // TODO: allow callback to return Result in SyncFilter + let res = || -> Result<(), _> { + let mut server_file = self + .sftp + .open(path) + .map_err(|_| CloudErrorKind::InvalidRequest)?; + let mut client_file = BufWriter::with_capacity(4096, request.placeholder()); + + server_file + .seek(SeekFrom::Start(position)) + .map_err(|_| CloudErrorKind::InvalidRequest)?; + client_file + .seek(SeekFrom::Start(position)) + .map_err(|_| CloudErrorKind::InvalidRequest)?; + + let mut buffer = [0; DOWNLOAD_CHUNK_SIZE_BYTES]; + + // TODO: move to a func and remove unwraps & allow to split up the entire read + // into segments done on separate threads + // transfer the data in chunks + loop { + client_file.get_ref().set_progress(end, position).unwrap(); + + // TODO: read directly to the BufWriters buffer + // TODO: ignore if the error was just interrupted + let bytes_read = server_file + .read(&mut buffer[0..DOWNLOAD_CHUNK_SIZE_BYTES]) + .map_err(|_| CloudErrorKind::InvalidRequest)?; + let bytes_written = client_file + .write(&buffer[0..bytes_read]) + .map_err(|_| CloudErrorKind::InvalidRequest)?; + position += bytes_written as u64; + + if position >= end { + break; + } + } - server_file.seek(SeekFrom::Start(position))?; - client_file.seek(SeekFrom::Start(position))?; + client_file + .flush() + .map_err(|_| CloudErrorKind::InvalidRequest)?; - let mut buffer = [0; DOWNLOAD_CHUNK_SIZE_BYTES]; + Ok(()) + }(); - // TODO: move to a func and remove unwraps & allow to split up the entire read - // into segments done on separate threads - // transfer the data in chunks - loop { - client_file.get_ref().set_progress(end, position).unwrap(); + if let Err(e) = res { + ticket.fail(e).unwrap(); + } + } - // TODO: read directly to the BufWriters buffer - // TODO: ignore if the error was just interrupted - let bytes_read = server_file.read(&mut buffer[0..DOWNLOAD_CHUNK_SIZE_BYTES])?; - let bytes_written = client_file.write(&buffer[0..bytes_read])?; - position += bytes_written as u64; + fn deleted(&self, _request: Request, _info: info::Deleted) { + println!("deleted"); + } - if position >= end { - break; + // TODO: I probably also have to delete the file from the disk + fn delete(&self, request: Request, ticket: ticket::Delete, info: info::Delete) { + println!("delete {:?}", request.path()); + let path = Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(request.file_blob()) }); + let res = || -> Result<(), _> { + match info.is_directory() { + true => self + .remove_dir_all(path) + .map_err(|_| CloudErrorKind::InvalidRequest)?, + false => self + .sftp + .unlink(path) + .map_err(|_| CloudErrorKind::InvalidRequest)?, } + ticket.pass().unwrap(); + Ok(()) + }(); + + if let Err(e) = res { + ticket.fail(e).unwrap(); } + } - client_file.flush()?; + // TODO: Do I have to move the file and set the file progress? or does the OS + // handle that? (I think I do) + fn rename(&self, request: Request, ticket: ticket::Rename, info: info::Rename) { + let res = || -> Result<(), _> { + match info.target_in_scope() { + true => { + // TODO: path should auto include the drive letter + let src = request.path(); + // TODO: should be relative + let dest = info.target_path(); + + match info.source_in_scope() { + // TODO: use fs::copy or fs::rename, whatever it is to move the local files, + // then use ConvertToPlaceholder. I'm not sure if I have to do this recursively + // for each file or only the top-level folder TODO: which + // rename flags do I use? how do I know if I should be overwriting? + true => self + .sftp + .rename(&src, &dest, None) + .map_err(|_| CloudErrorKind::InvalidRequest)?, + false => match info.is_directory() { + true => self + .create_dir_all(&src, &dest) + .map_err(|_| CloudErrorKind::InvalidRequest)?, + false => self + .create_file(&src, &dest) + .map_err(|_| CloudErrorKind::InvalidRequest)?, + }, + } + } + // TODO: do I need to delete it locally? + false => self + .sftp + .unlink(Path::new(unsafe { + OsStr::from_encoded_bytes_unchecked(request.file_blob()) + })) + .map_err(|_| CloudErrorKind::InvalidRequest)?, + } + ticket.pass().unwrap(); + Ok(()) + }(); - Ok(()) + if let Err(e) = res { + ticket.fail(e).unwrap(); + } } - // TODO: I probably also have to delete the file from the disk - fn delete( + fn fetch_placeholders( &self, request: Request, - ticket: ticket::Delete, - info: info::Delete, - ) -> Result<(), Self::Error> { - let path = Path::new(unsafe { request.file_blob::() }); - match info.is_directory() { - true => self.remove_dir_all(path)?, - false => self.sftp.unlink(path)?, - } + ticket: ticket::FetchPlaceholders, + info: info::FetchPlaceholders, + ) { + println!( + "fetch_placeholders {:?} {:?}", + request.path(), + info.pattern() + ); + let absolute = request.path(); + let client_path = get_client_path(); + let parent = absolute.strip_prefix(&client_path).unwrap(); + + let dirs = self.sftp.readdir(parent).unwrap(); + let placeholders = dirs + .iter() + .filter(|(path, _)| !Path::new(&client_path).join(path).exists()) + .map(|(path, stat)| { + println!("path: {:?}, stat {:?}", path, stat); + println!("is file: {}, is dir: {}", stat.is_file(), stat.is_dir()); + + let relative_path = path.strip_prefix(parent).unwrap(); + PlaceholderFile::new(relative_path) + .metadata( + if stat.is_dir() { + Metadata::directory() + } else { + Metadata::file() + } + .size(stat.size.unwrap_or_default()) + // .creation_time() // either the access time or write time, whichever is less + .last_access_time(stat.atime.unwrap_or_default()) + .last_write_time(stat.mtime.unwrap_or_default()) + .change_time(stat.mtime.unwrap_or_default()), + ) + .overwrite() + // .mark_sync() // need this? + .blob(path.as_os_str().as_encoded_bytes()) + }) + .collect::>(); - Ok(()) + ticket.pass_with_placeholder(&placeholders).unwrap(); } - // TODO: Do I have to move the file and set the file progress? or does the OS - // handle that? (I think I do) - fn rename( + fn closed(&self, request: Request, info: info::Closed) { + println!("closed {:?}, deleted {}", request.path(), info.deleted()); + } + + fn cancel_fetch_data(&self, _request: Request, _info: info::CancelFetchData) { + println!("cancel_fetch_data"); + } + + fn validate_data( &self, - request: Request, - ticket: ticket::Rename, - info: info::Rename, - ) -> Result<(), Self::Error> { - match info.target_in_scope() { - true => { - // TODO: path should auto include the drive letter - let src = request.path(); - // TODO: should be relative - let dest = info.target_path(); - - match info.source_in_scope() { - // TODO: use fs::copy or fs::rename, whatever it is to move the local files, - // then use CovertToPlaceholder. I'm not sure if I have to do this recursively - // for each file or only the top-level folder TODO: which - // rename flags do I use? how do I know if I should be overwriting? - true => self.sftp.rename(&src, &dest, None)?, - false => match info.is_directory() { - true => self.create_dir_all(&src, &dest)?, - false => self.create_file(&src, &dest)?, - }, - } - } - // TODO: do I need to delete it locally? - false => self - .sftp - .unlink(Path::new(unsafe { request.file_blob::() }))?, + _request: Request, + ticket: ticket::ValidateData, + _info: info::ValidateData, + ) { + println!("validate_data"); + #[allow(unused_must_use)] + { + ticket.fail(CloudErrorKind::NotSupported); } + } - Ok(()) + fn cancel_fetch_placeholders(&self, _request: Request, _info: info::CancelFetchPlaceholders) { + println!("cancel_fetch_placeholders"); } - fn closed(&self, request: Request, info: info::Closed) -> Result<(), Self::Error> { - Ok(()) + fn opened(&self, request: Request, _info: info::Opened) { + println!("opened: {:?}", request.path()); + } + + fn dehydrate(&self, _request: Request, ticket: ticket::Dehydrate, _info: info::Dehydrate) { + println!("dehydrate"); + #[allow(unused_must_use)] + { + ticket.fail(CloudErrorKind::NotSupported); + } + } + + fn dehydrated(&self, _request: Request, _info: info::Dehydrated) { + println!("dehydrated"); + } + + fn renamed(&self, _request: Request, _info: info::Renamed) { + println!("renamed"); } // TODO: acknowledgement callbacks @@ -240,19 +414,13 @@ pub enum SftpError { Sftp(#[from] ssh2::Error), } -impl ErrorReason for SftpError { - fn code(&self) -> u32 { - 0 - } +fn wait_for_ctrlc() { + let (tx, rx) = mpsc::channel(); - fn message(&self) -> &widestring::U16Str { - match self { - SftpError::Io(_) => todo!(), - SftpError::Sftp(_) => todo!(), - } - } + ctrlc::set_handler(move || { + tx.send(()).unwrap(); + }) + .expect("Error setting Ctrl-C handler"); - fn title(&self) -> &widestring::U16Str { - todo!() - } + rx.recv().unwrap(); } diff --git a/src/command/commands.rs b/src/command/commands.rs index 6a428e2..58d8fb1 100644 --- a/src/command/commands.rs +++ b/src/command/commands.rs @@ -151,7 +151,7 @@ impl Command for Update<'_> { #[derive(Debug, Clone, Default)] pub struct CreatePlaceholders<'a> { /// The placeholders to create. - pub placeholders: &'a [PlaceholderFile<'a>], + pub placeholders: &'a [PlaceholderFile<'a>], // FIXME: placeholder should be mutable /// The total amount of placeholders that are a child of the current directory. pub total: u64, } @@ -165,6 +165,10 @@ impl Command for CreatePlaceholders<'_> { unsafe fn result(info: CF_OPERATION_PARAMETERS_0) -> Self::Result { // iterate over the placeholders and return, in a new vector, whether or // not they were created with their new USN + if info.TransferPlaceholders.PlaceholderCount == 0 { + return vec![]; + } + slice::from_raw_parts( info.TransferPlaceholders.PlaceholderArray, info.TransferPlaceholders.PlaceholderCount as usize, @@ -182,10 +186,16 @@ impl Command for CreatePlaceholders<'_> { fn build(&self) -> CF_OPERATION_PARAMETERS_0 { CF_OPERATION_PARAMETERS_0 { TransferPlaceholders: CF_OPERATION_PARAMETERS_0_7 { - Flags: CloudFilters::CF_OPERATION_TRANSFER_PLACEHOLDERS_FLAG_NONE, + // TODO: this flag tells the system there are no more placeholders in this directory (when that can be untrue) + // in the future, implement streaming + Flags: CloudFilters::CF_OPERATION_TRANSFER_PLACEHOLDERS_FLAG_DISABLE_ON_DEMAND_POPULATION, CompletionStatus: Foundation::STATUS_SUCCESS, PlaceholderTotalCount: self.total as i64, - PlaceholderArray: self.placeholders.as_ptr() as *mut _, + PlaceholderArray: match self.placeholders.is_empty() { + // If the slice is empty there will be an invalid memory access error, not with null pointers, however. + true => ptr::null_mut(), + false => self.placeholders.as_ptr() as *mut _, + }, PlaceholderCount: self.placeholders.len() as u32, EntriesProcessed: 0, }, diff --git a/src/command/executor.rs b/src/command/executor.rs index ed8e8d9..c3af841 100644 --- a/src/command/executor.rs +++ b/src/command/executor.rs @@ -65,7 +65,7 @@ pub fn execute( Type: C::OPERATION, ConnectionKey: CF_CONNECTION_KEY(connection_key), TransferKey: transfer_key, - CorrelationVector: ptr::null_mut(), + CorrelationVector: ptr::null(), SyncStatus: ptr::null(), // https://docs.microsoft.com/en-us/answers/questions/749979/what-is-a-requestkey-cfapi.html RequestKey: CloudFilters::CF_REQUEST_KEY_DEFAULT as i64, diff --git a/src/ext/file.rs b/src/ext/file.rs index f339f21..fd85a7f 100644 --- a/src/ext/file.rs +++ b/src/ext/file.rs @@ -264,8 +264,9 @@ pub trait FileExt: AsRawHandle { todo!() } + #[allow(clippy::missing_safety_doc)] /// Gets various characteristics of a placeholder using the passed blob size. - fn sync_root_info_unchecked(&self, blob_size: usize) -> core::Result { + unsafe fn sync_root_info_unchecked(&self, blob_size: usize) -> core::Result { let mut data = vec![0; mem::size_of::() + blob_size]; unsafe { @@ -602,14 +603,12 @@ impl<'a> ConvertOptions<'a> { self } - /// Marks the placeholder as having no child placeholders on creation. - /// - /// If [PopulationType::Full][crate::PopulationType] is specified on registration, this flag - /// will prevent [SyncFilter::fetch_placeholders][crate::SyncFilter::fetch_placeholders] from - /// being called for this placeholder. + // TODO: make the name of this function more specific + /// Marks the placeholder as "partially full," such that [SyncFilter::fetch_placeholders][crate::SyncFilter::fetch_placeholders] + /// will be invoked when this directory is next accessed so that the remaining placeholders are inserted. /// /// Only applicable to placeholder directories. - pub fn has_no_children(mut self) -> Self { + pub fn has_children(mut self) -> Self { self.flags |= CloudFilters::CF_CONVERT_FLAG_ENABLE_ON_DEMAND_POPULATION; self } @@ -668,7 +667,6 @@ pub struct UpdateOptions<'a> { } impl<'a> UpdateOptions<'a> { - /// pub fn metadata(mut self, metadata: Metadata) -> Self { self.metadata = Some(metadata); self diff --git a/src/filter/ticket.rs b/src/filter/ticket.rs index 59a18f2..aafcfcc 100644 --- a/src/filter/ticket.rs +++ b/src/filter/ticket.rs @@ -6,6 +6,7 @@ use crate::{ command::{self, Command, Fallible}, error::CloudErrorKind, request::{RawConnectionKey, RawTransferKey}, + PlaceholderFile, Usn, }; /// A ticket for the [SyncFilter::fetch_data][crate::SyncFilter::fetch_data] callback. @@ -76,6 +77,20 @@ impl FetchPlaceholders { } } + /// Creates a list of placeholder files/directorys on the file system. + /// + /// The value returned is the final [Usn][crate::Usn] (and if they succeeded) after each placeholder is created. + pub fn pass_with_placeholder<'a>( + &self, + placeholders: &'a [PlaceholderFile<'a>], + ) -> core::Result>> { + command::CreatePlaceholders { + placeholders, + total: placeholders.len() as _, + } + .execute(self.connection_key, self.transfer_key) + } + /// Fail the callback with the specified error. pub fn fail(&self, error_kind: CloudErrorKind) -> core::Result<()> { command::CreatePlaceholders::fail(self.connection_key, self.transfer_key, error_kind) diff --git a/src/lib.rs b/src/lib.rs index e024740..8effa2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,12 +7,12 @@ pub mod command; mod error; /// Contains traits extending common structs from the [std][std]. pub mod ext; -mod filter; -mod placeholder; -mod placeholder_file; -mod request; -mod root; -mod usn; +pub mod filter; +pub mod placeholder; +pub mod placeholder_file; +pub mod request; +pub mod root; +pub mod usn; mod utility; pub use error::CloudErrorKind; diff --git a/src/placeholder_file.rs b/src/placeholder_file.rs index 0c69b5d..1ebd5a4 100644 --- a/src/placeholder_file.rs +++ b/src/placeholder_file.rs @@ -2,14 +2,14 @@ use std::{fs, marker::PhantomData, os::windows::prelude::MetadataExt, path::Path use widestring::U16CString; use windows::{ - core, + core::{self, PCWSTR}, Win32::{ Foundation, Storage::{ CloudFilters::{ self, CfCreatePlaceholders, CF_FS_METADATA, CF_PLACEHOLDER_CREATE_INFO, }, - FileSystem::FILE_BASIC_INFO, + FileSystem::{FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_NORMAL, FILE_BASIC_INFO}, }, }, }; @@ -18,13 +18,26 @@ use crate::usn::Usn; // TODO: this struct could probably have a better name to represent files/dirs /// A builder for creating new placeholder files/directories. +#[repr(C)] #[derive(Debug)] pub struct PlaceholderFile<'a>(CF_PLACEHOLDER_CREATE_INFO, PhantomData<&'a ()>); impl<'a> PlaceholderFile<'a> { /// Creates a new [PlaceholderFile][crate::PlaceholderFile]. - pub fn new() -> Self { - Self::default() + pub fn new(relative_path: impl AsRef) -> Self { + Self( + CF_PLACEHOLDER_CREATE_INFO { + RelativeFileName: PCWSTR( + U16CString::from_os_str(relative_path.as_ref()) + .unwrap() + .into_raw(), + ), + Flags: CloudFilters::CF_PLACEHOLDER_CREATE_FLAG_NONE, + Result: Foundation::S_OK, + ..Default::default() + }, + PhantomData, + ) } /// Marks this [PlaceholderFile][crate::PlaceholderFile] as having no child placeholders on @@ -101,19 +114,12 @@ impl<'a> PlaceholderFile<'a> { /// [FileExt::to_placeholder][crate::ext::FileExt::to_placeholder] for efficiency purposes. If you /// need to create multiple placeholders, consider using [BatchCreate][crate::BatchCreate]. /// - /// If you need to create placeholders from a callback, do not use this method. Instead, use - /// [Request::create_placeholder][crate::Request::create_placeholder]. - pub fn create>(mut self, path: P) -> core::Result { - let path = path.as_ref(); - - // TODO: handle unwraps - let mut file_name = U16CString::from_os_str(path.file_name().unwrap()).unwrap(); - self.0.RelativeFileName.0 = file_name.as_mut_ptr(); - + /// If you need to create placeholders from the [SyncFilter::fetch_placeholders][crate::SyncFilter::fetch_placeholders] callback, do not use this method. Instead, use + /// [FetchPlaceholders::pass_with_placeholders][crate::ticket::FetchPlaceholders::pass_with_placeholders]. + pub fn create>(mut self, parent: impl AsRef) -> core::Result { unsafe { CfCreatePlaceholders( - // TODO: handle unwrap - path.parent().unwrap().as_os_str(), + parent.as_ref().as_os_str(), &mut self as *mut _ as *mut _, 1, CloudFilters::CF_CREATE_FLAG_NONE, @@ -125,21 +131,10 @@ impl<'a> PlaceholderFile<'a> { } } -impl Default for PlaceholderFile<'_> { - fn default() -> Self { - Self( - CF_PLACEHOLDER_CREATE_INFO { - RelativeFileName: Default::default(), - FsMetadata: Default::default(), - FileIdentity: ptr::null_mut(), - // this is required only for files, who knows why - FileIdentityLength: 1, - Flags: CloudFilters::CF_PLACEHOLDER_CREATE_FLAG_NONE, - Result: Foundation::S_OK, - CreateUsn: 0, - }, - PhantomData, - ) +impl Drop for PlaceholderFile<'_> { + fn drop(&mut self) { + // Safety: `self.0.RelativeFileName.0` is a valid pointer to a valid UTF-16 string + drop(unsafe { U16CString::from_ptr_str(self.0.RelativeFileName.0) }) } } @@ -174,13 +169,28 @@ impl BatchCreate for [PlaceholderFile<'_>] { } /// The metadata for a [PlaceholderFile][crate::PlaceholderFile]. -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub struct Metadata(pub(crate) CF_FS_METADATA); impl Metadata { - /// Creates a new [Metadata][crate::Metadata]. - pub fn new() -> Self { - Self::default() + pub fn file() -> Self { + Self(CF_FS_METADATA { + BasicInfo: FILE_BASIC_INFO { + FileAttributes: FILE_ATTRIBUTE_NORMAL.0, + ..Default::default() + }, + ..Default::default() + }) + } + + pub fn directory() -> Self { + Self(CF_FS_METADATA { + BasicInfo: FILE_BASIC_INFO { + FileAttributes: FILE_ATTRIBUTE_DIRECTORY.0, + ..Default::default() + }, + ..Default::default() + }) } /// The time the file/directory was created. @@ -216,7 +226,7 @@ impl Metadata { // TODO: create a method for specifying that it's a directory. /// File attributes. pub fn attributes(mut self, attributes: u32) -> Self { - self.0.BasicInfo.FileAttributes = attributes; + self.0.BasicInfo.FileAttributes |= attributes; self } } diff --git a/src/request.rs b/src/request.rs index b9bc550..9433195 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,17 +1,9 @@ use std::{path::PathBuf, slice}; use widestring::{U16CStr, U16CString}; -use windows::{ - core, - Win32::Storage::CloudFilters::{CF_CALLBACK_INFO, CF_PROCESS_INFO}, -}; - -use crate::{ - command::{Command, CreatePlaceholders}, - placeholder::Placeholder, - placeholder_file::PlaceholderFile, - usn::Usn, -}; +use windows::Win32::Storage::CloudFilters::{CF_CALLBACK_INFO, CF_PROCESS_INFO}; + +use crate::placeholder::Placeholder; pub type RawConnectionKey = isize; pub type RawTransferKey = i64; @@ -91,9 +83,11 @@ impl Request { /// [Read here for more information on this /// function.](https://docs.microsoft.com/en-us/windows/win32/api/cfapi/ns-cfapi-cf_callback_info#remarks) pub fn path(&self) -> PathBuf { - unsafe { U16CStr::from_ptr_str(self.0.NormalizedPath.0) } - .to_os_string() - .into() + let mut path = + PathBuf::from(unsafe { U16CStr::from_ptr_str(self.0.VolumeDosName.0) }.to_os_string()); + path.push(unsafe { U16CStr::from_ptr_str(self.0.NormalizedPath.0) }.to_os_string()); + + path } /// A numeric scale ranging from @@ -152,58 +146,6 @@ impl Request { /// activity (meaning, no placeholder methods are invoked). If you are prone to this issue, /// consider calling this method or call placeholder methods more frequently. pub fn reset_timeout() {} - - /// Creates a placeholder file under the current placeholder directory. - /// - /// This function will fail if the placeholder associated with the current callback is not a - /// directory. Use [Request::path][crate::Request::path] to identify if the placeholder is a - /// directory. - #[inline] - pub fn create_placeholder(&self, placeholder: PlaceholderFile) -> core::Result { - self.create_placeholders(&[placeholder]) - .map(|mut x| x.remove(0))? - } - - /// Creates multiple placeholder files at once. The returned list contains the resulting - /// [Usn][crate::Usn] wrapped in a [Result][std::result::Result] to signify whether or not the - /// placeholder was created successfully. - /// - /// This function will fail if the placeholder associated with the current callback is not a - /// directory. Use the [Request::path][crate::Request::path] method to identify if the - /// placeholder is a directory. - #[inline] - pub fn create_placeholders( - &self, - placeholders: &[PlaceholderFile], - ) -> core::Result>> { - self.create_placeholders_with_total(placeholders, placeholders.len() as u64) - } - - /// Creates multiple placeholder files at once. The returned list contains the resulting - /// [Usn][crate::Usn] wrapped in a [Result][std::result::Result] to signify whether or not the placeholder was - /// created successfully. - /// - /// The `total` parameter specifies the total number of placeholder files that are a child of - /// the current placeholder directory. If this value is unknown or is the length of the passed - /// slice, consider calling - /// [Request::create_placeholders][crate::Request::create_placeholders]. - /// - /// This method is equivalent to calling - /// [CreatePlaceholders::execute][crate::command::CreatePlaceholders::execute]. - /// - /// This function will fail if the placeholder associated with the current callback is not a - /// directory. Use the [path][crate::Request::path] method to identify if the placeholder is a directory. - pub fn create_placeholders_with_total( - &self, - placeholders: &[PlaceholderFile], - total: u64, - ) -> core::Result>> { - CreatePlaceholders { - placeholders, - total, - } - .execute(self.connection_key(), self.transfer_key()) - } } /// Information about the calling process. diff --git a/src/root/sync_root.rs b/src/root/sync_root.rs index d757644..6feaeb3 100644 --- a/src/root/sync_root.rs +++ b/src/root/sync_root.rs @@ -59,7 +59,7 @@ impl SyncRootIdBuilder { /// [SecurityId][crate::SecurityId] struct. /// /// By default, a sync root registered without a user security id will be installed globally. - pub fn user_security_id(&mut self, security_id: SecurityId) -> &mut Self { + pub fn user_security_id(mut self, security_id: SecurityId) -> Self { self.user_security_id = security_id; self } @@ -68,7 +68,7 @@ impl SyncRootIdBuilder { /// /// This value does not have any actual meaning and it could theoretically be anything. /// However, it is encouraged to set this value to the account name of the user on the remote. - pub fn account_name(&mut self, account_name: U16String) -> &mut Self { + pub fn account_name(mut self, account_name: U16String) -> Self { self.account_name = account_name; self }