Skip to main content

talos_api_rs/resources/
files.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Typed wrappers for File Operation APIs.
4//!
5//! Provides access to file listing, reading, copying, and disk usage.
6
7use 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// =============================================================================
14// List (Directory Listing)
15// =============================================================================
16
17/// Type of file to filter.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum FileType {
20    /// Regular file.
21    #[default]
22    Regular,
23    /// Directory.
24    Directory,
25    /// Symbolic link.
26    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/// Request to list directory contents.
40#[derive(Debug, Clone, Default)]
41pub struct ListRequest {
42    /// Root directory to list.
43    pub root: String,
44    /// Whether to recurse into subdirectories.
45    pub recurse: bool,
46    /// Maximum recursion depth (0 = unlimited).
47    pub recursion_depth: i32,
48    /// File types to include.
49    pub types: Vec<FileType>,
50    /// Whether to report extended attributes.
51    pub report_xattrs: bool,
52}
53
54impl ListRequest {
55    /// Create a new list request for a path.
56    #[must_use]
57    pub fn new(root: impl Into<String>) -> Self {
58        Self {
59            root: root.into(),
60            ..Default::default()
61        }
62    }
63
64    /// Create a builder for more complex requests.
65    #[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/// Builder for `ListRequest`.
84#[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    /// Create a new builder.
95    #[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    /// Enable recursive listing.
107    #[must_use]
108    pub fn recurse(mut self, recurse: bool) -> Self {
109        self.recurse = recurse;
110        self
111    }
112
113    /// Set maximum recursion depth.
114    #[must_use]
115    pub fn recursion_depth(mut self, depth: i32) -> Self {
116        self.recursion_depth = depth;
117        self
118    }
119
120    /// Filter by file types.
121    #[must_use]
122    pub fn types(mut self, types: Vec<FileType>) -> Self {
123        self.types = types;
124        self
125    }
126
127    /// Report extended attributes.
128    #[must_use]
129    pub fn report_xattrs(mut self, report: bool) -> Self {
130        self.report_xattrs = report;
131        self
132    }
133
134    /// Build the request.
135    #[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/// Information about a file or directory.
148#[derive(Debug, Clone)]
149pub struct FileInfo {
150    /// Node that returned this info.
151    pub node: Option<String>,
152    /// File name (including path).
153    pub name: String,
154    /// File size in bytes.
155    pub size: i64,
156    /// UNIX mode/permission flags.
157    pub mode: u32,
158    /// UNIX timestamp of last modification.
159    pub modified: i64,
160    /// Whether this is a directory.
161    pub is_dir: bool,
162    /// Error message if any.
163    pub error: Option<String>,
164    /// Symlink target if this is a symlink.
165    pub link: Option<String>,
166    /// Relative name from root path.
167    pub relative_name: String,
168    /// Owner UID.
169    pub uid: u32,
170    /// Owner GID.
171    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    /// Check if this entry has an error.
202    #[must_use]
203    pub fn has_error(&self) -> bool {
204        self.error.is_some()
205    }
206
207    /// Check if this is a regular file.
208    #[must_use]
209    pub fn is_file(&self) -> bool {
210        !self.is_dir && self.link.is_none()
211    }
212
213    /// Check if this is a symlink.
214    #[must_use]
215    pub fn is_symlink(&self) -> bool {
216        self.link.is_some()
217    }
218}
219
220/// Response from a list request (streaming).
221#[derive(Debug, Clone, Default)]
222pub struct ListResponse {
223    /// File entries.
224    pub entries: Vec<FileInfo>,
225}
226
227impl ListResponse {
228    /// Create a new response.
229    #[must_use]
230    pub fn new(entries: Vec<FileInfo>) -> Self {
231        Self { entries }
232    }
233
234    /// Get the number of entries.
235    #[must_use]
236    pub fn len(&self) -> usize {
237        self.entries.len()
238    }
239
240    /// Check if empty.
241    #[must_use]
242    pub fn is_empty(&self) -> bool {
243        self.entries.is_empty()
244    }
245
246    /// Get only directories.
247    #[must_use]
248    pub fn directories(&self) -> Vec<&FileInfo> {
249        self.entries.iter().filter(|e| e.is_dir).collect()
250    }
251
252    /// Get only files.
253    #[must_use]
254    pub fn files(&self) -> Vec<&FileInfo> {
255        self.entries.iter().filter(|e| e.is_file()).collect()
256    }
257}
258
259// =============================================================================
260// Read (File Reading)
261// =============================================================================
262
263/// Request to read a file.
264#[derive(Debug, Clone)]
265pub struct ReadRequest {
266    /// Path to the file to read.
267    pub path: String,
268}
269
270impl ReadRequest {
271    /// Create a new read request.
272    #[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/// Response from a read request (streaming).
285#[derive(Debug, Clone, Default)]
286pub struct ReadResponse {
287    /// File content.
288    pub data: Vec<u8>,
289    /// Node that returned this data.
290    pub node: Option<String>,
291}
292
293impl ReadResponse {
294    /// Create a new response.
295    #[must_use]
296    pub fn new(data: Vec<u8>, node: Option<String>) -> Self {
297        Self { data, node }
298    }
299
300    /// Get data as UTF-8 string.
301    ///
302    /// Returns `None` if the data is not valid UTF-8.
303    #[must_use]
304    pub fn as_str(&self) -> Option<&str> {
305        std::str::from_utf8(&self.data).ok()
306    }
307
308    /// Get data as lossy UTF-8 string.
309    #[must_use]
310    pub fn as_string_lossy(&self) -> String {
311        String::from_utf8_lossy(&self.data).into_owned()
312    }
313
314    /// Get data length.
315    #[must_use]
316    pub fn len(&self) -> usize {
317        self.data.len()
318    }
319
320    /// Check if empty.
321    #[must_use]
322    pub fn is_empty(&self) -> bool {
323        self.data.is_empty()
324    }
325}
326
327// =============================================================================
328// Copy (File/Directory Copy)
329// =============================================================================
330
331/// Request to copy a file or directory.
332#[derive(Debug, Clone)]
333pub struct CopyRequest {
334    /// Root path to copy from.
335    pub root_path: String,
336}
337
338impl CopyRequest {
339    /// Create a new copy request.
340    #[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/// Response from a copy request (streaming tar data).
357#[derive(Debug, Clone, Default)]
358pub struct CopyResponse {
359    /// Tar archive data.
360    pub data: Vec<u8>,
361    /// Node that returned this data.
362    pub node: Option<String>,
363}
364
365impl CopyResponse {
366    /// Create a new response.
367    #[must_use]
368    pub fn new(data: Vec<u8>, node: Option<String>) -> Self {
369        Self { data, node }
370    }
371
372    /// Get data length.
373    #[must_use]
374    pub fn len(&self) -> usize {
375        self.data.len()
376    }
377
378    /// Check if empty.
379    #[must_use]
380    pub fn is_empty(&self) -> bool {
381        self.data.is_empty()
382    }
383}
384
385// =============================================================================
386// DiskUsage
387// =============================================================================
388
389/// Request to get disk usage.
390#[derive(Debug, Clone, Default)]
391pub struct DiskUsageRequest {
392    /// Paths to calculate disk usage for.
393    pub paths: Vec<String>,
394    /// Maximum recursion depth (0 = unlimited).
395    pub recursion_depth: i32,
396    /// Include all files, not just directories.
397    pub all: bool,
398    /// Size threshold (positive = exclude smaller, negative = exclude larger).
399    pub threshold: i64,
400}
401
402impl DiskUsageRequest {
403    /// Create a new disk usage request for a path.
404    #[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    /// Create a request for multiple paths.
413    #[must_use]
414    pub fn for_paths(paths: Vec<String>) -> Self {
415        Self {
416            paths,
417            ..Default::default()
418        }
419    }
420
421    /// Create a builder for more complex requests.
422    #[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/// Builder for `DiskUsageRequest`.
440#[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    /// Add a path.
450    #[must_use]
451    pub fn path(mut self, path: impl Into<String>) -> Self {
452        self.paths.push(path.into());
453        self
454    }
455
456    /// Add multiple paths.
457    #[must_use]
458    pub fn paths(mut self, paths: Vec<String>) -> Self {
459        self.paths.extend(paths);
460        self
461    }
462
463    /// Set recursion depth.
464    #[must_use]
465    pub fn recursion_depth(mut self, depth: i32) -> Self {
466        self.recursion_depth = depth;
467        self
468    }
469
470    /// Include all files.
471    #[must_use]
472    pub fn all(mut self, all: bool) -> Self {
473        self.all = all;
474        self
475    }
476
477    /// Set size threshold.
478    #[must_use]
479    pub fn threshold(mut self, threshold: i64) -> Self {
480        self.threshold = threshold;
481        self
482    }
483
484    /// Build the request.
485    #[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/// Disk usage information for a file or directory.
497#[derive(Debug, Clone)]
498pub struct DiskUsageInfo {
499    /// Node that returned this info.
500    pub node: Option<String>,
501    /// File/directory name.
502    pub name: String,
503    /// Size in bytes.
504    pub size: i64,
505    /// Error message if any.
506    pub error: Option<String>,
507    /// Relative name from root.
508    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    /// Check if this entry has an error.
529    #[must_use]
530    pub fn has_error(&self) -> bool {
531        self.error.is_some()
532    }
533
534    /// Get size in human-readable format.
535    #[must_use]
536    pub fn size_human(&self) -> String {
537        humanize_bytes(self.size as u64)
538    }
539}
540
541/// Response from a disk usage request (streaming).
542#[derive(Debug, Clone, Default)]
543pub struct DiskUsageResponse {
544    /// Disk usage entries.
545    pub entries: Vec<DiskUsageInfo>,
546}
547
548impl DiskUsageResponse {
549    /// Create a new response.
550    #[must_use]
551    pub fn new(entries: Vec<DiskUsageInfo>) -> Self {
552        Self { entries }
553    }
554
555    /// Get total size across all entries.
556    #[must_use]
557    pub fn total_size(&self) -> i64 {
558        self.entries.iter().map(|e| e.size).sum()
559    }
560
561    /// Get the number of entries.
562    #[must_use]
563    pub fn len(&self) -> usize {
564        self.entries.len()
565    }
566
567    /// Check if empty.
568    #[must_use]
569    pub fn is_empty(&self) -> bool {
570        self.entries.is_empty()
571    }
572}
573
574/// Convert bytes to human-readable format.
575fn 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}