diff --git a/CHANGELOG.md b/CHANGELOG.md index b953c81..e3114b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- `Change` structure now provides a single `path` instead of `paths` with the + `distant-local` implementation sending a separate `Changed` event per path +- `ChangeDetails` now includes a `renamed` field to capture the new path name + when known + ## [0.20.0-alpha.8] ### Added diff --git a/distant-core/src/client/watcher.rs b/distant-core/src/client/watcher.rs index 57c3d01..c7f63a9 100644 --- a/distant-core/src/client/watcher.rs +++ b/distant-core/src/client/watcher.rs @@ -265,13 +265,13 @@ mod tests { protocol::Response::Changed(Change { timestamp: 0, kind: ChangeKind::Access, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), }), protocol::Response::Changed(Change { timestamp: 1, kind: ChangeKind::Modify, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), }), ], @@ -286,7 +286,7 @@ mod tests { Change { timestamp: 0, kind: ChangeKind::Access, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), } ); @@ -297,7 +297,7 @@ mod tests { Change { timestamp: 1, kind: ChangeKind::Modify, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), } ); @@ -340,7 +340,7 @@ mod tests { protocol::Response::Changed(Change { timestamp: 0, kind: ChangeKind::Access, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), }), )) @@ -354,7 +354,7 @@ mod tests { protocol::Response::Changed(Change { timestamp: 1, kind: ChangeKind::Modify, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), }), )) @@ -368,7 +368,7 @@ mod tests { protocol::Response::Changed(Change { timestamp: 2, kind: ChangeKind::Delete, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), }), )) @@ -382,7 +382,7 @@ mod tests { Change { timestamp: 0, kind: ChangeKind::Access, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), } ); @@ -393,7 +393,7 @@ mod tests { Change { timestamp: 2, kind: ChangeKind::Delete, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), } ); @@ -434,19 +434,19 @@ mod tests { protocol::Response::Changed(Change { timestamp: 0, kind: ChangeKind::Access, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), }), protocol::Response::Changed(Change { timestamp: 1, kind: ChangeKind::Modify, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), }), protocol::Response::Changed(Change { timestamp: 2, kind: ChangeKind::Delete, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), }), ], @@ -473,7 +473,7 @@ mod tests { Change { timestamp: 0, kind: ChangeKind::Access, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), } ); @@ -498,7 +498,7 @@ mod tests { protocol::Response::Changed(Change { timestamp: 3, kind: ChangeKind::Unknown, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), }), )) @@ -512,7 +512,7 @@ mod tests { Some(Change { timestamp: 1, kind: ChangeKind::Modify, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), }) ); @@ -521,7 +521,7 @@ mod tests { Some(Change { timestamp: 2, kind: ChangeKind::Delete, - paths: vec![test_path.to_path_buf()], + path: test_path.to_path_buf(), details: Default::default(), }) ); diff --git a/distant-local/src/api.rs b/distant-local/src/api.rs index eb731f3..fe6e2b9 100644 --- a/distant-local/src/api.rs +++ b/distant-local/src/api.rs @@ -426,17 +426,17 @@ impl DistantApi for Api { .accessed() .ok() .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()) - .map(|d| d.as_millis()), + .map(|d| d.as_secs()), created: metadata .created() .ok() .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()) - .map(|d| d.as_millis()), + .map(|d| d.as_secs()), modified: metadata .modified() .ok() .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()) - .map(|d| d.as_millis()), + .map(|d| d.as_secs()), len: metadata.len(), readonly: metadata.permissions().readonly(), file_type: if file_type.is_dir() { @@ -1547,29 +1547,17 @@ mod tests { } /// Validates a response as being a series of changes that include the provided paths - fn validate_changed_paths( - data: &Response, - expected_paths: &[PathBuf], - should_panic: bool, - ) -> bool { + fn validate_changed_path(data: &Response, expected_path: &Path, should_panic: bool) -> bool { match data { Response::Changed(change) if should_panic => { - let paths: Vec = change - .paths - .iter() - .map(|x| x.canonicalize().unwrap()) - .collect(); - assert_eq!(paths, expected_paths, "Wrong paths reported: {:?}", change); + let path = change.path.canonicalize().unwrap(); + assert_eq!(path, expected_path, "Wrong path reported: {:?}", change); true } Response::Changed(change) => { - let paths: Vec = change - .paths - .iter() - .map(|x| x.canonicalize().unwrap()) - .collect(); - paths == expected_paths + let path = change.path.canonicalize().unwrap(); + path == expected_path } x if should_panic => panic!("Unexpected response: {:?}", x), _ => false, @@ -1602,9 +1590,9 @@ mod tests { .recv() .await .expect("Channel closed before we got change"); - validate_changed_paths( + validate_changed_path( &data, - &[file.path().to_path_buf().canonicalize().unwrap()], + &file.path().to_path_buf().canonicalize().unwrap(), /* should_panic */ true, ); } @@ -1657,9 +1645,9 @@ mod tests { let path = file.path().to_path_buf(); assert!( - responses.iter().any(|res| validate_changed_paths( + responses.iter().any(|res| validate_changed_path( res, - &[file.path().to_path_buf().canonicalize().unwrap()], + &file.path().to_path_buf().canonicalize().unwrap(), /* should_panic */ false, )), "Missing {:?} in {:?}", @@ -1672,9 +1660,9 @@ mod tests { let path = nested_file.path().to_path_buf(); assert!( - responses.iter().any(|res| validate_changed_paths( + responses.iter().any(|res| validate_changed_path( res, - &[file.path().to_path_buf().canonicalize().unwrap()], + &file.path().to_path_buf().canonicalize().unwrap(), /* should_panic */ false, )), "Missing {:?} in {:?}", @@ -1740,9 +1728,9 @@ mod tests { .recv() .await .expect("Channel closed before we got change"); - validate_changed_paths( + validate_changed_path( &data, - &[file_1.path().to_path_buf().canonicalize().unwrap()], + &file_1.path().to_path_buf().canonicalize().unwrap(), /* should_panic */ true, ); @@ -1752,9 +1740,9 @@ mod tests { .recv() .await .expect("Channel closed before we got change"); - validate_changed_paths( + validate_changed_path( &data, - &[file_2.path().to_path_buf().canonicalize().unwrap()], + &file_2.path().to_path_buf().canonicalize().unwrap(), /* should_panic */ true, ); } diff --git a/distant-local/src/api/state/watcher.rs b/distant-local/src/api/state/watcher.rs index 7bfcee8..1e5db77 100644 --- a/distant-local/src/api/state/watcher.rs +++ b/distant-local/src/api/state/watcher.rs @@ -5,9 +5,9 @@ use std::path::{Path, PathBuf}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use distant_core::net::common::ConnectionId; -use distant_core::protocol::{Change, ChangeDetails, ChangeDetailsAttributes, ChangeKind}; +use distant_core::protocol::{Change, ChangeDetails, ChangeDetailsAttribute, ChangeKind}; use log::*; -use notify::event::{AccessKind, AccessMode, MetadataKind, ModifyKind}; +use notify::event::{AccessKind, AccessMode, MetadataKind, ModifyKind, RenameMode}; use notify::{ Config as WatcherConfig, Error as WatcherError, ErrorKind as WatcherErrorKind, Event as WatcherEvent, EventKind, PollWatcher, RecommendedWatcher, RecursiveMode, Watcher, @@ -337,33 +337,71 @@ async fn watcher_task( _ => ChangeKind::Unknown, }; - let attributes = match ev.kind { - EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)) => { - vec![ChangeDetailsAttributes::Timestamp] - } - EventKind::Modify(ModifyKind::Metadata( - MetadataKind::Ownership | MetadataKind::Permissions, - )) => vec![ChangeDetailsAttributes::Permissions], - _ => Vec::new(), - }; - for registered_path in registered_paths.iter() { - let change = Change { - timestamp, - kind, - paths: ev.paths.clone(), - details: ChangeDetails { - attributes: attributes.clone(), - extra: ev.info().map(ToString::to_string), - }, - }; - match registered_path.filter_and_send(change).await { - Ok(_) => (), - Err(x) => error!( - "[Conn {}] Failed to forward changes to paths: {}", - registered_path.id(), - x + // For rename both, we assume the paths is a pair that represents before and + // after, so we want to grab the before and use it! + let (paths, renamed): (&[PathBuf], Option) = match ev.kind { + EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => ( + &ev.paths[0..1], + if ev.paths.len() > 1 { + ev.paths.last().cloned() + } else { + None + }, ), + _ => (&ev.paths, None), + }; + + for path in paths { + let attribute = match ev.kind { + EventKind::Modify(ModifyKind::Metadata(MetadataKind::Ownership)) => { + Some(ChangeDetailsAttribute::Ownership) + } + EventKind::Modify(ModifyKind::Metadata(MetadataKind::Permissions)) => { + Some(ChangeDetailsAttribute::Permissions) + } + EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)) => { + Some(ChangeDetailsAttribute::Timestamp) + } + _ => None, + }; + + // Calculate a timestamp for creation & modification paths + let details_timestamp = match ev.kind { + EventKind::Create(_) => tokio::fs::symlink_metadata(path.as_path()) + .await + .ok() + .and_then(|m| m.created().ok()) + .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) + .map(|d| d.as_secs()), + EventKind::Modify(_) => tokio::fs::symlink_metadata(path.as_path()) + .await + .ok() + .and_then(|m| m.modified().ok()) + .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) + .map(|d| d.as_secs()), + _ => None, + }; + + let change = Change { + timestamp, + kind, + path: path.to_path_buf(), + details: ChangeDetails { + attribute, + renamed: renamed.clone(), + timestamp: details_timestamp, + extra: ev.info().map(ToString::to_string), + }, + }; + match registered_path.filter_and_send(change).await { + Ok(_) => (), + Err(x) => error!( + "[Conn {}] Failed to forward changes to paths: {}", + registered_path.id(), + x + ), + } } } } diff --git a/distant-local/src/api/state/watcher/path.rs b/distant-local/src/api/state/watcher/path.rs index ef52c4b..39dabe2 100644 --- a/distant-local/src/api/state/watcher/path.rs +++ b/distant-local/src/api/state/watcher/path.rs @@ -119,18 +119,16 @@ impl RegisteredPath { } /// Sends a reply for a change tied to this registered path, filtering - /// out any paths that are not applicable + /// out any changes that are not applicable. /// - /// Returns true if message was sent, and false if not - pub async fn filter_and_send(&self, mut change: Change) -> io::Result { + /// Returns true if message was sent, and false if not. + pub async fn filter_and_send(&self, change: Change) -> io::Result { if !self.allowed().contains(&change.kind) { return Ok(false); } - // filter the paths that are not applicable - change.paths.retain(|p| self.applies_to_path(p.as_path())); - - if !change.paths.is_empty() { + // Only send if this registered path applies to the changed path + if self.applies_to_path(&change.path) { self.reply .send(Response::Changed(change)) .await @@ -141,9 +139,9 @@ impl RegisteredPath { } /// Sends an error message and includes paths if provided, skipping sending the message if - /// no paths match and `skip_if_no_paths` is true + /// no paths match and `skip_if_no_paths` is true. /// - /// Returns true if message was sent, and false if not + /// Returns true if message was sent, and false if not. pub async fn filter_and_send_error( &self, msg: &str, diff --git a/distant-protocol/src/common/change.rs b/distant-protocol/src/common/change.rs index 13c38dc..9f5de94 100644 --- a/distant-protocol/src/common/change.rs +++ b/distant-protocol/src/common/change.rs @@ -10,7 +10,7 @@ use derive_more::{Deref, DerefMut, IntoIterator}; use serde::{Deserialize, Serialize}; use strum::{EnumString, EnumVariantNames, VariantNames}; -/// Change to one or more paths on the filesystem. +/// Change to a path on the filesystem. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case", deny_unknown_fields)] pub struct Change { @@ -22,23 +22,36 @@ pub struct Change { /// Label describing the kind of change pub kind: ChangeKind, - /// Paths that were changed - pub paths: Vec, + /// Path that was changed + pub path: PathBuf, /// Additional details associated with the change #[serde(default, skip_serializing_if = "ChangeDetails::is_empty")] pub details: ChangeDetails, } -/// Details about a change +/// Optional details about a change. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(default, rename_all = "snake_case", deny_unknown_fields)] pub struct ChangeDetails { - /// Clarity on type of attribute changes that have occurred (for kind == attribute) - #[serde(skip_serializing_if = "Vec::is_empty")] - pub attributes: Vec, + /// Clarity on type of attribute change that occurred (for kind == attribute). + #[serde(skip_serializing_if = "Option::is_none")] + pub attribute: Option, + + /// When event is renaming, this will be populated with the resulting name + /// when we know both the old and new names (for kind == rename) + #[serde(skip_serializing_if = "Option::is_none")] + pub renamed: Option, + + /// Unix timestamps (in seconds) related to the change. For other platforms, their timestamps + /// are converted into a Unix timestamp format. + /// + /// * For create events, this represents the `ctime` field from stat (or equivalent on other platforms). + /// * For modify events, this represents the `mtime` field from stat (or equivalent on other platforms). + #[serde(rename = "ts", skip_serializing_if = "Option::is_none")] + pub timestamp: Option, - /// Optional information about the change that is typically platform-specific + /// Optional information about the change that is typically platform-specific. #[serde(skip_serializing_if = "Option::is_none")] pub extra: Option, } @@ -46,14 +59,15 @@ pub struct ChangeDetails { impl ChangeDetails { /// Returns true if no details are contained within. pub fn is_empty(&self) -> bool { - self.attributes.is_empty() && self.extra.is_none() + self.attribute.is_none() && self.timestamp.is_none() && self.extra.is_none() } } /// Specific details about modification #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case", deny_unknown_fields)] -pub enum ChangeDetailsAttributes { +pub enum ChangeDetailsAttribute { + Ownership, Permissions, Timestamp, } diff --git a/distant-protocol/src/common/metadata.rs b/distant-protocol/src/common/metadata.rs index 5ddcdaa..e63bb3f 100644 --- a/distant-protocol/src/common/metadata.rs +++ b/distant-protocol/src/common/metadata.rs @@ -4,7 +4,6 @@ use bitflags::bitflags; use serde::{Deserialize, Serialize}; use crate::common::FileType; -use crate::utils::{deserialize_u128_option, serialize_u128_option}; /// Represents metadata about some path on a remote machine. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -23,41 +22,20 @@ pub struct Metadata { /// Whether or not the file/directory/symlink is marked as unwriteable. pub readonly: bool, - /// Represents the last time (in milliseconds) when the file/directory/symlink was accessed; + /// Represents the last time (in seconds) when the file/directory/symlink was accessed; /// can be optional as certain systems don't support this. - /// - /// Note that this is represented as a string and not a number when serialized! - #[serde( - default, - skip_serializing_if = "Option::is_none", - serialize_with = "serialize_u128_option", - deserialize_with = "deserialize_u128_option" - )] - pub accessed: Option, - - /// Represents when (in milliseconds) the file/directory/symlink was created; + #[serde(default, skip_serializing_if = "Option::is_none")] + pub accessed: Option, + + /// Represents when (in seconds) the file/directory/symlink was created; /// can be optional as certain systems don't support this. - /// - /// Note that this is represented as a string and not a number when serialized! - #[serde( - default, - skip_serializing_if = "Option::is_none", - serialize_with = "serialize_u128_option", - deserialize_with = "deserialize_u128_option" - )] - pub created: Option, - - /// Represents the last time (in milliseconds) when the file/directory/symlink was modified; + #[serde(default, skip_serializing_if = "Option::is_none")] + pub created: Option, + + /// Represents the last time (in seconds) when the file/directory/symlink was modified; /// can be optional as certain systems don't support this. - /// - /// Note that this is represented as a string and not a number when serialized! - #[serde( - default, - skip_serializing_if = "Option::is_none", - serialize_with = "serialize_u128_option", - deserialize_with = "deserialize_u128_option" - )] - pub modified: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub modified: Option, /// Represents metadata that is specific to a unix remote machine. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -369,9 +347,9 @@ mod tests { file_type: FileType::Dir, len: 999, readonly: true, - accessed: Some(u128::MAX), - created: Some(u128::MAX), - modified: Some(u128::MAX), + accessed: Some(u64::MAX), + created: Some(u64::MAX), + modified: Some(u64::MAX), unix: Some(UnixMetadata { owner_read: true, owner_write: false, @@ -402,10 +380,6 @@ mod tests { }), }; - // NOTE: These values are too big to normally serialize, so we have to convert them to - // a string type, which is why the value here also needs to be a string. - let max_u128_str = u128::MAX.to_string(); - let value = serde_json::to_value(metadata).unwrap(); assert_eq!( value, @@ -414,9 +388,9 @@ mod tests { "file_type": "dir", "len": 999, "readonly": true, - "accessed": max_u128_str, - "created": max_u128_str, - "modified": max_u128_str, + "accessed": u64::MAX, + "created": u64::MAX, + "modified": u64::MAX, "unix": { "owner_read": true, "owner_write": false, @@ -476,18 +450,14 @@ mod tests { #[test] fn should_be_able_to_deserialize_full_metadata_from_json() { - // NOTE: These values are too big to normally serialize, so we have to convert them to - // a string type, which is why the value here also needs to be a string. - let max_u128_str = u128::MAX.to_string(); - let value = serde_json::json!({ "canonicalized_path": "test-dir", "file_type": "dir", "len": 999, "readonly": true, - "accessed": max_u128_str, - "created": max_u128_str, - "modified": max_u128_str, + "accessed": u64::MAX, + "created": u64::MAX, + "modified": u64::MAX, "unix": { "owner_read": true, "owner_write": false, @@ -526,9 +496,9 @@ mod tests { file_type: FileType::Dir, len: 999, readonly: true, - accessed: Some(u128::MAX), - created: Some(u128::MAX), - modified: Some(u128::MAX), + accessed: Some(u64::MAX), + created: Some(u64::MAX), + modified: Some(u64::MAX), unix: Some(UnixMetadata { owner_read: true, owner_write: false, @@ -589,9 +559,9 @@ mod tests { file_type: FileType::Dir, len: 999, readonly: true, - accessed: Some(u128::MAX), - created: Some(u128::MAX), - modified: Some(u128::MAX), + accessed: Some(u64::MAX), + created: Some(u64::MAX), + modified: Some(u64::MAX), unix: Some(UnixMetadata { owner_read: true, owner_write: false, @@ -676,9 +646,9 @@ mod tests { file_type: FileType::Dir, len: 999, readonly: true, - accessed: Some(u128::MAX), - created: Some(u128::MAX), - modified: Some(u128::MAX), + accessed: Some(u64::MAX), + created: Some(u64::MAX), + modified: Some(u64::MAX), unix: Some(UnixMetadata { owner_read: true, owner_write: false, @@ -718,9 +688,9 @@ mod tests { file_type: FileType::Dir, len: 999, readonly: true, - accessed: Some(u128::MAX), - created: Some(u128::MAX), - modified: Some(u128::MAX), + accessed: Some(u64::MAX), + created: Some(u64::MAX), + modified: Some(u64::MAX), unix: Some(UnixMetadata { owner_read: true, owner_write: false, diff --git a/distant-protocol/src/response.rs b/distant-protocol/src/response.rs index 1d98afb..8470ce1 100644 --- a/distant-protocol/src/response.rs +++ b/distant-protocol/src/response.rs @@ -615,14 +615,14 @@ mod tests { use std::path::PathBuf; use super::*; - use crate::common::{ChangeDetails, ChangeDetailsAttributes, ChangeKind}; + use crate::common::{ChangeDetails, ChangeDetailsAttribute, ChangeKind}; #[test] fn should_be_able_to_serialize_minimal_payload_to_json() { let payload = Response::Changed(Change { timestamp: u64::MAX, kind: ChangeKind::Access, - paths: vec![PathBuf::from("path")], + path: PathBuf::from("path"), details: ChangeDetails::default(), }); @@ -633,7 +633,7 @@ mod tests { "type": "changed", "ts": u64::MAX, "kind": "access", - "paths": ["path"], + "path": "path", }) ); } @@ -643,9 +643,11 @@ mod tests { let payload = Response::Changed(Change { timestamp: u64::MAX, kind: ChangeKind::Access, - paths: vec![PathBuf::from("path")], + path: PathBuf::from("path"), details: ChangeDetails { - attributes: vec![ChangeDetailsAttributes::Permissions], + attribute: Some(ChangeDetailsAttribute::Permissions), + renamed: Some(PathBuf::from("renamed")), + timestamp: Some(u64::MAX), extra: Some(String::from("info")), }, }); @@ -657,9 +659,11 @@ mod tests { "type": "changed", "ts": u64::MAX, "kind": "access", - "paths": ["path"], + "path": "path", "details": { - "attributes": ["permissions"], + "attribute": "permissions", + "renamed": "renamed", + "ts": u64::MAX, "extra": "info", }, }) @@ -672,7 +676,7 @@ mod tests { "type": "changed", "ts": u64::MAX, "kind": "access", - "paths": ["path"], + "path": "path", }); let payload: Response = serde_json::from_value(value).unwrap(); @@ -681,7 +685,7 @@ mod tests { Response::Changed(Change { timestamp: u64::MAX, kind: ChangeKind::Access, - paths: vec![PathBuf::from("path")], + path: PathBuf::from("path"), details: ChangeDetails::default(), }) ); @@ -693,9 +697,11 @@ mod tests { "type": "changed", "ts": u64::MAX, "kind": "access", - "paths": ["path"], + "path": "path", "details": { - "attributes": ["permissions"], + "attribute": "permissions", + "renamed": "renamed", + "ts": u64::MAX, "extra": "info", }, }); @@ -706,9 +712,11 @@ mod tests { Response::Changed(Change { timestamp: u64::MAX, kind: ChangeKind::Access, - paths: vec![PathBuf::from("path")], + path: PathBuf::from("path"), details: ChangeDetails { - attributes: vec![ChangeDetailsAttributes::Permissions], + attribute: Some(ChangeDetailsAttribute::Permissions), + renamed: Some(PathBuf::from("renamed")), + timestamp: Some(u64::MAX), extra: Some(String::from("info")), }, }) @@ -720,7 +728,7 @@ mod tests { let payload = Response::Changed(Change { timestamp: u64::MAX, kind: ChangeKind::Access, - paths: vec![PathBuf::from("path")], + path: PathBuf::from("path"), details: ChangeDetails::default(), }); @@ -736,9 +744,11 @@ mod tests { let payload = Response::Changed(Change { timestamp: u64::MAX, kind: ChangeKind::Access, - paths: vec![PathBuf::from("path")], + path: PathBuf::from("path"), details: ChangeDetails { - attributes: vec![ChangeDetailsAttributes::Permissions], + attribute: Some(ChangeDetailsAttribute::Permissions), + renamed: Some(PathBuf::from("renamed")), + timestamp: Some(u64::MAX), extra: Some(String::from("info")), }, }); @@ -759,7 +769,7 @@ mod tests { let buf = rmp_serde::encode::to_vec_named(&Response::Changed(Change { timestamp: u64::MAX, kind: ChangeKind::Access, - paths: vec![PathBuf::from("path")], + path: PathBuf::from("path"), details: ChangeDetails::default(), })) .unwrap(); @@ -770,7 +780,7 @@ mod tests { Response::Changed(Change { timestamp: u64::MAX, kind: ChangeKind::Access, - paths: vec![PathBuf::from("path")], + path: PathBuf::from("path"), details: ChangeDetails::default(), }) ); @@ -785,9 +795,11 @@ mod tests { let buf = rmp_serde::encode::to_vec_named(&Response::Changed(Change { timestamp: u64::MAX, kind: ChangeKind::Access, - paths: vec![PathBuf::from("path")], + path: PathBuf::from("path"), details: ChangeDetails { - attributes: vec![ChangeDetailsAttributes::Permissions], + attribute: Some(ChangeDetailsAttribute::Permissions), + renamed: Some(PathBuf::from("renamed")), + timestamp: Some(u64::MAX), extra: Some(String::from("info")), }, })) @@ -799,9 +811,11 @@ mod tests { Response::Changed(Change { timestamp: u64::MAX, kind: ChangeKind::Access, - paths: vec![PathBuf::from("path")], + path: PathBuf::from("path"), details: ChangeDetails { - attributes: vec![ChangeDetailsAttributes::Permissions], + attribute: Some(ChangeDetailsAttribute::Permissions), + renamed: Some(PathBuf::from("renamed")), + timestamp: Some(u64::MAX), extra: Some(String::from("info")), }, }) @@ -900,9 +914,9 @@ mod tests { file_type: FileType::File, len: u64::MAX, readonly: true, - accessed: Some(u128::MAX), - created: Some(u128::MAX), - modified: Some(u128::MAX), + accessed: Some(u64::MAX), + created: Some(u64::MAX), + modified: Some(u64::MAX), unix: Some(UnixMetadata { owner_read: true, owner_write: false, @@ -933,10 +947,6 @@ mod tests { }), }); - // NOTE: These values are too big to normally serialize, so we have to convert them to - // a string type, which is why the value here also needs to be a string. - let u128_max_str = u128::MAX.to_string(); - let value = serde_json::to_value(payload).unwrap(); assert_eq!( value, @@ -946,9 +956,9 @@ mod tests { "file_type": "file", "len": u64::MAX, "readonly": true, - "accessed": u128_max_str, - "created": u128_max_str, - "modified": u128_max_str, + "accessed": u64::MAX, + "created": u64::MAX, + "modified": u64::MAX, "unix": { "owner_read": true, "owner_write": false, @@ -1009,16 +1019,15 @@ mod tests { #[test] fn should_be_able_to_deserialize_full_payload_from_json() { - let u128_max_str = u128::MAX.to_string(); let value = serde_json::json!({ "type": "metadata", "canonicalized_path": "path", "file_type": "file", "len": u64::MAX, "readonly": true, - "accessed": u128_max_str, - "created": u128_max_str, - "modified": u128_max_str, + "accessed": u64::MAX, + "created": u64::MAX, + "modified": u64::MAX, "unix": { "owner_read": true, "owner_write": false, @@ -1057,9 +1066,9 @@ mod tests { file_type: FileType::File, len: u64::MAX, readonly: true, - accessed: Some(u128::MAX), - created: Some(u128::MAX), - modified: Some(u128::MAX), + accessed: Some(u64::MAX), + created: Some(u64::MAX), + modified: Some(u64::MAX), unix: Some(UnixMetadata { owner_read: true, owner_write: false, @@ -1120,9 +1129,9 @@ mod tests { file_type: FileType::File, len: u64::MAX, readonly: true, - accessed: Some(u128::MAX), - created: Some(u128::MAX), - modified: Some(u128::MAX), + accessed: Some(u64::MAX), + created: Some(u64::MAX), + modified: Some(u64::MAX), unix: Some(UnixMetadata { owner_read: true, owner_write: false, @@ -1207,9 +1216,9 @@ mod tests { file_type: FileType::File, len: u64::MAX, readonly: true, - accessed: Some(u128::MAX), - created: Some(u128::MAX), - modified: Some(u128::MAX), + accessed: Some(u64::MAX), + created: Some(u64::MAX), + modified: Some(u64::MAX), unix: Some(UnixMetadata { owner_read: true, owner_write: false, @@ -1249,9 +1258,9 @@ mod tests { file_type: FileType::File, len: u64::MAX, readonly: true, - accessed: Some(u128::MAX), - created: Some(u128::MAX), - modified: Some(u128::MAX), + accessed: Some(u64::MAX), + created: Some(u64::MAX), + modified: Some(u64::MAX), unix: Some(UnixMetadata { owner_read: true, owner_write: false, diff --git a/distant-protocol/src/utils.rs b/distant-protocol/src/utils.rs index 7902654..9a84ede 100644 --- a/distant-protocol/src/utils.rs +++ b/distant-protocol/src/utils.rs @@ -1,5 +1,3 @@ -use serde::{Deserialize, Serialize}; - /// Used purely for skipping serialization of values that are false by default. #[inline] pub const fn is_false(value: &bool) -> bool { @@ -17,28 +15,3 @@ pub const fn is_one(value: &usize) -> bool { pub const fn one() -> usize { 1 } - -pub fn deserialize_u128_option<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - match Option::::deserialize(deserializer)? { - Some(s) => match s.parse::() { - Ok(value) => Ok(Some(value)), - Err(error) => Err(serde::de::Error::custom(format!( - "Cannot convert to u128 with error: {error:?}" - ))), - }, - None => Ok(None), - } -} - -pub fn serialize_u128_option( - val: &Option, - s: S, -) -> Result { - match val { - Some(v) => format!("{}", *v).serialize(s), - None => s.serialize_unit(), - } -} diff --git a/distant-ssh2/src/api.rs b/distant-ssh2/src/api.rs index fd8b975..2c596ce 100644 --- a/distant-ssh2/src/api.rs +++ b/distant-ssh2/src/api.rs @@ -655,8 +655,8 @@ impl DistantApi for SshDistantApi { .permissions .map(|x| !x.owner_write && !x.group_write && !x.other_write) .unwrap_or(true), - accessed: metadata.accessed.map(u128::from), - modified: metadata.modified.map(u128::from), + accessed: metadata.accessed, + modified: metadata.modified, created: None, unix: metadata.permissions.as_ref().map(|p| UnixMetadata { owner_read: p.owner_read, diff --git a/src/cli/commands/common/format.rs b/src/cli/commands/common/format.rs index c6d825d..b4b9ed8 100644 --- a/src/cli/commands/common/format.rs +++ b/src/cli/commands/common/format.rs @@ -154,21 +154,16 @@ fn format_shell(state: &mut FormatterState, data: protocol::Response) -> Output } protocol::Response::Changed(change) => Output::StdoutLine( format!( - "{}{}", + "{} {}", match change.kind { - ChangeKind::Create => "Following paths were created:\n", - ChangeKind::Delete => "Following paths were removed:\n", - x if x.is_access() => "Following paths were accessed:\n", - x if x.is_modify() => "Following paths were modified:\n", - x if x.is_rename() => "Following paths were renamed:\n", - _ => "Following paths were affected:\n", + ChangeKind::Create => "(Created)", + ChangeKind::Delete => "(Removed)", + x if x.is_access() => "(Accessed)", + x if x.is_modify() => "(Modified)", + x if x.is_rename() => "(Renamed)", + _ => "(Affected)", }, - change - .paths - .into_iter() - .map(|p| format!("* {}", p.to_string_lossy())) - .collect::>() - .join("\n") + change.path.to_string_lossy() ) .into_bytes(), ), diff --git a/tests/cli/api/watch.rs b/tests/cli/api/watch.rs index d3a02f6..4524831 100644 --- a/tests/cli/api/watch.rs +++ b/tests/cli/api/watch.rs @@ -63,8 +63,8 @@ async fn should_support_json_watching_single_file(mut api_process: CtxCommand