1use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11#[cfg(feature = "openapi")]
12use utoipa::ToSchema;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16#[cfg_attr(feature = "openapi", derive(ToSchema))]
17pub struct FileInfo {
18 pub id: Uuid,
20 pub session_id: Uuid,
22 pub path: String,
24 pub name: String,
26 pub is_directory: bool,
28 pub is_readonly: bool,
30 pub size_bytes: i64,
32 pub created_at: DateTime<Utc>,
34 pub updated_at: DateTime<Utc>,
36}
37
38impl FileInfo {
39 pub fn name_from_path(path: &str) -> String {
41 if path == "/" {
42 "/".to_string()
43 } else {
44 path.rsplit('/').next().unwrap_or(path).to_string()
45 }
46 }
47
48 pub fn parent_path(path: &str) -> Option<String> {
50 if path == "/" {
51 None
52 } else {
53 let parent = path.rsplit_once('/').map(|(p, _)| p).unwrap_or("/");
54 Some(if parent.is_empty() { "/" } else { parent }.to_string())
55 }
56 }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61#[cfg_attr(feature = "openapi", derive(ToSchema))]
62pub struct SessionFile {
63 pub id: Uuid,
65 pub session_id: Uuid,
67 pub path: String,
69 pub name: String,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub content: Option<String>,
74 #[serde(default = "default_encoding")]
76 pub encoding: String,
77 pub is_directory: bool,
79 pub is_readonly: bool,
81 pub size_bytes: i64,
83 pub created_at: DateTime<Utc>,
85 pub updated_at: DateTime<Utc>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
91#[cfg_attr(feature = "openapi", derive(ToSchema))]
92pub struct InitialFile {
93 pub path: String,
95 pub content: String,
97 #[serde(default = "default_encoding")]
99 pub encoding: String,
100 #[serde(default)]
102 pub is_readonly: bool,
103}
104
105fn default_encoding() -> String {
106 "text".to_string()
107}
108
109impl SessionFile {
110 pub fn is_text_content(bytes: &[u8]) -> bool {
112 let check_len = bytes.len().min(8192);
114 !bytes[..check_len].contains(&0)
115 }
116
117 pub fn encode_content(bytes: &[u8]) -> (String, String) {
119 if Self::is_text_content(bytes) {
120 match String::from_utf8(bytes.to_vec()) {
121 Ok(text) => (text, "text".to_string()),
122 Err(_) => (BASE64.encode(bytes), "base64".to_string()),
123 }
124 } else {
125 (BASE64.encode(bytes), "base64".to_string())
126 }
127 }
128
129 pub fn decode_content(content: &str, encoding: &str) -> Result<Vec<u8>, base64::DecodeError> {
131 match encoding {
132 "base64" => BASE64.decode(content),
133 _ => Ok(content.as_bytes().to_vec()),
134 }
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140#[cfg_attr(feature = "openapi", derive(ToSchema))]
141pub struct FileStat {
142 pub path: String,
144 pub name: String,
146 pub is_directory: bool,
148 pub is_readonly: bool,
150 pub size_bytes: i64,
152 pub created_at: DateTime<Utc>,
154 pub updated_at: DateTime<Utc>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160#[cfg_attr(feature = "openapi", derive(ToSchema))]
161pub struct GrepMatch {
162 pub path: String,
163 pub line_number: usize,
164 pub line: String,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169#[cfg_attr(feature = "openapi", derive(ToSchema))]
170pub struct GrepResult {
171 pub path: String,
172 pub matches: Vec<GrepMatch>,
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn test_name_from_path() {
181 assert_eq!(FileInfo::name_from_path("/"), "/");
182 assert_eq!(FileInfo::name_from_path("/foo"), "foo");
183 assert_eq!(FileInfo::name_from_path("/foo/bar"), "bar");
184 assert_eq!(FileInfo::name_from_path("/foo/bar/baz.txt"), "baz.txt");
185 }
186
187 #[test]
188 fn test_parent_path() {
189 assert_eq!(FileInfo::parent_path("/"), None);
190 assert_eq!(FileInfo::parent_path("/foo"), Some("/".to_string()));
191 assert_eq!(FileInfo::parent_path("/foo/bar"), Some("/foo".to_string()));
192 assert_eq!(
193 FileInfo::parent_path("/foo/bar/baz"),
194 Some("/foo/bar".to_string())
195 );
196 }
197
198 #[test]
199 fn test_is_text_content() {
200 assert!(SessionFile::is_text_content(b"hello world"));
201 assert!(SessionFile::is_text_content(b"line1\nline2\n"));
202 assert!(!SessionFile::is_text_content(b"hello\0world"));
203 }
204
205 #[test]
206 fn test_encode_content_text() {
207 let (content, encoding) = SessionFile::encode_content(b"hello world");
208 assert_eq!(content, "hello world");
209 assert_eq!(encoding, "text");
210 }
211
212 #[test]
213 fn test_encode_content_binary() {
214 let binary = b"\x89PNG\r\n\x1a\n\0";
216 let (content, encoding) = SessionFile::encode_content(binary);
217 assert_eq!(encoding, "base64");
218 assert!(!content.is_empty());
219 }
220
221 #[test]
222 fn test_decode_content_text() {
223 let decoded = SessionFile::decode_content("hello world", "text").unwrap();
224 assert_eq!(decoded, b"hello world");
225 }
226
227 #[test]
228 fn test_decode_content_base64() {
229 let decoded = SessionFile::decode_content("aGVsbG8=", "base64").unwrap();
230 assert_eq!(decoded, b"hello");
231 }
232
233 #[test]
234 fn test_encode_decode_roundtrip() {
235 let original = b"Test content with special chars: \xc3\xa9\xc3\xa0";
236 let (encoded, encoding) = SessionFile::encode_content(original);
237 let decoded = SessionFile::decode_content(&encoded, &encoding).unwrap();
238 assert_eq!(decoded, original);
239 }
240
241 #[test]
242 fn test_file_info_serialization() {
243 let file_info = FileInfo {
244 id: Uuid::nil(),
245 session_id: Uuid::nil(),
246 path: "/test.txt".to_string(),
247 name: "test.txt".to_string(),
248 is_directory: false,
249 is_readonly: false,
250 size_bytes: 100,
251 created_at: DateTime::default(),
252 updated_at: DateTime::default(),
253 };
254
255 let json = serde_json::to_string(&file_info).unwrap();
256 assert!(json.contains("\"path\":\"/test.txt\""));
257 assert!(json.contains("\"is_directory\":false"));
258 }
259
260 #[test]
261 fn test_grep_result_serialization() {
262 let result = GrepResult {
263 path: "/test.txt".to_string(),
264 matches: vec![GrepMatch {
265 path: "/test.txt".to_string(),
266 line_number: 1,
267 line: "hello world".to_string(),
268 }],
269 };
270
271 let json = serde_json::to_string(&result).unwrap();
272 assert!(json.contains("\"line_number\":1"));
273 assert!(json.contains("\"line\":\"hello world\""));
274 }
275}