1use axum::{
7 extract::{Multipart, Path, Query, State},
8 http::StatusCode,
9 response::IntoResponse,
10 Json,
11};
12use serde::{Deserialize, Serialize};
13use tracing::{error, info};
14
15use crate::shared_state::UnifiedAppState;
16use crate::memory_db::{LocalFile, LocalFileTree};
17
18#[derive(Debug, Serialize)]
20pub struct FileEntryResponse {
21 pub id: i64,
22 pub name: String,
23 pub path: String,
24 #[serde(rename = "isDirectory")]
25 pub is_directory: bool,
26 pub size: i64,
27 pub modified: String,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub children: Option<Vec<FileEntryResponse>>,
30}
31
32impl From<LocalFile> for FileEntryResponse {
33 fn from(f: LocalFile) -> Self {
34 Self {
35 id: f.id,
36 name: f.name,
37 path: f.path,
38 is_directory: f.is_directory,
39 size: f.size_bytes,
40 modified: f.modified_at.to_rfc3339(),
41 children: None,
42 }
43 }
44}
45
46impl From<LocalFileTree> for FileEntryResponse {
47 fn from(t: LocalFileTree) -> Self {
48 Self {
49 id: t.file.id,
50 name: t.file.name,
51 path: t.file.path,
52 is_directory: t.file.is_directory,
53 size: t.file.size_bytes,
54 modified: t.file.modified_at.to_rfc3339(),
55 children: t.children.map(|c| c.into_iter().map(FileEntryResponse::from).collect()),
56 }
57 }
58}
59
60#[derive(Debug, Deserialize)]
61pub struct CreateFolderRequest {
62 pub name: String,
63 #[serde(default)]
64 pub parent_id: Option<i64>,
65}
66
67#[derive(Debug, Deserialize)]
68pub struct DeleteQuery {
69 #[serde(default)]
70 pub path: Option<String>,
71 #[serde(default)]
72 pub id: Option<i64>,
73}
74
75#[derive(Debug, Deserialize)]
76pub struct SearchQuery {
77 pub q: String,
78}
79
80#[derive(Debug, Deserialize)]
81pub struct UploadQuery {
82 #[serde(default)]
83 pub parent_id: Option<i64>,
84}
85
86pub async fn get_files(
88 State(state): State<UnifiedAppState>,
89) -> Result<impl IntoResponse, StatusCode> {
90 let local_files = &state.shared_state.database_pool.local_files;
91
92 match local_files.get_file_tree() {
93 Ok(tree) => {
94 let response: Vec<FileEntryResponse> = tree.into_iter()
95 .map(FileEntryResponse::from)
96 .collect();
97 Ok(Json(response))
98 }
99 Err(e) => {
100 error!("Failed to get files: {}", e);
101 Ok(Json(Vec::<FileEntryResponse>::new()))
103 }
104 }
105}
106
107pub async fn get_file_by_id(
109 State(state): State<UnifiedAppState>,
110 Path(id): Path<i64>,
111) -> Result<impl IntoResponse, StatusCode> {
112 let local_files = &state.shared_state.database_pool.local_files;
113
114 match local_files.get_file(id) {
115 Ok(file) => Ok(Json(FileEntryResponse::from(file))),
116 Err(e) => {
117 error!("Failed to get file {}: {}", id, e);
118 Err(StatusCode::NOT_FOUND)
119 }
120 }
121}
122
123pub async fn get_file_content(
125 State(state): State<UnifiedAppState>,
126 Path(id): Path<i64>,
127) -> Result<impl IntoResponse, StatusCode> {
128 let local_files = &state.shared_state.database_pool.local_files;
129
130 match local_files.get_file_content_string(id) {
131 Ok(content) => {
132 Ok(Json(serde_json::json!({
133 "id": id,
134 "content": content
135 })))
136 }
137 Err(e) => {
138 error!("Failed to get file content {}: {}", id, e);
139 Err(StatusCode::NOT_FOUND)
140 }
141 }
142}
143
144pub async fn search_files(
146 State(state): State<UnifiedAppState>,
147 Query(query): Query<SearchQuery>,
148) -> Result<impl IntoResponse, StatusCode> {
149 let local_files = &state.shared_state.database_pool.local_files;
150
151 match local_files.search_files(&query.q) {
152 Ok(files) => {
153 let response: Vec<FileEntryResponse> = files.into_iter()
154 .map(FileEntryResponse::from)
155 .collect();
156 Ok(Json(response))
157 }
158 Err(e) => {
159 error!("Failed to search files: {}", e);
160 Ok(Json(Vec::<FileEntryResponse>::new()))
161 }
162 }
163}
164
165pub async fn get_all_files(
167 State(state): State<UnifiedAppState>,
168) -> Result<impl IntoResponse, StatusCode> {
169 let local_files = &state.shared_state.database_pool.local_files;
170
171 match local_files.get_all_files() {
172 Ok(files) => {
173 let response: Vec<FileEntryResponse> = files.into_iter()
174 .map(FileEntryResponse::from)
175 .collect();
176 Ok(Json(response))
177 }
178 Err(e) => {
179 error!("Failed to get all files: {}", e);
180 Ok(Json(Vec::<FileEntryResponse>::new()))
181 }
182 }
183}
184
185pub async fn create_folder(
187 State(state): State<UnifiedAppState>,
188 Json(request): Json<CreateFolderRequest>,
189) -> Result<impl IntoResponse, StatusCode> {
190 let local_files = &state.shared_state.database_pool.local_files;
191
192 if request.name.trim().is_empty() {
193 return Err(StatusCode::BAD_REQUEST);
194 }
195
196 match local_files.create_folder(request.parent_id, &request.name) {
197 Ok(folder) => {
198 info!("Created folder: {}", folder.path);
199 Ok(Json(serde_json::json!({
200 "message": "Folder created successfully",
201 "id": folder.id,
202 "path": folder.path
203 })))
204 }
205 Err(e) => {
206 error!("Failed to create folder '{}': {}", request.name, e);
207 Err(StatusCode::INTERNAL_SERVER_ERROR)
208 }
209 }
210}
211
212pub async fn upload_file(
214 State(state): State<UnifiedAppState>,
215 Query(query): Query<UploadQuery>,
216 mut multipart: Multipart,
217) -> Result<impl IntoResponse, StatusCode> {
218 let local_files = &state.shared_state.database_pool.local_files;
219
220 let mut file_count = 0;
221 const MAX_FILES: usize = 16;
222 const MAX_FILE_SIZE: usize = 10 * 1024 * 1024; let allowed_extensions = [
225 "pdf", "doc", "docx", "txt", "rtf", "odt",
227 "xls", "xlsx", "csv", "ods",
229 "ppt", "pptx", "odp",
231 "js", "ts", "jsx", "tsx", "py", "java", "cpp", "c", "cs",
233 "html", "css", "scss", "json", "xml", "yaml", "yml", "md",
234 "go", "rs", "php", "rb", "swift", "kt", "scala", "sql",
235 "sh", "bat", "ps1", "dockerfile", "env", "toml", "ini", "cfg"
236 ];
237
238 while let Some(field) = multipart.next_field().await.map_err(|e| {
239 error!("Error reading multipart field: {}", e);
240 StatusCode::BAD_REQUEST
241 })? {
242 if file_count >= MAX_FILES {
243 return Err(StatusCode::BAD_REQUEST);
244 }
245
246 let file_name = field.file_name().unwrap_or("unknown_filename").to_string();
247
248 let file_extension = std::path::Path::new(&file_name)
250 .extension()
251 .and_then(|ext| ext.to_str())
252 .unwrap_or("")
253 .to_lowercase();
254
255 if !allowed_extensions.contains(&file_extension.as_str()) {
256 error!("File type not allowed: {}", file_extension);
257 continue; }
259
260 let data = field.bytes().await.map_err(|e| {
262 error!("Error reading file {}: {}", file_name, e);
263 StatusCode::BAD_REQUEST
264 })?;
265
266 if data.len() > MAX_FILE_SIZE {
268 error!("File {} exceeds size limit of {} bytes", file_name, MAX_FILE_SIZE);
269 return Err(StatusCode::PAYLOAD_TOO_LARGE);
270 }
271
272 let mime_type = mime_guess::from_path(&file_name)
274 .first()
275 .map(|m| m.to_string());
276
277 match local_files.upload_file(query.parent_id, &file_name, &data, mime_type.as_deref()) {
279 Ok(file) => {
280 info!("Uploaded file: {} ({} bytes)", file.path, data.len());
281 file_count += 1;
282 }
283 Err(e) => {
284 error!("Failed to upload file {}: {}", file_name, e);
285 return Err(StatusCode::INTERNAL_SERVER_ERROR);
286 }
287 }
288 }
289
290 Ok(Json(serde_json::json!({
291 "message": format!("Successfully uploaded {} file(s)", file_count),
292 "count": file_count
293 })))
294}
295
296pub async fn delete_file(
298 State(state): State<UnifiedAppState>,
299 Query(query): Query<DeleteQuery>,
300) -> Result<impl IntoResponse, StatusCode> {
301 let local_files = &state.shared_state.database_pool.local_files;
302
303 let file_id = if let Some(id) = query.id {
305 id
306 } else if let Some(path) = &query.path {
307 match local_files.get_file_by_path(path) {
308 Ok(file) => file.id,
309 Err(e) => {
310 error!("File not found at path {}: {}", path, e);
311 return Err(StatusCode::NOT_FOUND);
312 }
313 }
314 } else {
315 return Err(StatusCode::BAD_REQUEST);
316 };
317
318 match local_files.delete_file(file_id) {
319 Ok(()) => {
320 info!("Deleted file/folder with id {}", file_id);
321 Ok(Json(serde_json::json!({
322 "message": "File/directory deleted successfully"
323 })))
324 }
325 Err(e) => {
326 error!("Failed to delete file {}: {}", file_id, e);
327 Err(StatusCode::INTERNAL_SERVER_ERROR)
328 }
329 }
330}
331
332pub async fn delete_file_by_id(
334 State(state): State<UnifiedAppState>,
335 Path(id): Path<i64>,
336) -> Result<impl IntoResponse, StatusCode> {
337 let local_files = &state.shared_state.database_pool.local_files;
338
339 match local_files.delete_file(id) {
340 Ok(()) => {
341 info!("Deleted file/folder with id {}", id);
342 Ok(Json(serde_json::json!({
343 "message": "File/directory deleted successfully"
344 })))
345 }
346 Err(e) => {
347 error!("Failed to delete file {}: {}", id, e);
348 Err(StatusCode::INTERNAL_SERVER_ERROR)
349 }
350 }
351}
352
353pub async fn sync_files(
355 State(state): State<UnifiedAppState>,
356) -> Result<impl IntoResponse, StatusCode> {
357 let local_files = &state.shared_state.database_pool.local_files;
358
359 match local_files.sync_from_filesystem() {
360 Ok(count) => {
361 info!("Synced {} files from filesystem", count);
362 Ok(Json(serde_json::json!({
363 "message": format!("Synced {} files from filesystem", count),
364 "count": count
365 })))
366 }
367 Err(e) => {
368 error!("Failed to sync files: {}", e);
369 Err(StatusCode::INTERNAL_SERVER_ERROR)
370 }
371 }
372}
373
374pub async fn resync_files(
376 State(state): State<UnifiedAppState>,
377) -> Result<impl IntoResponse, StatusCode> {
378 let local_files = &state.shared_state.database_pool.local_files;
379
380 match local_files.clear_all() {
382 Ok(cleared) => {
383 info!("Cleared {} entries from local_files", cleared);
384 }
385 Err(e) => {
386 error!("Failed to clear local_files: {}", e);
387 return Err(StatusCode::INTERNAL_SERVER_ERROR);
388 }
389 }
390
391 match local_files.sync_from_filesystem() {
393 Ok(count) => {
394 info!("Resynced {} files from filesystem", count);
395 Ok(Json(serde_json::json!({
396 "message": format!("Cleared and resynced {} files from filesystem", count),
397 "count": count
398 })))
399 }
400 Err(e) => {
401 error!("Failed to sync files: {}", e);
402 Err(StatusCode::INTERNAL_SERVER_ERROR)
403 }
404 }
405}