offline_intelligence/api/
all_files_api.rs1use 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#[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
98pub 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
118pub 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
134pub 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
156pub 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
177pub 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
197pub 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
223pub 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 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 let relative_path = field
247 .headers()
248 .get("webkitrelativepath")
249 .and_then(|h| h.to_str().ok())
250 .map(|s| {
251 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 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, 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 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
312pub 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
358pub 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
379pub 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}