Skip to main content

offline_intelligence/api/
all_files_api.rs

1//! All Files API endpoints - unlimited storage for all file formats
2//!
3//! Provides user-managed file storage for context inclusion via @filename.
4
5use axum::{
6    extract::{Multipart, Path, Query, State},
7    http::StatusCode,
8    response::IntoResponse,
9    Json,
10};
11use serde::{Deserialize, Serialize};
12use tracing::{error, info, warn};
13
14use crate::shared_state::UnifiedAppState;
15use crate::memory_db::{AllFile, AllFileTree};
16
17/// Response structure for all file entries
18#[derive(Debug, Serialize)]
19pub struct AllFileEntryResponse {
20    pub id: i64,
21    pub name: String,
22    pub path: String,
23    #[serde(rename = "isDirectory")]
24    pub is_directory: bool,
25    pub size: i64,
26    pub modified: String,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub last_accessed: Option<String>,
29    pub access_count: i64,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub children: Option<Vec<AllFileEntryResponse>>,
32}
33
34impl From<AllFile> for AllFileEntryResponse {
35    fn from(f: AllFile) -> Self {
36        Self {
37            id: f.id,
38            name: f.name,
39            path: f.path,
40            is_directory: f.is_directory,
41            size: f.size_bytes,
42            modified: f.modified_at.to_rfc3339(),
43            last_accessed: f.last_accessed.map(|dt| dt.to_rfc3339()),
44            access_count: f.access_count,
45            children: None,
46        }
47    }
48}
49
50impl From<AllFileTree> for AllFileEntryResponse {
51    fn from(t: AllFileTree) -> Self {
52        Self {
53            id: t.file.id,
54            name: t.file.name.clone(),
55            path: t.file.path.clone(),
56            is_directory: t.file.is_directory,
57            size: t.file.size_bytes,
58            modified: t.file.modified_at.to_rfc3339(),
59            last_accessed: t.file.last_accessed.map(|dt| dt.to_rfc3339()),
60            access_count: t.file.access_count,
61            children: t.children.map(|c| c.into_iter().map(AllFileEntryResponse::from).collect()),
62        }
63    }
64}
65
66#[derive(Debug, Deserialize)]
67pub struct CreateFolderRequest {
68    pub name: String,
69    #[serde(default)]
70    pub parent_id: Option<i64>,
71}
72
73#[derive(Debug, Deserialize)]
74pub struct DeleteQuery {
75    #[serde(default)]
76    pub path: Option<String>,
77    #[serde(default)]
78    pub id: Option<i64>,
79}
80
81#[derive(Debug, Deserialize)]
82pub struct SearchQuery {
83    pub q: String,
84}
85
86#[derive(Debug, Deserialize)]
87pub struct UploadQuery {
88    #[serde(default)]
89    pub parent_id: Option<i64>,
90}
91
92#[derive(Debug, Deserialize)]
93pub struct UploadWithPathRequest {
94    #[serde(default)]
95    pub parent_path: Option<String>,
96}
97
98/// GET /all-files - Get all files as a nested tree
99pub async fn get_all_files(
100    State(state): State<UnifiedAppState>,
101) -> Result<impl IntoResponse, StatusCode> {
102    let all_files = &state.shared_state.database_pool.all_files;
103
104    match all_files.get_file_tree() {
105        Ok(tree) => {
106            let response: Vec<AllFileEntryResponse> = tree.into_iter()
107                .map(AllFileEntryResponse::from)
108                .collect();
109            Ok(Json(response))
110        }
111        Err(e) => {
112            error!("Failed to get all files: {}", e);
113            Err(StatusCode::INTERNAL_SERVER_ERROR)
114        }
115    }
116}
117
118/// GET /all-files/:id - Get single file metadata
119pub async fn get_all_file_by_id(
120    State(state): State<UnifiedAppState>,
121    Path(id): Path<i64>,
122) -> Result<impl IntoResponse, StatusCode> {
123    let all_files = &state.shared_state.database_pool.all_files;
124
125    match all_files.get_file(id) {
126        Ok(file) => Ok(Json(AllFileEntryResponse::from(file))),
127        Err(e) => {
128            error!("Failed to get all file {}: {}", id, e);
129            Err(StatusCode::NOT_FOUND)
130        }
131    }
132}
133
134/// GET /all-files/:id/content - Get file content for LLM processing
135pub async fn get_all_file_content(
136    State(state): State<UnifiedAppState>,
137    Path(id): Path<i64>,
138) -> Result<impl IntoResponse, StatusCode> {
139    let all_files = &state.shared_state.database_pool.all_files;
140
141    match all_files.get_file_content_string(id) {
142        Ok(content) => {
143            let _ = all_files.record_access(id);
144            Ok(Json(serde_json::json!({
145                "id": id,
146                "content": content
147            })))
148        }
149        Err(e) => {
150            error!("Failed to get all file content {}: {}", id, e);
151            Err(StatusCode::NOT_FOUND)
152        }
153    }
154}
155
156/// GET /all-files/search?q=... - Search files by name
157pub async fn search_all_files(
158    State(state): State<UnifiedAppState>,
159    Query(query): Query<SearchQuery>,
160) -> Result<impl IntoResponse, StatusCode> {
161    let all_files = &state.shared_state.database_pool.all_files;
162
163    match all_files.search_files(&query.q) {
164        Ok(files) => {
165            let response: Vec<AllFileEntryResponse> = files.into_iter()
166                .map(AllFileEntryResponse::from)
167                .collect();
168            Ok(Json(response))
169        }
170        Err(e) => {
171            error!("Failed to search all files: {}", e);
172            Ok(Json(Vec::<AllFileEntryResponse>::new()))
173        }
174    }
175}
176
177/// GET /all-files/all - Get flat list of all files (for @filename autocomplete)
178pub async fn get_all_files_flat(
179    State(state): State<UnifiedAppState>,
180) -> Result<impl IntoResponse, StatusCode> {
181    let all_files = &state.shared_state.database_pool.all_files;
182
183    match all_files.get_all_files() {
184        Ok(files) => {
185            let response: Vec<AllFileEntryResponse> = files.into_iter()
186                .map(AllFileEntryResponse::from)
187                .collect();
188            Ok(Json(response))
189        }
190        Err(e) => {
191            error!("Failed to get all files: {}", e);
192            Ok(Json(Vec::<AllFileEntryResponse>::new()))
193        }
194    }
195}
196
197/// POST /all-files/folder - Create a new folder
198pub async fn create_all_files_folder(
199    State(state): State<UnifiedAppState>,
200    Json(request): Json<CreateFolderRequest>,
201) -> Result<impl IntoResponse, StatusCode> {
202    let all_files = &state.shared_state.database_pool.all_files;
203
204    if request.name.trim().is_empty() {
205        return Err(StatusCode::BAD_REQUEST);
206    }
207
208    match all_files.create_folder(request.parent_id, &request.name) {
209        Ok(folder) => {
210            info!("Created all_files folder: {}", folder.path);
211            Ok(Json(serde_json::json!({
212                "message": "Folder created successfully",
213                "folder": AllFileEntryResponse::from(folder)
214            })))
215        }
216        Err(e) => {
217            error!("Failed to create all_files folder: {}", e);
218            Err(StatusCode::INTERNAL_SERVER_ERROR)
219        }
220    }
221}
222
223/// POST /all-files/upload - Upload files (supports folder structure via webkitdirectory)
224pub async fn upload_all_file(
225    State(state): State<UnifiedAppState>,
226    Query(query): Query<UploadQuery>,
227    mut multipart: Multipart,
228) -> Result<impl IntoResponse, StatusCode> {
229    let all_files = &state.shared_state.database_pool.all_files;
230    let parent_id = query.parent_id;
231    
232    let mut files_to_upload: Vec<(Option<String>, String, Vec<u8>)> = Vec::new();
233
234    // Collect multipart fields
235    let mut field_count = 0;
236    let mut has_error = false;
237    loop {
238        match multipart.next_field().await {
239            Ok(Some(field)) => {
240                field_count += 1;
241                let filename = field.file_name()
242                    .map(|s| s.to_string())
243                    .unwrap_or_else(|| format!("file{}", files_to_upload.len()));
244
245                // Get relative path from webkitdirectory uploads BEFORE reading bytes
246                let relative_path = field
247                    .headers()
248                    .get("webkitrelativepath")
249                    .and_then(|h| h.to_str().ok())
250                    .map(|s| {
251                        // Remove the filename from the path to get just the directory
252                        let path = std::path::Path::new(s);
253                        let parent = path.parent()
254                            .map(|p| p.to_string_lossy().to_string())
255                            .unwrap_or_default();
256                        // Normalize path separators to forward slashes for consistency
257                        parent.replace('\\', "/")
258                    });
259
260                match field.bytes().await {
261                    Ok(data) => {
262                        let size = data.len();
263                        info!("Processing file: {} ({} bytes), relative_path: {:?}", filename, size, relative_path);
264                        files_to_upload.push((relative_path, filename, data.to_vec()));
265                    }
266                    Err(e) => {
267                        error!("Failed to read file data for {}: {}", filename, e);
268                        has_error = true;
269                    }
270                }
271            }
272            Ok(None) => break, // No more fields
273            Err(e) => {
274                error!("Error reading multipart field: {}", e);
275                has_error = true;
276                break;
277            }
278        }
279    }
280
281    info!("Total fields received: {}, files to upload: {}", field_count, files_to_upload.len());
282
283    if files_to_upload.is_empty() {
284        if has_error {
285            error!("Upload failed due to errors reading fields");
286            return Err(StatusCode::BAD_REQUEST);
287        }
288        warn!("No files to upload - received {} fields but 0 parseable files", field_count);
289        return Err(StatusCode::BAD_REQUEST);
290    }
291
292    // Use the structure upload function to handle folders
293    info!("Uploading {} files with structure, parent_id: {:?}", files_to_upload.len(), parent_id);
294    match all_files.upload_files_with_structure(files_to_upload, parent_id) {
295        Ok(uploaded) => {
296            let response: Vec<AllFileEntryResponse> = uploaded.into_iter()
297                .map(AllFileEntryResponse::from)
298                .collect();
299            info!("Uploaded {} files with structure", response.len());
300            Ok(Json(serde_json::json!({
301                "message": format!("Uploaded {} file(s) successfully", response.len()),
302                "files": response
303            })))
304        }
305        Err(e) => {
306            error!("Failed to upload files: {}", e);
307            Err(StatusCode::INTERNAL_SERVER_ERROR)
308        }
309    }
310}
311
312/// POST /all-files/upload-structure - Upload files with folder structure
313pub async fn upload_all_files_structure(
314    State(state): State<UnifiedAppState>,
315    mut multipart: Multipart,
316) -> Result<impl IntoResponse, StatusCode> {
317    let all_files = &state.shared_state.database_pool.all_files;
318    let mut files_to_upload: Vec<(Option<String>, String, Vec<u8>)> = Vec::new();
319
320    while let Some(field_result) = multipart.next_field().await.ok().flatten() {
321        let filename = field_result.file_name()
322            .map(|s| s.to_string())
323            .unwrap_or_else(|| "file".to_string());
324
325        let relative_path = field_result
326            .headers()
327            .get("relative-path")
328            .and_then(|h| h.to_str().ok())
329            .map(|s| s.to_string());
330
331        let data = field_result.bytes().await.map_err(|_| StatusCode::BAD_REQUEST)?;
332
333        files_to_upload.push((relative_path, filename, data.to_vec()));
334    }
335
336    if files_to_upload.is_empty() {
337        return Err(StatusCode::BAD_REQUEST);
338    }
339
340    match all_files.upload_files_with_structure(files_to_upload, None) {
341        Ok(uploaded) => {
342            let response: Vec<AllFileEntryResponse> = uploaded.into_iter()
343                .map(AllFileEntryResponse::from)
344                .collect();
345            info!("Uploaded {} files with structure", response.len());
346            Ok(Json(serde_json::json!({
347                "message": format!("Uploaded {} file(s) successfully", response.len()),
348                "files": response
349            })))
350        }
351        Err(e) => {
352            error!("Failed to upload files with structure: {}", e);
353            Err(StatusCode::INTERNAL_SERVER_ERROR)
354        }
355    }
356}
357
358/// DELETE /all-files/:id - Delete file or folder by ID
359pub async fn delete_all_file_by_id(
360    State(state): State<UnifiedAppState>,
361    Path(id): Path<i64>,
362) -> Result<impl IntoResponse, StatusCode> {
363    let all_files = &state.shared_state.database_pool.all_files;
364
365    match all_files.delete_file(id) {
366        Ok(_) => {
367            info!("Deleted all file id: {}", id);
368            Ok(Json(serde_json::json!({
369                "message": "File deleted successfully"
370            })))
371        }
372        Err(e) => {
373            error!("Failed to delete all file {}: {}", id, e);
374            Err(StatusCode::INTERNAL_SERVER_ERROR)
375        }
376    }
377}
378
379/// DELETE /all-files?path=... or ?id=... - Delete file by path or ID
380pub async fn delete_all_file(
381    State(state): State<UnifiedAppState>,
382    Query(query): Query<DeleteQuery>,
383) -> Result<impl IntoResponse, StatusCode> {
384    let all_files = &state.shared_state.database_pool.all_files;
385
386    if let Some(id) = query.id {
387        match all_files.delete_file(id) {
388            Ok(_) => {
389                info!("Deleted all file id: {}", id);
390                Ok(Json(serde_json::json!({
391                    "message": "File deleted successfully"
392                })))
393            }
394            Err(e) => {
395                error!("Failed to delete all file by id {}: {}", id, e);
396                Err(StatusCode::INTERNAL_SERVER_ERROR)
397            }
398        }
399    } else if let Some(path) = query.path {
400        match all_files.get_file_by_path(&path) {
401            Ok(file) => {
402                match all_files.delete_file(file.id) {
403                    Ok(_) => {
404                        info!("Deleted all file: {}", path);
405                        Ok(Json(serde_json::json!({
406                            "message": "File deleted successfully"
407                        })))
408                    }
409                    Err(e) => {
410                        error!("Failed to delete all file {}: {}", path, e);
411                        Err(StatusCode::INTERNAL_SERVER_ERROR)
412                    }
413                }
414            }
415            Err(e) => {
416                error!("All file not found: {}: {}", path, e);
417                Err(StatusCode::NOT_FOUND)
418            }
419        }
420    } else {
421        Err(StatusCode::BAD_REQUEST)
422    }
423}