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

refactor/UseArrayForRequestResponse
Chip Senkbeil 11 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]
### 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

@ -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(),
})
);

@ -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<PathBuf> = 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<PathBuf> = 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,
);
}

@ -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<W>(
_ => 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<PathBuf>) = 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
),
}
}
}
}

@ -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<bool> {
/// Returns true if message was sent, and false if not.
pub async fn filter_and_send(&self, change: Change) -> io::Result<bool> {
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<T>(
&self,
msg: &str,

@ -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<PathBuf>,
/// 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<ChangeDetailsAttributes>,
/// Clarity on type of attribute change that occurred (for kind == attribute).
#[serde(skip_serializing_if = "Option::is_none")]
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")]
pub extra: Option<String>,
}
@ -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,
}

@ -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<u128>,
/// Represents when (in milliseconds) the file/directory/symlink was created;
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accessed: Option<u64>,
/// 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<u128>,
/// Represents the last time (in milliseconds) when the file/directory/symlink was modified;
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created: Option<u64>,
/// 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<u128>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub modified: Option<u64>,
/// 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,

@ -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,

@ -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<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
.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,

@ -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::<Vec<String>>()
.join("\n")
change.path.to_string_lossy()
)
.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["payload"]["type"], "changed", "JSON: {res}");
assert_eq!(
res["payload"]["paths"],
json!([file.to_path_buf().canonicalize().unwrap()]),
res["payload"]["path"],
json!(file.to_path_buf().canonicalize().unwrap()),
"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["payload"]["type"], "changed", "JSON: {res}");
assert_eq!(
res["payload"]["paths"],
json!([dir.to_path_buf().canonicalize().unwrap()]),
res["payload"]["path"],
json!(dir.to_path_buf().canonicalize().unwrap()),
"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["payload"]["type"], "changed", "JSON: {res}");
assert_eq!(
res["payload"]["paths"],
json!([file.to_path_buf().canonicalize().unwrap()]),
res["payload"]["path"],
json!(file.to_path_buf().canonicalize().unwrap()),
"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["payload"]["type"], "changed", "JSON: {res}");
assert_eq!(
res["payload"]["paths"],
json!([file1.to_path_buf().canonicalize().unwrap()]),
res["payload"]["path"],
json!(file1.to_path_buf().canonicalize().unwrap()),
"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["payload"]["type"], "changed", "JSON: {res}");
assert_eq!(
res["payload"]["paths"],
json!([file2.to_path_buf().canonicalize().unwrap()]),
res["payload"]["path"],
json!(file2.to_path_buf().canonicalize().unwrap()),
"JSON: {res}"
);
}

Loading…
Cancel
Save