Refactor Change to use single path & support renamed detail field (#196)

refactor/UseArrayForRequestResponse
Chip Senkbeil 12 months ago committed by GitHub
parent a36263e7e1
commit 791a41c29e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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] ## [0.20.0-alpha.8]
### Added ### Added

@ -265,13 +265,13 @@ mod tests {
protocol::Response::Changed(Change { protocol::Response::Changed(Change {
timestamp: 0, timestamp: 0,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
}), }),
protocol::Response::Changed(Change { protocol::Response::Changed(Change {
timestamp: 1, timestamp: 1,
kind: ChangeKind::Modify, kind: ChangeKind::Modify,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
}), }),
], ],
@ -286,7 +286,7 @@ mod tests {
Change { Change {
timestamp: 0, timestamp: 0,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
} }
); );
@ -297,7 +297,7 @@ mod tests {
Change { Change {
timestamp: 1, timestamp: 1,
kind: ChangeKind::Modify, kind: ChangeKind::Modify,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
} }
); );
@ -340,7 +340,7 @@ mod tests {
protocol::Response::Changed(Change { protocol::Response::Changed(Change {
timestamp: 0, timestamp: 0,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
}), }),
)) ))
@ -354,7 +354,7 @@ mod tests {
protocol::Response::Changed(Change { protocol::Response::Changed(Change {
timestamp: 1, timestamp: 1,
kind: ChangeKind::Modify, kind: ChangeKind::Modify,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
}), }),
)) ))
@ -368,7 +368,7 @@ mod tests {
protocol::Response::Changed(Change { protocol::Response::Changed(Change {
timestamp: 2, timestamp: 2,
kind: ChangeKind::Delete, kind: ChangeKind::Delete,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
}), }),
)) ))
@ -382,7 +382,7 @@ mod tests {
Change { Change {
timestamp: 0, timestamp: 0,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
} }
); );
@ -393,7 +393,7 @@ mod tests {
Change { Change {
timestamp: 2, timestamp: 2,
kind: ChangeKind::Delete, kind: ChangeKind::Delete,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
} }
); );
@ -434,19 +434,19 @@ mod tests {
protocol::Response::Changed(Change { protocol::Response::Changed(Change {
timestamp: 0, timestamp: 0,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
}), }),
protocol::Response::Changed(Change { protocol::Response::Changed(Change {
timestamp: 1, timestamp: 1,
kind: ChangeKind::Modify, kind: ChangeKind::Modify,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
}), }),
protocol::Response::Changed(Change { protocol::Response::Changed(Change {
timestamp: 2, timestamp: 2,
kind: ChangeKind::Delete, kind: ChangeKind::Delete,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
}), }),
], ],
@ -473,7 +473,7 @@ mod tests {
Change { Change {
timestamp: 0, timestamp: 0,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
} }
); );
@ -498,7 +498,7 @@ mod tests {
protocol::Response::Changed(Change { protocol::Response::Changed(Change {
timestamp: 3, timestamp: 3,
kind: ChangeKind::Unknown, kind: ChangeKind::Unknown,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
}), }),
)) ))
@ -512,7 +512,7 @@ mod tests {
Some(Change { Some(Change {
timestamp: 1, timestamp: 1,
kind: ChangeKind::Modify, kind: ChangeKind::Modify,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
}) })
); );
@ -521,7 +521,7 @@ mod tests {
Some(Change { Some(Change {
timestamp: 2, timestamp: 2,
kind: ChangeKind::Delete, kind: ChangeKind::Delete,
paths: vec![test_path.to_path_buf()], path: test_path.to_path_buf(),
details: Default::default(), details: Default::default(),
}) })
); );

