initial commit

pull/196/head
Chip Senkbeil 1 year ago
parent a36263e7e1
commit 01f03413ab
No known key found for this signature in database
GPG Key ID: 35EF1F8EC72A4131

@ -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() {

@ -5,7 +5,7 @@ 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::{
@ -337,26 +337,49 @@ 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),
},
};
for path in ev.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,
details: ChangeDetails {
attribute,
timestamp: details_timestamp,
extra: ev.info().map(ToString::to_string),
},
};
}
match registered_path.filter_and_send(change).await {
Ok(_) => (),
Err(x) => error!(

@ -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,31 @@ 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>,
/// 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 +54,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(),
});
@ -643,9 +643,10 @@ 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),
timestamp: Some(u64::MAX),
extra: Some(String::from("info")),
},
});
@ -659,7 +660,8 @@ mod tests {
"kind": "access",
"paths": ["path"],
"details": {
"attributes": ["permissions"],
"attribute": "permissions",
"ts": u64::MAX,
"extra": "info",
},
})
@ -672,7 +674,7 @@ mod tests {
"type": "changed",
"ts": u64::MAX,
"kind": "access",
"paths": ["path"],
"paths": "path",
});
let payload: Response = serde_json::from_value(value).unwrap();
@ -681,7 +683,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 +695,10 @@ mod tests {
"type": "changed",
"ts": u64::MAX,
"kind": "access",
"paths": ["path"],
"path": "path",
"details": {
"attributes": ["permissions"],
"attribute": "permissions",
"ts": u64::MAX,
"extra": "info",
},
});
@ -706,9 +709,10 @@ 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),
timestamp: Some(u64::MAX),
extra: Some(String::from("info")),
},
})
@ -720,7 +724,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 +740,10 @@ 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),
timestamp: Some(u64::MAX),
extra: Some(String::from("info")),
},
});
@ -759,7 +764,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 +775,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 +790,10 @@ 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),
timestamp: Some(u64::MAX),
extra: Some(String::from("info")),
},
}))
@ -799,9 +805,10 @@ 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),
timestamp: Some(u64::MAX),
extra: Some(String::from("info")),
},
})
@ -900,9 +907,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 +940,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 +949,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 +1012,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 +1059,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 +1122,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 +1209,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 +1251,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,

Loading…
Cancel
Save