1use crate::api::generated::machine::{
8 CopyRequest as ProtoCopyRequest, DiskUsageInfo as ProtoDiskUsageInfo,
9 DiskUsageRequest as ProtoDiskUsageRequest, FileInfo as ProtoFileInfo,
10 ListRequest as ProtoListRequest, ReadRequest as ProtoReadRequest,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum FileType {
20 #[default]
22 Regular,
23 Directory,
25 Symlink,
27}
28
29impl From<FileType> for i32 {
30 fn from(ft: FileType) -> Self {
31 match ft {
32 FileType::Regular => 0,
33 FileType::Directory => 1,
34 FileType::Symlink => 2,
35 }
36 }
37}
38
39#[derive(Debug, Clone, Default)]
41pub struct ListRequest {
42 pub root: String,
44 pub recurse: bool,
46 pub recursion_depth: i32,
48 pub types: Vec<FileType>,
50 pub report_xattrs: bool,
52}
53
54impl ListRequest {
55 #[must_use]
57 pub fn new(root: impl Into<String>) -> Self {
58 Self {
59 root: root.into(),
60 ..Default::default()
61 }
62 }
63
64 #[must_use]
66 pub fn builder(root: impl Into<String>) -> ListRequestBuilder {
67 ListRequestBuilder::new(root)
68 }
69}
70
71impl From<ListRequest> for ProtoListRequest {
72 fn from(req: ListRequest) -> Self {
73 Self {
74 root: req.root,
75 recurse: req.recurse,
76 recursion_depth: req.recursion_depth,
77 types: req.types.into_iter().map(i32::from).collect(),
78 report_xattrs: req.report_xattrs,
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
85pub struct ListRequestBuilder {
86 root: String,
87 recurse: bool,
88 recursion_depth: i32,
89 types: Vec<FileType>,
90 report_xattrs: bool,
91}
92
93impl ListRequestBuilder {
94 #[must_use]
96 pub fn new(root: impl Into<String>) -> Self {
97 Self {
98 root: root.into(),
99 recurse: false,
100 recursion_depth: 0,
101 types: Vec::new(),
102 report_xattrs: false,
103 }
104 }
105
106 #[must_use]
108 pub fn recurse(mut self, recurse: bool) -> Self {
109 self.recurse = recurse;
110 self
111 }
112
113 #[must_use]
115 pub fn recursion_depth(mut self, depth: i32) -> Self {
116 self.recursion_depth = depth;
117 self
118 }
119
120 #[must_use]
122 pub fn types(mut self, types: Vec<FileType>) -> Self {
123 self.types = types;
124 self
125 }
126
127 #[must_use]
129 pub fn report_xattrs(mut self, report: bool) -> Self {
130 self.report_xattrs = report;
131 self
132 }
133
134 #[must_use]
136 pub fn build(self) -> ListRequest {
137 ListRequest {
138 root: self.root,
139 recurse: self.recurse,
140 recursion_depth: self.recursion_depth,
141 types: self.types,
142 report_xattrs: self.report_xattrs,
143 }
144 }
145}
146
147#[derive(Debug, Clone)]
149pub struct FileInfo {
150 pub node: Option<String>,
152 pub name: String,
154 pub size: i64,
156 pub mode: u32,
158 pub modified: i64,
160 pub is_dir: bool,
162 pub error: Option<String>,
164 pub link: Option<String>,
166 pub relative_name: String,
168 pub uid: u32,
170 pub gid: u32,
172}
173
174impl From<ProtoFileInfo> for FileInfo {
175 fn from(proto: ProtoFileInfo) -> Self {
176 Self {
177 node: proto.metadata.map(|m| m.hostname),
178 name: proto.name,
179 size: proto.size,
180 mode: proto.mode,
181 modified: proto.modified,
182 is_dir: proto.is_dir,
183 error: if proto.error.is_empty() {
184 None
185 } else {
186 Some(proto.error)
187 },
188 link: if proto.link.is_empty() {
189 None
190 } else {
191 Some(proto.link)
192 },
193 relative_name: proto.relative_name,
194 uid: proto.uid,
195 gid: proto.gid,
196 }
197 }
198}
199
200impl FileInfo {
201 #[must_use]
203 pub fn has_error(&self) -> bool {
204 self.error.is_some()
205 }
206
207 #[must_use]
209 pub fn is_file(&self) -> bool {
210 !self.is_dir && self.link.is_none()
211 }
212
213 #[must_use]
215 pub fn is_symlink(&self) -> bool {
216 self.link.is_some()
217 }
218}
219
220#[derive(Debug, Clone, Default)]
222pub struct ListResponse {
223 pub entries: Vec<FileInfo>,
225}
226
227impl ListResponse {
228 #[must_use]
230 pub fn new(entries: Vec<FileInfo>) -> Self {
231 Self { entries }
232 }
233
234 #[must_use]
236 pub fn len(&self) -> usize {
237 self.entries.len()
238 }
239
240 #[must_use]
242 pub fn is_empty(&self) -> bool {
243 self.entries.is_empty()
244 }
245
246 #[must_use]
248 pub fn directories(&self) -> Vec<&FileInfo> {
249 self.entries.iter().filter(|e| e.is_dir).collect()
250 }
251
252 #[must_use]
254 pub fn files(&self) -> Vec<&FileInfo> {
255 self.entries.iter().filter(|e| e.is_file()).collect()
256 }
257}
258
259#[derive(Debug, Clone)]
265pub struct ReadRequest {
266 pub path: String,
268}
269
270impl ReadRequest {
271 #[must_use]
273 pub fn new(path: impl Into<String>) -> Self {
274 Self { path: path.into() }
275 }
276}
277
278impl From<ReadRequest> for ProtoReadRequest {
279 fn from(req: ReadRequest) -> Self {
280 Self { path: req.path }
281 }
282}
283
284#[derive(Debug, Clone, Default)]
286pub struct ReadResponse {
287 pub data: Vec<u8>,
289 pub node: Option<String>,
291}
292
293impl ReadResponse {
294 #[must_use]
296 pub fn new(data: Vec<u8>, node: Option<String>) -> Self {
297 Self { data, node }
298 }
299
300 #[must_use]
304 pub fn as_str(&self) -> Option<&str> {
305 std::str::from_utf8(&self.data).ok()
306 }
307
308 #[must_use]
310 pub fn as_string_lossy(&self) -> String {
311 String::from_utf8_lossy(&self.data).into_owned()
312 }
313
314 #[must_use]
316 pub fn len(&self) -> usize {
317 self.data.len()
318 }
319
320 #[must_use]
322 pub fn is_empty(&self) -> bool {
323 self.data.is_empty()
324 }
325}
326
327#[derive(Debug, Clone)]
333pub struct CopyRequest {
334 pub root_path: String,
336}
337
338impl CopyRequest {
339 #[must_use]
341 pub fn new(root_path: impl Into<String>) -> Self {
342 Self {
343 root_path: root_path.into(),
344 }
345 }
346}
347
348impl From<CopyRequest> for ProtoCopyRequest {
349 fn from(req: CopyRequest) -> Self {
350 Self {
351 root_path: req.root_path,
352 }
353 }
354}
355
356#[derive(Debug, Clone, Default)]
358pub struct CopyResponse {
359 pub data: Vec<u8>,
361 pub node: Option<String>,
363}
364
365impl CopyResponse {
366 #[must_use]
368 pub fn new(data: Vec<u8>, node: Option<String>) -> Self {
369 Self { data, node }
370 }
371
372 #[must_use]
374 pub fn len(&self) -> usize {
375 self.data.len()
376 }
377
378 #[must_use]
380 pub fn is_empty(&self) -> bool {
381 self.data.is_empty()
382 }
383}
384
385#[derive(Debug, Clone, Default)]
391pub struct DiskUsageRequest {
392 pub paths: Vec<String>,
394 pub recursion_depth: i32,
396 pub all: bool,
398 pub threshold: i64,
400}
401
402impl DiskUsageRequest {
403 #[must_use]
405 pub fn new(path: impl Into<String>) -> Self {
406 Self {
407 paths: vec![path.into()],
408 ..Default::default()
409 }
410 }
411
412 #[must_use]
414 pub fn for_paths(paths: Vec<String>) -> Self {
415 Self {
416 paths,
417 ..Default::default()
418 }
419 }
420
421 #[must_use]
423 pub fn builder() -> DiskUsageRequestBuilder {
424 DiskUsageRequestBuilder::default()
425 }
426}
427
428impl From<DiskUsageRequest> for ProtoDiskUsageRequest {
429 fn from(req: DiskUsageRequest) -> Self {
430 Self {
431 paths: req.paths,
432 recursion_depth: req.recursion_depth,
433 all: req.all,
434 threshold: req.threshold,
435 }
436 }
437}
438
439#[derive(Debug, Clone, Default)]
441pub struct DiskUsageRequestBuilder {
442 paths: Vec<String>,
443 recursion_depth: i32,
444 all: bool,
445 threshold: i64,
446}
447
448impl DiskUsageRequestBuilder {
449 #[must_use]
451 pub fn path(mut self, path: impl Into<String>) -> Self {
452 self.paths.push(path.into());
453 self
454 }
455
456 #[must_use]
458 pub fn paths(mut self, paths: Vec<String>) -> Self {
459 self.paths.extend(paths);
460 self
461 }
462
463 #[must_use]
465 pub fn recursion_depth(mut self, depth: i32) -> Self {
466 self.recursion_depth = depth;
467 self
468 }
469
470 #[must_use]
472 pub fn all(mut self, all: bool) -> Self {
473 self.all = all;
474 self
475 }
476
477 #[must_use]
479 pub fn threshold(mut self, threshold: i64) -> Self {
480 self.threshold = threshold;
481 self
482 }
483
484 #[must_use]
486 pub fn build(self) -> DiskUsageRequest {
487 DiskUsageRequest {
488 paths: self.paths,
489 recursion_depth: self.recursion_depth,
490 all: self.all,
491 threshold: self.threshold,
492 }
493 }
494}
495
496#[derive(Debug, Clone)]
498pub struct DiskUsageInfo {
499 pub node: Option<String>,
501 pub name: String,
503 pub size: i64,
505 pub error: Option<String>,
507 pub relative_name: String,
509}
510
511impl From<ProtoDiskUsageInfo> for DiskUsageInfo {
512 fn from(proto: ProtoDiskUsageInfo) -> Self {
513 Self {
514 node: proto.metadata.map(|m| m.hostname),
515 name: proto.name,
516 size: proto.size,
517 error: if proto.error.is_empty() {
518 None
519 } else {
520 Some(proto.error)
521 },
522 relative_name: proto.relative_name,
523 }
524 }
525}
526
527impl DiskUsageInfo {
528 #[must_use]
530 pub fn has_error(&self) -> bool {
531 self.error.is_some()
532 }
533
534 #[must_use]
536 pub fn size_human(&self) -> String {
537 humanize_bytes(self.size as u64)
538 }
539}
540
541#[derive(Debug, Clone, Default)]
543pub struct DiskUsageResponse {
544 pub entries: Vec<DiskUsageInfo>,
546}
547
548impl DiskUsageResponse {
549 #[must_use]
551 pub fn new(entries: Vec<DiskUsageInfo>) -> Self {
552 Self { entries }
553 }
554
555 #[must_use]
557 pub fn total_size(&self) -> i64 {
558 self.entries.iter().map(|e| e.size).sum()
559 }
560
561 #[must_use]
563 pub fn len(&self) -> usize {
564 self.entries.len()
565 }
566
567 #[must_use]
569 pub fn is_empty(&self) -> bool {
570 self.entries.is_empty()
571 }
572}
573
574fn humanize_bytes(bytes: u64) -> String {
576 const KB: u64 = 1024;
577 const MB: u64 = KB * 1024;
578 const GB: u64 = MB * 1024;
579 const TB: u64 = GB * 1024;
580
581 if bytes >= TB {
582 format!("{:.2} TB", bytes as f64 / TB as f64)
583 } else if bytes >= GB {
584 format!("{:.2} GB", bytes as f64 / GB as f64)
585 } else if bytes >= MB {
586 format!("{:.2} MB", bytes as f64 / MB as f64)
587 } else if bytes >= KB {
588 format!("{:.2} KB", bytes as f64 / KB as f64)
589 } else {
590 format!("{bytes} B")
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 #[test]
599 fn test_list_request_new() {
600 let req = ListRequest::new("/var/log");
601 assert_eq!(req.root, "/var/log");
602 assert!(!req.recurse);
603 }
604
605 #[test]
606 fn test_list_request_builder() {
607 let req = ListRequest::builder("/etc")
608 .recurse(true)
609 .recursion_depth(3)
610 .types(vec![FileType::Regular, FileType::Directory])
611 .report_xattrs(true)
612 .build();
613
614 assert_eq!(req.root, "/etc");
615 assert!(req.recurse);
616 assert_eq!(req.recursion_depth, 3);
617 assert_eq!(req.types.len(), 2);
618 assert!(req.report_xattrs);
619 }
620
621 #[test]
622 fn test_file_info() {
623 let info = FileInfo {
624 node: Some("node1".to_string()),
625 name: "/var/log/syslog".to_string(),
626 size: 1024,
627 mode: 0o644,
628 modified: 1234567890,
629 is_dir: false,
630 error: None,
631 link: None,
632 relative_name: "syslog".to_string(),
633 uid: 0,
634 gid: 0,
635 };
636
637 assert!(info.is_file());
638 assert!(!info.is_dir);
639 assert!(!info.is_symlink());
640 assert!(!info.has_error());
641 }
642
643 #[test]
644 fn test_read_request() {
645 let req = ReadRequest::new("/etc/hosts");
646 assert_eq!(req.path, "/etc/hosts");
647 }
648
649 #[test]
650 fn test_read_response() {
651 let resp = ReadResponse::new(b"hello world".to_vec(), Some("node1".to_string()));
652 assert_eq!(resp.as_str(), Some("hello world"));
653 assert_eq!(resp.len(), 11);
654 }
655
656 #[test]
657 fn test_copy_request() {
658 let req = CopyRequest::new("/var/log");
659 assert_eq!(req.root_path, "/var/log");
660 }
661
662 #[test]
663 fn test_disk_usage_request() {
664 let req = DiskUsageRequest::new("/var");
665 assert_eq!(req.paths, vec!["/var"]);
666 }
667
668 #[test]
669 fn test_disk_usage_request_builder() {
670 let req = DiskUsageRequest::builder()
671 .path("/var")
672 .path("/tmp")
673 .recursion_depth(2)
674 .all(true)
675 .threshold(1024)
676 .build();
677
678 assert_eq!(req.paths, vec!["/var", "/tmp"]);
679 assert_eq!(req.recursion_depth, 2);
680 assert!(req.all);
681 assert_eq!(req.threshold, 1024);
682 }
683
684 #[test]
685 fn test_humanize_bytes() {
686 assert_eq!(humanize_bytes(512), "512 B");
687 assert_eq!(humanize_bytes(1024), "1.00 KB");
688 assert_eq!(humanize_bytes(1024 * 1024), "1.00 MB");
689 assert_eq!(humanize_bytes(1024 * 1024 * 1024), "1.00 GB");
690 }
691}