@ -426,17 +426,17 @@ impl DistantApi for Api {
.accessed() .accessed()
.ok() .ok()
.and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()) .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
.map(|d| d.as_millis()), .map(|d| d.as_secs()),
created: metadata created: metadata
.created() .created()
.ok() .ok()
.and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()) .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
.map(|d| d.as_millis()), .map(|d| d.as_secs()),
modified: metadata modified: metadata
.modified() .modified()
.ok() .ok()
.and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()) .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
.map(|d| d.as_millis()), .map(|d| d.as_secs()),
len: metadata.len(), len: metadata.len(),
readonly: metadata.permissions().readonly(), readonly: metadata.permissions().readonly(),
file_type: if file_type.is_dir() { 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 /// Validates a response as being a series of changes that include the provided paths
fn validate_changed_paths( fn validate_changed_path(data: &Response, expected_path: &Path, should_panic: bool) -> bool {
data: &Response,
expected_paths: &[PathBuf],
should_panic: bool,
) -> bool {
match data { match data {
Response::Changed(change) if should_panic => { Response::Changed(change) if should_panic => {
let paths: Vec<PathBuf> = change let path = change.path.canonicalize().unwrap();
.paths assert_eq!(path, expected_path, "Wrong path reported: {:?}", change);
.iter()
.map(|x| x.canonicalize().unwrap())
.collect();
assert_eq!(paths, expected_paths, "Wrong paths reported: {:?}", change);
true true
} }
Response::Changed(change) => { Response::Changed(change) => {
let paths: Vec<PathBuf> = change let path = change.path.canonicalize().unwrap();
.paths path == expected_path
.iter()
.map(|x| x.canonicalize().unwrap())
.collect();
paths == expected_paths
} }
x if should_panic => panic!("Unexpected response: {:?}", x), x if should_panic => panic!("Unexpected response: {:?}", x),
_ => false, _ => false,
@ -1602,9 +1590,9 @@ mod tests {
.recv() .recv()
.await .await
.expect("Channel closed before we got change"); .expect("Channel closed before we got change");
validate_changed_paths( validate_changed_path(
&data, &data,
&[file.path().to_path_buf().canonicalize().unwrap()], &file.path().to_path_buf().canonicalize().unwrap(),
/* should_panic */ true, /* should_panic */ true,
); );
} }
@ -1657,9 +1645,9 @@ mod tests {
let path = file.path().to_path_buf(); let path = file.path().to_path_buf();
assert!( assert!(
responses.iter().any(|res| validate_changed_paths( responses.iter().any(|res| validate_changed_path(
res, res,
&[file.path().to_path_buf().canonicalize().unwrap()], &file.path().to_path_buf().canonicalize().unwrap(),
/* should_panic */ false, /* should_panic */ false,
)), )),
"Missing {:?} in {:?}", "Missing {:?} in {:?}",
@ -1672,9 +1660,9 @@ mod tests {
let path = nested_file.path().to_path_buf(); let path = nested_file.path().to_path_buf();
assert!( assert!(
responses.iter().any(|res| validate_changed_paths( responses.iter().any(|res| validate_changed_path(
res, res,
&[file.path().to_path_buf().canonicalize().unwrap()], &file.path().to_path_buf().canonicalize().unwrap(),
/* should_panic */ false, /* should_panic */ false,
)), )),
"Missing {:?} in {:?}", "Missing {:?} in {:?}",
@ -1740,9 +1728,9 @@ mod tests {
.recv() .recv()
.await .await
.expect("Channel closed before we got change"); .expect("Channel closed before we got change");
validate_changed_paths( validate_changed_path(
&data, &data,
&[file_1.path().to_path_buf().canonicalize().unwrap()], &file_1.path().to_path_buf().canonicalize().unwrap(),
/* should_panic */ true, /* should_panic */ true,
); );
@ -1752,9 +1740,9 @@ mod tests {
.recv() .recv()
.await .await
.expect("Channel closed before we got change"); .expect("Channel closed before we got change");
validate_changed_paths( validate_changed_path(
&data, &data,
&[file_2.path().to_path_buf().canonicalize().unwrap()], &file_2.path().to_path_buf().canonicalize().unwrap(),
/* should_panic */ true, /* should_panic */ true,
); );
} }

@ -5,9 +5,9 @@ use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use distant_core::net::common::ConnectionId; 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 log::*;
use notify::event::{AccessKind, AccessMode, MetadataKind, ModifyKind}; use notify::event::{AccessKind, AccessMode, MetadataKind, ModifyKind, RenameMode};
use notify::{ use notify::{
Config as WatcherConfig, Error as WatcherError, ErrorKind as WatcherErrorKind, Config as WatcherConfig, Error as WatcherError, ErrorKind as WatcherErrorKind,
Event as WatcherEvent, EventKind, PollWatcher, RecommendedWatcher, RecursiveMode, Watcher, Event as WatcherEvent, EventKind, PollWatcher, RecommendedWatcher, RecursiveMode, Watcher,
@ -337,33 +337,71 @@ async fn watcher_task<W>(
_ => ChangeKind::Unknown, _ => 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() { for registered_path in registered_paths.iter() {
let change = Change { // For rename both, we assume the paths is a pair that represents before and
timestamp, // after, so we want to grab the before and use it!
kind, let (paths, renamed): (&[PathBuf], Option<PathBuf>) = match ev.kind {
paths: ev.paths.clone(), EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => (
details: ChangeDetails { &ev.paths[0..1],
attributes: attributes.clone(), if ev.paths.len() > 1 {
extra: ev.info().map(ToString::to_string), ev.paths.last().cloned()
}, } else {
}; None
match registered_path.filter_and_send(change).await { },
Ok(_) => (),
Err(x) => error!(
"[Conn {}] Failed to forward changes to paths: {}",
registered_path.id(),
x
), ),
_ => (&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
),
}
} }
} }
} }

@ -119,18 +119,16 @@ impl RegisteredPath {
} }
/// Sends a reply for a change tied to this registered path, filtering /// 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 /// Returns true if message was sent, and false if not.
pub async fn filter_and_send(&self, mut change: Change) -> io::Result<bool> { pub async fn filter_and_send(&self, change: Change) -> io::Result<bool> {
if !self.allowed().contains(&change.kind) { if !self.allowed().contains(&change.kind) {
return Ok(false); return Ok(false);
} }
// filter the paths that are not applicable // Only send if this registered path applies to the changed path
change.paths.retain(|p| self.applies_to_path(p.as_path())); if self.applies_to_path(&change.path) {
if !change.paths.is_empty() {
self.reply self.reply
.send(Response::Changed(change)) .send(Response::Changed(change))
.await .await
@ -141,9 +139,9 @@ impl RegisteredPath {
} }
/// Sends an error message and includes paths if provided, skipping sending the message if /// 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<T>( pub async fn filter_and_send_error<T>(
&self, &self,
msg: &str, msg: &str,

@ -10,7 +10,7 @@ use derive_more::{Deref, DerefMut, IntoIterator};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::{EnumString, EnumVariantNames, VariantNames}; 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)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)] #[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct Change { pub struct Change {
@ -22,23 +22,36 @@ pub struct Change {
/// Label describing the kind of change /// Label describing the kind of change
pub kind: ChangeKind, pub kind: ChangeKind,
/// Paths that were changed /// Path that was changed
pub paths: Vec<PathBuf>, pub path: PathBuf,
/// Additional details associated with the change /// Additional details associated with the change
#[serde(default, skip_serializing_if = "ChangeDetails::is_empty")] #[serde(default, skip_serializing_if = "ChangeDetails::is_empty")]
pub details: ChangeDetails, pub details: ChangeDetails,
} }
/// Details about a change /// Optional details about a change.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "snake_case", deny_unknown_fields)] #[serde(default, rename_all = "snake_case", deny_unknown_fields)]
pub struct ChangeDetails { pub struct ChangeDetails {
/// Clarity on type of attribute changes that have occurred (for kind == attribute) /// Clarity on type of attribute change that occurred (for kind == attribute).
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Vec<ChangeDetailsAttributes>, pub attribute: Option<ChangeDetailsAttribute>,
/// 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<PathBuf>,
/// 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<u64>,
/// 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")] #[serde(skip_serializing_if = "Option::is_none")]
pub extra: Option<String>, pub extra: Option<String>,
} }
@ -46,14 +59,15 @@ pub struct ChangeDetails {
impl ChangeDetails { impl ChangeDetails {
/// Returns true if no details are contained within. /// Returns true if no details are contained within.
pub fn is_empty(&self) -> bool { 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 /// Specific details about modification
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)] #[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum ChangeDetailsAttributes { pub enum ChangeDetailsAttribute {
Ownership,
Permissions, Permissions,
Timestamp, Timestamp,
} }

@ -4,7 +4,6 @@ use bitflags::bitflags;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::common::FileType; use crate::common::FileType;
use crate::utils::{deserialize_u128_option, serialize_u128_option};
/// Represents metadata about some path on a remote machine. /// Represents metadata about some path on a remote machine.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[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. /// Whether or not the file/directory/symlink is marked as unwriteable.
pub readonly: bool, 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. /// can be optional as certain systems don't support this.
/// #[serde(default, skip_serializing_if = "Option::is_none")]
/// Note that this is represented as a string and not a number when serialized! pub accessed: Option<u64>,
#[serde(
default, /// Represents when (in seconds) the file/directory/symlink was created;
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_u128_option",
deserialize_with = "deserialize_u128_option"
)]
pub accessed: Option<u128>,
/// Represents when (in milliseconds) the file/directory/symlink was created;
/// can be optional as certain systems don't support this. /// can be optional as certain systems don't support this.
/// #[serde(default, skip_serializing_if = "Option::is_none")]
/// Note that this is represented as a string and not a number when serialized! pub created: Option<u64>,
#[serde(
default, /// Represents the last time (in seconds) when the file/directory/symlink was modified;
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_u128_option",
deserialize_with = "deserialize_u128_option"
)]
pub created: Option<u128>,
/// Represents the last time (in milliseconds) when the file/directory/symlink was modified;
/// can be optional as certain systems don't support this. /// can be optional as certain systems don't support this.
/// #[serde(default, skip_serializing_if = "Option::is_none")]
/// Note that this is represented as a string and not a number when serialized! pub modified: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_u128_option",
deserialize_with = "deserialize_u128_option"
)]
pub modified: Option<u128>,
/// Represents metadata that is specific to a unix remote machine. /// Represents metadata that is specific to a unix remote machine.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@ -369,9 +347,9 @@ mod tests {
file_type: FileType::Dir, file_type: FileType::Dir,
len: 999, len: 999,
readonly: true, readonly: true,
accessed: Some(u128::MAX), accessed: Some(u64::MAX),
created: Some(u128::MAX), created: Some(u64::MAX),
modified: Some(u128::MAX), modified: Some(u64::MAX),
unix: Some(UnixMetadata { unix: Some(UnixMetadata {
owner_read: true, owner_read: true,
owner_write: false, 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(); let value = serde_json::to_value(metadata).unwrap();
assert_eq!( assert_eq!(
value, value,
@ -414,9 +388,9 @@ mod tests {
"file_type": "dir", "file_type": "dir",
"len": 999, "len": 999,
"readonly": true, "readonly": true,
"accessed": max_u128_str, "accessed": u64::MAX,
"created": max_u128_str, "created": u64::MAX,
"modified": max_u128_str, "modified": u64::MAX,
"unix": { "unix": {
"owner_read": true, "owner_read": true,
"owner_write": false, "owner_write": false,
@ -476,18 +450,14 @@ mod tests {
#[test] #[test]
fn should_be_able_to_deserialize_full_metadata_from_json() { 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!({ let value = serde_json::json!({
"canonicalized_path": "test-dir", "canonicalized_path": "test-dir",
"file_type": "dir", "file_type": "dir",
"len": 999, "len": 999,
"readonly": true, "readonly": true,
"accessed": max_u128_str, "accessed": u64::MAX,
"created": max_u128_str, "created": u64::MAX,
"modified": max_u128_str, "modified": u64::MAX,
"unix": { "unix": {
"owner_read": true, "owner_read": true,
"owner_write": false, "owner_write": false,
@ -526,9 +496,9 @@ mod tests {
file_type: FileType::Dir, file_type: FileType::Dir,
len: 999, len: 999,
readonly: true, readonly: true,
accessed: Some(u128::MAX), accessed: Some(u64::MAX),
created: Some(u128::MAX), created: Some(u64::MAX),
modified: Some(u128::MAX), modified: Some(u64::MAX),
unix: Some(UnixMetadata { unix: Some(UnixMetadata {
owner_read: true, owner_read: true,
owner_write: false, owner_write: false,
@ -589,9 +559,9 @@ mod tests {
file_type: FileType::Dir, file_type: FileType::Dir,
len: 999, len: 999,
readonly: true, readonly: true,
accessed: Some(u128::MAX), accessed: Some(u64::MAX),
created: Some(u128::MAX), created: Some(u64::MAX),
modified: Some(u128::MAX), modified: Some(u64::MAX),
unix: Some(UnixMetadata { unix: Some(UnixMetadata {
owner_read: true, owner_read: true,
owner_write: false, owner_write: false,
@ -676,9 +646,9 @@ mod tests {
file_type: FileType::Dir, file_type: FileType::Dir,
len: 999, len: 999,
readonly: true, readonly: true,
accessed: Some(u128::MAX), accessed: Some(u64::MAX),
created: Some(u128::MAX), created: Some(u64::MAX),
modified: Some(u128::MAX), modified: Some(u64::MAX),
unix: Some(UnixMetadata { unix: Some(UnixMetadata {
owner_read: true, owner_read: true,
owner_write: false, owner_write: false,
@ -718,9 +688,9 @@ mod tests {
file_type: FileType::Dir, file_type: FileType::Dir,
len: 999, len: 999,
readonly: true, readonly: true,
accessed: Some(u128::MAX), accessed: Some(u64::MAX),
created: Some(u128::MAX), created: Some(u64::MAX),
modified: Some(u128::MAX), modified: Some(u64::MAX),
unix: Some(UnixMetadata { unix: Some(UnixMetadata {
owner_read: true, owner_read: true,
owner_write: false, owner_write: false,

@ -615,14 +615,14 @@ mod tests {
use std::path::PathBuf; use std::path::PathBuf;
use super::*; use super::*;
use crate::common::{ChangeDetails, ChangeDetailsAttributes, ChangeKind}; use crate::common::{ChangeDetails, ChangeDetailsAttribute, ChangeKind};
#[test] #[test]
fn should_be_able_to_serialize_minimal_payload_to_json() { fn should_be_able_to_serialize_minimal_payload_to_json() {
let payload = Response::Changed(Change { let payload = Response::Changed(Change {
timestamp: u64::MAX, timestamp: u64::MAX,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![PathBuf::from("path")], path: PathBuf::from("path"),
details: ChangeDetails::default(), details: ChangeDetails::default(),
}); });
@ -633,7 +633,7 @@ mod tests {
"type": "changed", "type": "changed",
"ts": u64::MAX, "ts": u64::MAX,
"kind": "access", "kind": "access",
"paths": ["path"], "path": "path",
}) })
); );
} }
@ -643,9 +643,11 @@ mod tests {
let payload = Response::Changed(Change { let payload = Response::Changed(Change {
timestamp: u64::MAX, timestamp: u64::MAX,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![PathBuf::from("path")], path: PathBuf::from("path"),
details: ChangeDetails { details: ChangeDetails {
attributes: vec![ChangeDetailsAttributes::Permissions], attribute: Some(ChangeDetailsAttribute::Permissions),
renamed: Some(PathBuf::from("renamed")),
timestamp: Some(u64::MAX),
extra: Some(String::from("info")), extra: Some(String::from("info")),
}, },
}); });
@ -657,9 +659,11 @@ mod tests {
"type": "changed", "type": "changed",
"ts": u64::MAX, "ts": u64::MAX,
"kind": "access", "kind": "access",
"paths": ["path"], "path": "path",
"details": { "details": {
"attributes": ["permissions"], "attribute": "permissions",
"renamed": "renamed",
"ts": u64::MAX,
"extra": "info", "extra": "info",
}, },
}) })
@ -672,7 +676,7 @@ mod tests {
"type": "changed", "type": "changed",
"ts": u64::MAX, "ts": u64::MAX,
"kind": "access", "kind": "access",
"paths": ["path"], "path": "path",
}); });
let payload: Response = serde_json::from_value(value).unwrap(); let payload: Response = serde_json::from_value(value).unwrap();
@ -681,7 +685,7 @@ mod tests {
Response::Changed(Change { Response::Changed(Change {
timestamp: u64::MAX, timestamp: u64::MAX,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![PathBuf::from("path")], path: PathBuf::from("path"),
details: ChangeDetails::default(), details: ChangeDetails::default(),
}) })
); );
@ -693,9 +697,11 @@ mod tests {
"type": "changed", "type": "changed",
"ts": u64::MAX, "ts": u64::MAX,
"kind": "access", "kind": "access",
"paths": ["path"], "path": "path",
"details": { "details": {
"attributes": ["permissions"], "attribute": "permissions",
"renamed": "renamed",
"ts": u64::MAX,
"extra": "info", "extra": "info",
}, },
}); });
@ -706,9 +712,11 @@ mod tests {
Response::Changed(Change { Response::Changed(Change {
timestamp: u64::MAX, timestamp: u64::MAX,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![PathBuf::from("path")], path: PathBuf::from("path"),
details: ChangeDetails { details: ChangeDetails {
attributes: vec![ChangeDetailsAttributes::Permissions], attribute: Some(ChangeDetailsAttribute::Permissions),
renamed: Some(PathBuf::from("renamed")),
timestamp: Some(u64::MAX),
extra: Some(String::from("info")), extra: Some(String::from("info")),
}, },
}) })
@ -720,7 +728,7 @@ mod tests {
let payload = Response::Changed(Change { let payload = Response::Changed(Change {
timestamp: u64::MAX, timestamp: u64::MAX,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![PathBuf::from("path")], path: PathBuf::from("path"),
details: ChangeDetails::default(), details: ChangeDetails::default(),
}); });
@ -736,9 +744,11 @@ mod tests {
let payload = Response::Changed(Change { let payload = Response::Changed(Change {
timestamp: u64::MAX, timestamp: u64::MAX,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![PathBuf::from("path")], path: PathBuf::from("path"),
details: ChangeDetails { details: ChangeDetails {
attributes: vec![ChangeDetailsAttributes::Permissions], attribute: Some(ChangeDetailsAttribute::Permissions),
renamed: Some(PathBuf::from("renamed")),
timestamp: Some(u64::MAX),
extra: Some(String::from("info")), extra: Some(String::from("info")),
}, },
}); });
@ -759,7 +769,7 @@ mod tests {
let buf = rmp_serde::encode::to_vec_named(&Response::Changed(Change { let buf = rmp_serde::encode::to_vec_named(&Response::Changed(Change {
timestamp: u64::MAX, timestamp: u64::MAX,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![PathBuf::from("path")], path: PathBuf::from("path"),
details: ChangeDetails::default(), details: ChangeDetails::default(),
})) }))
.unwrap(); .unwrap();
@ -770,7 +780,7 @@ mod tests {
Response::Changed(Change { Response::Changed(Change {
timestamp: u64::MAX, timestamp: u64::MAX,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![PathBuf::from("path")], path: PathBuf::from("path"),
details: ChangeDetails::default(), details: ChangeDetails::default(),
}) })
); );
@ -785,9 +795,11 @@ mod tests {
let buf = rmp_serde::encode::to_vec_named(&Response::Changed(Change { let buf = rmp_serde::encode::to_vec_named(&Response::Changed(Change {
timestamp: u64::MAX, timestamp: u64::MAX,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![PathBuf::from("path")], path: PathBuf::from("path"),
details: ChangeDetails { details: ChangeDetails {
attributes: vec![ChangeDetailsAttributes::Permissions], attribute: Some(ChangeDetailsAttribute::Permissions),
renamed: Some(PathBuf::from("renamed")),
timestamp: Some(u64::MAX),
extra: Some(String::from("info")), extra: Some(String::from("info")),
}, },
})) }))
@ -799,9 +811,11 @@ mod tests {
Response::Changed(Change { Response::Changed(Change {
timestamp: u64::MAX, timestamp: u64::MAX,
kind: ChangeKind::Access, kind: ChangeKind::Access,
paths: vec![PathBuf::from("path")], path: PathBuf::from("path"),
details: ChangeDetails { details: ChangeDetails {
attributes: vec![ChangeDetailsAttributes::Permissions], attribute: Some(ChangeDetailsAttribute::Permissions),
renamed: Some(PathBuf::from("renamed")),
timestamp: Some(u64::MAX),
extra: Some(String::from("info")), extra: Some(String::from("info")),
}, },
}) })
@ -900,9 +914,9 @@ mod tests {
file_type: FileType::File, file_type: FileType::File,
len: u64::MAX, len: u64::MAX,
readonly: true, readonly: true,
accessed: Some(u128::MAX), accessed: Some(u64::MAX),
created: Some(u128::MAX), created: Some(u64::MAX),
modified: Some(u128::MAX), modified: Some(u64::MAX),
unix: Some(UnixMetadata { unix: Some(UnixMetadata {
owner_read: true, owner_read: true,
owner_write: false, 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(); let value = serde_json::to_value(payload).unwrap();
assert_eq!( assert_eq!(
value, value,
@ -946,9 +956,9 @@ mod tests {
"file_type": "file", "file_type": "file",
"len": u64::MAX, "len": u64::MAX,
"readonly": true, "readonly": true,
"accessed": u128_max_str, "accessed": u64::MAX,
"created": u128_max_str, "created": u64::MAX,
"modified": u128_max_str, "modified": u64::MAX,
"unix": { "unix": {
"owner_read": true, "owner_read": true,
"owner_write": false, "owner_write": false,
@ -1009,16 +1019,15 @@ mod tests {
#[test] #[test]
fn should_be_able_to_deserialize_full_payload_from_json() { fn should_be_able_to_deserialize_full_payload_from_json() {
let u128_max_str = u128::MAX.to_string();
let value = serde_json::json!({ let value = serde_json::json!({
"type": "metadata", "type": "metadata",
"canonicalized_path": "path", "canonicalized_path": "path",
"file_type": "file", "file_type": "file",
"len": u64::MAX, "len": u64::MAX,
"readonly": true, "readonly": true,
"accessed": u128_max_str, "accessed": u64::MAX,
"created": u128_max_str, "created": u64::MAX,
"modified": u128_max_str, "modified": u64::MAX,
"unix": { "unix": {
"owner_read": true, "owner_read": true,
"owner_write": false, "owner_write": false,
@ -1057,9 +1066,9 @@ mod tests {
file_type: FileType::File, file_type: FileType::File,
len: u64::MAX, len: u64::MAX,
readonly: true, readonly: true,
accessed: Some(u128::MAX), accessed: Some(u64::MAX),
created: Some(u128::MAX), created: Some(u64::MAX),
modified: Some(u128::MAX), modified: Some(u64::MAX),
unix: Some(UnixMetadata { unix: Some(UnixMetadata {
owner_read: true, owner_read: true,
owner_write: false, owner_write: false,
@ -1120,9 +1129,9 @@ mod tests {
file_type: FileType::File, file_type: FileType::File,
len: u64::MAX, len: u64::MAX,
readonly: true, readonly: true,
accessed: Some(u128::MAX), accessed: Some(u64::MAX),
created: Some(u128::MAX), created: Some(u64::MAX),
modified: Some(u128::MAX), modified: Some(u64::MAX),
unix: Some(UnixMetadata { unix: Some(UnixMetadata {
owner_read: true, owner_read: true,
owner_write: false, owner_write: false,
@ -1207,9 +1216,9 @@ mod tests {
file_type: FileType::File, file_type: FileType::File,
len: u64::MAX, len: u64::MAX,
readonly: true, readonly: true,
accessed: Some(u128::MAX), accessed: Some(u64::MAX),
created: Some(u128::MAX), created: Some(u64::MAX),
modified: Some(u128::MAX), modified: Some(u64::MAX),
unix: Some(UnixMetadata { unix: Some(UnixMetadata {
owner_read: true, owner_read: true,
owner_write: false, owner_write: false,
@ -1249,9 +1258,9 @@ mod tests {
file_type: FileType::File, file_type: FileType::File,
len: u64::MAX, len: u64::MAX,
readonly: true, readonly: true,
accessed: Some(u128::MAX), accessed: Some(u64::MAX),
created: Some(u128::MAX), created: Some(u64::MAX),
modified: Some(u128::MAX), modified: Some(u64::MAX),
unix: Some(UnixMetadata { unix: Some(UnixMetadata {
owner_read: true, owner_read: true,
owner_write: false, owner_write: false,

@ -1,5 +1,3 @@
use serde::{Deserialize, Serialize};
/// Used purely for skipping serialization of values that are false by default. /// Used purely for skipping serialization of values that are false by default.
#[inline] #[inline]
pub const fn is_false(value: &bool) -> bool { 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 { pub const fn one() -> usize {
1 1
} }
pub fn deserialize_u128_option<'de, D>(deserializer: D) -> Result<Option<u128>, D::Error>
where
D: serde::Deserializer<'de>,
{
match Option::<String>::deserialize(deserializer)? {
Some(s) => match s.parse::<u128>() {
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<S: serde::Serializer>(
val: &Option<u128>,
s: S,
) -> Result<S::Ok, S::Error> {
match val {
Some(v) => format!("{}", *v).serialize(s),
None => s.serialize_unit(),
}
}

@ -655,8 +655,8 @@ impl DistantApi for SshDistantApi {
.permissions .permissions
.map(|x| !x.owner_write && !x.group_write && !x.other_write) .map(|x| !x.owner_write && !x.group_write && !x.other_write)
.unwrap_or(true), .unwrap_or(true),
accessed: metadata.accessed.map(u128::from), accessed: metadata.accessed,
modified: metadata.modified.map(u128::from), modified: metadata.modified,
created: None, created: None,
unix: metadata.permissions.as_ref().map(|p| UnixMetadata { unix: metadata.permissions.as_ref().map(|p| UnixMetadata {
owner_read: p.owner_read, owner_read: p.owner_read,

@ -154,21 +154,16 @@ fn format_shell(state: &mut FormatterState, data: protocol::Response) -> Output
} }
protocol::Response::Changed(change) => Output::StdoutLine( protocol::Response::Changed(change) => Output::StdoutLine(
format!( format!(
"{}{}", "{} {}",
match change.kind { match change.kind {
ChangeKind::Create => "Following paths were created:\n", ChangeKind::Create => "(Created)",
ChangeKind::Delete => "Following paths were removed:\n", ChangeKind::Delete => "(Removed)",
x if x.is_access() => "Following paths were accessed:\n", x if x.is_access() => "(Accessed)",
x if x.is_modify() => "Following paths were modified:\n", x if x.is_modify() => "(Modified)",
x if x.is_rename() => "Following paths were renamed:\n", x if x.is_rename() => "(Renamed)",
_ => "Following paths were affected:\n", _ => "(Affected)",
}, },
change change.path.to_string_lossy()
.paths
.into_iter()
.map(|p| format!("* {}", p.to_string_lossy()))
.collect::<Vec<String>>()
.join("\n")
) )
.into_bytes(), .into_bytes(),
), ),

@ -63,8 +63,8 @@ async fn should_support_json_watching_single_file(mut api_process: CtxCommand<Ap
assert_eq!(res["origin_id"], id, "JSON: {res}"); assert_eq!(res["origin_id"], id, "JSON: {res}");
assert_eq!(res["payload"]["type"], "changed", "JSON: {res}"); assert_eq!(res["payload"]["type"], "changed", "JSON: {res}");
assert_eq!( assert_eq!(
res["payload"]["paths"], res["payload"]["path"],
json!([file.to_path_buf().canonicalize().unwrap()]), json!(file.to_path_buf().canonicalize().unwrap()),
"JSON: {res}" "JSON: {res}"
); );
} }
@ -121,8 +121,8 @@ async fn should_support_json_watching_directory_recursively(
assert_eq!(res["origin_id"], id, "JSON: {res}"); assert_eq!(res["origin_id"], id, "JSON: {res}");
assert_eq!(res["payload"]["type"], "changed", "JSON: {res}"); assert_eq!(res["payload"]["type"], "changed", "JSON: {res}");
assert_eq!( assert_eq!(
res["payload"]["paths"], res["payload"]["path"],
json!([dir.to_path_buf().canonicalize().unwrap()]), json!(dir.to_path_buf().canonicalize().unwrap()),
"JSON: {res}" "JSON: {res}"
); );
} }
@ -137,8 +137,8 @@ async fn should_support_json_watching_directory_recursively(
assert_eq!(res["origin_id"], id, "JSON: {res}"); assert_eq!(res["origin_id"], id, "JSON: {res}");
assert_eq!(res["payload"]["type"], "changed", "JSON: {res}"); assert_eq!(res["payload"]["type"], "changed", "JSON: {res}");
assert_eq!( assert_eq!(
res["payload"]["paths"], res["payload"]["path"],
json!([file.to_path_buf().canonicalize().unwrap()]), json!(file.to_path_buf().canonicalize().unwrap()),
"JSON: {res}" "JSON: {res}"
); );
} }
@ -213,8 +213,8 @@ async fn should_support_json_reporting_changes_using_correct_request_id(
assert_eq!(res["origin_id"], id_1, "JSON: {res}"); assert_eq!(res["origin_id"], id_1, "JSON: {res}");
assert_eq!(res["payload"]["type"], "changed", "JSON: {res}"); assert_eq!(res["payload"]["type"], "changed", "JSON: {res}");
assert_eq!( assert_eq!(
res["payload"]["paths"], res["payload"]["path"],
json!([file1.to_path_buf().canonicalize().unwrap()]), json!(file1.to_path_buf().canonicalize().unwrap()),
"JSON: {res}" "JSON: {res}"
); );
@ -245,8 +245,8 @@ async fn should_support_json_reporting_changes_using_correct_request_id(
assert_eq!(res["origin_id"], id_2, "JSON: {res}"); assert_eq!(res["origin_id"], id_2, "JSON: {res}");
assert_eq!(res["payload"]["type"], "changed", "JSON: {res}"); assert_eq!(res["payload"]["type"], "changed", "JSON: {res}");
assert_eq!( assert_eq!(
res["payload"]["paths"], res["payload"]["path"],
json!([file2.to_path_buf().canonicalize().unwrap()]), json!(file2.to_path_buf().canonicalize().unwrap()),
"JSON: {res}" "JSON: {res}"
); );
} }

Loading…
Cancel
Save