ssh_commander_core/
file_entry.rs1use serde::Serialize;
9
10pub const FILE_TRANSFER_CHUNK_SIZE: usize = 32 * 1024;
13
14#[derive(Debug, Clone, Serialize)]
17pub struct FileEntry {
18 pub name: String,
19 pub size: u64,
20 pub modified: Option<String>,
22 pub modified_unix: Option<i64>,
27 pub permissions: Option<String>,
28 pub owner: Option<String>,
29 pub group: Option<String>,
30 pub file_type: FileEntryType,
31}
32
33pub type RemoteFileEntry = FileEntry;
35
36#[derive(Debug, Clone, Serialize, PartialEq)]
37pub enum FileEntryType {
38 File,
39 Directory,
40 Symlink,
41}
42
43pub fn format_unix_timestamp(secs: i64) -> String {
47 chrono::DateTime::<chrono::Utc>::from_timestamp(secs, 0)
48 .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
49 .unwrap_or_else(|| "invalid-timestamp".to_string())
50}
51
52pub(crate) fn format_permissions(mode: u32) -> String {
54 let mut s = String::with_capacity(9);
55 let flags = [
56 (0o400, 'r'),
57 (0o200, 'w'),
58 (0o100, 'x'),
59 (0o040, 'r'),
60 (0o020, 'w'),
61 (0o010, 'x'),
62 (0o004, 'r'),
63 (0o002, 'w'),
64 (0o001, 'x'),
65 ];
66 for (bit, ch) in flags.iter() {
67 if mode & bit != 0 {
68 s.push(*ch);
69 } else {
70 s.push('-');
71 }
72 }
73 s
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn test_format_permissions_full() {
82 assert_eq!(format_permissions(0o777), "rwxrwxrwx");
83 }
84
85 #[test]
86 fn test_format_permissions_none() {
87 assert_eq!(format_permissions(0o000), "---------");
88 }
89
90 #[test]
91 fn test_format_permissions_typical_file() {
92 assert_eq!(format_permissions(0o644), "rw-r--r--");
93 }
94
95 #[test]
96 fn test_format_permissions_typical_dir() {
97 assert_eq!(format_permissions(0o755), "rwxr-xr-x");
98 }
99
100 #[test]
101 fn test_format_permissions_write_only() {
102 assert_eq!(format_permissions(0o200), "-w-------");
103 }
104
105 #[test]
106 fn format_unix_timestamp_epoch() {
107 assert_eq!(format_unix_timestamp(0), "1970-01-01 00:00:00");
108 }
109
110 #[test]
111 fn format_unix_timestamp_known_date() {
112 assert_eq!(format_unix_timestamp(1704067200), "2024-01-01 00:00:00");
114 }
115
116 #[test]
117 fn format_unix_timestamp_with_time() {
118 assert_eq!(format_unix_timestamp(961068645), "2000-06-15 11:30:45");
120 }
121
122 #[test]
123 fn format_unix_timestamp_post_2106() {
124 assert_eq!(format_unix_timestamp(7258118400), "2200-01-01 00:00:00");
127 }
128
129 #[test]
130 fn test_file_entry_type_serialization() {
131 let entry = RemoteFileEntry {
132 name: "test.txt".to_string(),
133 size: 1024,
134 modified: Some("2024-01-01 00:00:00".to_string()),
135 modified_unix: Some(1_704_067_200),
136 permissions: Some("rw-r--r--".to_string()),
137 owner: Some("501".to_string()),
138 group: Some("20".to_string()),
139 file_type: FileEntryType::File,
140 };
141 let json = serde_json::to_string(&entry).unwrap();
142 assert!(json.contains("\"name\":\"test.txt\""));
143 assert!(json.contains("\"size\":1024"));
144 assert!(json.contains("File"));
145 }
146
147 #[test]
148 fn test_directory_entry_serialization() {
149 let entry = RemoteFileEntry {
150 name: "mydir".to_string(),
151 size: 4096,
152 modified: None,
153 modified_unix: None,
154 permissions: Some("rwxr-xr-x".to_string()),
155 owner: None,
156 group: None,
157 file_type: FileEntryType::Directory,
158 };
159 let json = serde_json::to_string(&entry).unwrap();
160 assert!(json.contains("Directory"));
161 assert!(json.contains("\"modified\":null"));
162 }
163
164 #[test]
165 fn test_symlink_entry_serialization() {
166 let entry = RemoteFileEntry {
167 name: "link".to_string(),
168 size: 0,
169 modified: None,
170 modified_unix: None,
171 permissions: None,
172 owner: None,
173 group: None,
174 file_type: FileEntryType::Symlink,
175 };
176 let json = serde_json::to_string(&entry).unwrap();
177 assert!(json.contains("Symlink"));
178 }
179}