use crate::api::generated::machine::{
CopyRequest as ProtoCopyRequest, DiskUsageInfo as ProtoDiskUsageInfo,
DiskUsageRequest as ProtoDiskUsageRequest, FileInfo as ProtoFileInfo,
ListRequest as ProtoListRequest, ReadRequest as ProtoReadRequest,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FileType {
#[default]
Regular,
Directory,
Symlink,
}
impl From<FileType> for i32 {
fn from(ft: FileType) -> Self {
match ft {
FileType::Regular => 0,
FileType::Directory => 1,
FileType::Symlink => 2,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ListRequest {
pub root: String,
pub recurse: bool,
pub recursion_depth: i32,
pub types: Vec<FileType>,
pub report_xattrs: bool,
}
impl ListRequest {
#[must_use]
pub fn new(root: impl Into<String>) -> Self {
Self {
root: root.into(),
..Default::default()
}
}
#[must_use]
pub fn builder(root: impl Into<String>) -> ListRequestBuilder {
ListRequestBuilder::new(root)
}
}
impl From<ListRequest> for ProtoListRequest {
fn from(req: ListRequest) -> Self {
Self {
root: req.root,
recurse: req.recurse,
recursion_depth: req.recursion_depth,
types: req.types.into_iter().map(i32::from).collect(),
report_xattrs: req.report_xattrs,
}
}
}
#[derive(Debug, Clone)]
pub struct ListRequestBuilder {
root: String,
recurse: bool,
recursion_depth: i32,
types: Vec<FileType>,
report_xattrs: bool,
}
impl ListRequestBuilder {
#[must_use]
pub fn new(root: impl Into<String>) -> Self {
Self {
root: root.into(),
recurse: false,
recursion_depth: 0,
types: Vec::new(),
report_xattrs: false,
}
}
#[must_use]
pub fn recurse(mut self, recurse: bool) -> Self {
self.recurse = recurse;
self
}
#[must_use]
pub fn recursion_depth(mut self, depth: i32) -> Self {
self.recursion_depth = depth;
self
}
#[must_use]
pub fn types(mut self, types: Vec<FileType>) -> Self {
self.types = types;
self
}
#[must_use]
pub fn report_xattrs(mut self, report: bool) -> Self {
self.report_xattrs = report;
self
}
#[must_use]
pub fn build(self) -> ListRequest {
ListRequest {
root: self.root,
recurse: self.recurse,
recursion_depth: self.recursion_depth,
types: self.types,
report_xattrs: self.report_xattrs,
}
}
}
#[derive(Debug, Clone)]
pub struct FileInfo {
pub node: Option<String>,
pub name: String,
pub size: i64,
pub mode: u32,
pub modified: i64,
pub is_dir: bool,
pub error: Option<String>,
pub link: Option<String>,
pub relative_name: String,
pub uid: u32,
pub gid: u32,
}
impl From<ProtoFileInfo> for FileInfo {
fn from(proto: ProtoFileInfo) -> Self {
Self {
node: proto.metadata.map(|m| m.hostname),
name: proto.name,
size: proto.size,
mode: proto.mode,
modified: proto.modified,
is_dir: proto.is_dir,
error: if proto.error.is_empty() {
None
} else {
Some(proto.error)
},
link: if proto.link.is_empty() {
None
} else {
Some(proto.link)
},
relative_name: proto.relative_name,
uid: proto.uid,
gid: proto.gid,
}
}
}
impl FileInfo {
#[must_use]
pub fn has_error(&self) -> bool {
self.error.is_some()
}
#[must_use]
pub fn is_file(&self) -> bool {
!self.is_dir && self.link.is_none()
}
#[must_use]
pub fn is_symlink(&self) -> bool {
self.link.is_some()
}
}
#[derive(Debug, Clone, Default)]
pub struct ListResponse {
pub entries: Vec<FileInfo>,
}
impl ListResponse {
#[must_use]
pub fn new(entries: Vec<FileInfo>) -> Self {
Self { entries }
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn directories(&self) -> Vec<&FileInfo> {
self.entries.iter().filter(|e| e.is_dir).collect()
}
#[must_use]
pub fn files(&self) -> Vec<&FileInfo> {
self.entries.iter().filter(|e| e.is_file()).collect()
}
}
#[derive(Debug, Clone)]
pub struct ReadRequest {
pub path: String,
}
impl ReadRequest {
#[must_use]
pub fn new(path: impl Into<String>) -> Self {
Self { path: path.into() }
}
}
impl From<ReadRequest> for ProtoReadRequest {
fn from(req: ReadRequest) -> Self {
Self { path: req.path }
}
}
#[derive(Debug, Clone, Default)]
pub struct ReadResponse {
pub data: Vec<u8>,
pub node: Option<String>,
}
impl ReadResponse {
#[must_use]
pub fn new(data: Vec<u8>, node: Option<String>) -> Self {
Self { data, node }
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
std::str::from_utf8(&self.data).ok()
}
#[must_use]
pub fn as_string_lossy(&self) -> String {
String::from_utf8_lossy(&self.data).into_owned()
}
#[must_use]
pub fn len(&self) -> usize {
self.data.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct CopyRequest {
pub root_path: String,
}
impl CopyRequest {
#[must_use]
pub fn new(root_path: impl Into<String>) -> Self {
Self {
root_path: root_path.into(),
}
}
}
impl From<CopyRequest> for ProtoCopyRequest {
fn from(req: CopyRequest) -> Self {
Self {
root_path: req.root_path,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CopyResponse {
pub data: Vec<u8>,
pub node: Option<String>,
}
impl CopyResponse {
#[must_use]
pub fn new(data: Vec<u8>, node: Option<String>) -> Self {
Self { data, node }
}
#[must_use]
pub fn len(&self) -> usize {
self.data.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct DiskUsageRequest {
pub paths: Vec<String>,
pub recursion_depth: i32,
pub all: bool,
pub threshold: i64,
}
impl DiskUsageRequest {
#[must_use]
pub fn new(path: impl Into<String>) -> Self {
Self {
paths: vec![path.into()],
..Default::default()
}
}
#[must_use]
pub fn for_paths(paths: Vec<String>) -> Self {
Self {
paths,
..Default::default()
}
}
#[must_use]
pub fn builder() -> DiskUsageRequestBuilder {
DiskUsageRequestBuilder::default()
}
}
impl From<DiskUsageRequest> for ProtoDiskUsageRequest {
fn from(req: DiskUsageRequest) -> Self {
Self {
paths: req.paths,
recursion_depth: req.recursion_depth,
all: req.all,
threshold: req.threshold,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct DiskUsageRequestBuilder {
paths: Vec<String>,
recursion_depth: i32,
all: bool,
threshold: i64,
}
impl DiskUsageRequestBuilder {
#[must_use]
pub fn path(mut self, path: impl Into<String>) -> Self {
self.paths.push(path.into());
self
}
#[must_use]
pub fn paths(mut self, paths: Vec<String>) -> Self {
self.paths.extend(paths);
self
}
#[must_use]
pub fn recursion_depth(mut self, depth: i32) -> Self {
self.recursion_depth = depth;
self
}
#[must_use]
pub fn all(mut self, all: bool) -> Self {
self.all = all;
self
}
#[must_use]
pub fn threshold(mut self, threshold: i64) -> Self {
self.threshold = threshold;
self
}
#[must_use]
pub fn build(self) -> DiskUsageRequest {
DiskUsageRequest {
paths: self.paths,
recursion_depth: self.recursion_depth,
all: self.all,
threshold: self.threshold,
}
}
}
#[derive(Debug, Clone)]
pub struct DiskUsageInfo {
pub node: Option<String>,
pub name: String,
pub size: i64,
pub error: Option<String>,
pub relative_name: String,
}
impl From<ProtoDiskUsageInfo> for DiskUsageInfo {
fn from(proto: ProtoDiskUsageInfo) -> Self {
Self {
node: proto.metadata.map(|m| m.hostname),
name: proto.name,
size: proto.size,
error: if proto.error.is_empty() {
None
} else {
Some(proto.error)
},
relative_name: proto.relative_name,
}
}
}
impl DiskUsageInfo {
#[must_use]
pub fn has_error(&self) -> bool {
self.error.is_some()
}
#[must_use]
pub fn size_human(&self) -> String {
humanize_bytes(self.size as u64)
}
}
#[derive(Debug, Clone, Default)]
pub struct DiskUsageResponse {
pub entries: Vec<DiskUsageInfo>,
}
impl DiskUsageResponse {
#[must_use]
pub fn new(entries: Vec<DiskUsageInfo>) -> Self {
Self { entries }
}
#[must_use]
pub fn total_size(&self) -> i64 {
self.entries.iter().map(|e| e.size).sum()
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
fn humanize_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
const TB: u64 = GB * 1024;
if bytes >= TB {
format!("{:.2} TB", bytes as f64 / TB as f64)
} else if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{bytes} B")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_list_request_new() {
let req = ListRequest::new("/var/log");
assert_eq!(req.root, "/var/log");
assert!(!req.recurse);
}
#[test]
fn test_list_request_builder() {
let req = ListRequest::builder("/etc")
.recurse(true)
.recursion_depth(3)
.types(vec![FileType::Regular, FileType::Directory])
.report_xattrs(true)
.build();
assert_eq!(req.root, "/etc");
assert!(req.recurse);
assert_eq!(req.recursion_depth, 3);
assert_eq!(req.types.len(), 2);
assert!(req.report_xattrs);
}
#[test]
fn test_file_info() {
let info = FileInfo {
node: Some("node1".to_string()),
name: "/var/log/syslog".to_string(),
size: 1024,
mode: 0o644,
modified: 1234567890,
is_dir: false,
error: None,
link: None,
relative_name: "syslog".to_string(),
uid: 0,
gid: 0,
};
assert!(info.is_file());
assert!(!info.is_dir);
assert!(!info.is_symlink());
assert!(!info.has_error());
}
#[test]
fn test_read_request() {
let req = ReadRequest::new("/etc/hosts");
assert_eq!(req.path, "/etc/hosts");
}
#[test]
fn test_read_response() {
let resp = ReadResponse::new(b"hello world".to_vec(), Some("node1".to_string()));
assert_eq!(resp.as_str(), Some("hello world"));
assert_eq!(resp.len(), 11);
}
#[test]
fn test_copy_request() {
let req = CopyRequest::new("/var/log");
assert_eq!(req.root_path, "/var/log");
}
#[test]
fn test_disk_usage_request() {
let req = DiskUsageRequest::new("/var");
assert_eq!(req.paths, vec!["/var"]);
}
#[test]
fn test_disk_usage_request_builder() {
let req = DiskUsageRequest::builder()
.path("/var")
.path("/tmp")
.recursion_depth(2)
.all(true)
.threshold(1024)
.build();
assert_eq!(req.paths, vec!["/var", "/tmp"]);
assert_eq!(req.recursion_depth, 2);
assert!(req.all);
assert_eq!(req.threshold, 1024);
}
#[test]
fn test_humanize_bytes() {
assert_eq!(humanize_bytes(512), "512 B");
assert_eq!(humanize_bytes(1024), "1.00 KB");
assert_eq!(humanize_bytes(1024 * 1024), "1.00 MB");
assert_eq!(humanize_bytes(1024 * 1024 * 1024), "1.00 GB");
}
}