Skip to main content

icb_server/
upload.rs

1//! Handle project upload via multipart/form-data.
2//!
3//! Receives a ZIP file and optional query parameter `languages` (comma‑separated).
4//! Extracts the archive into a temporary directory and builds a graph
5//! using the specified languages, or auto‑detection if none are given.
6//! System headers are always excluded.
7
8use actix_multipart::Multipart;
9use actix_web::{web, HttpResponse};
10use futures_util::StreamExt;
11use icb_graph::graph::CodePropertyGraph;
12use std::io::Cursor;
13use std::sync::Mutex;
14use tempfile::tempdir;
15use zip::ZipArchive;
16
17use crate::graph_builder;
18
19#[derive(serde::Deserialize)]
20pub struct UploadQuery {
21    pub languages: Option<String>,
22}
23
24/// Handles ZIP upload.
25pub async fn handle_upload(
26    data: web::Data<Mutex<CodePropertyGraph>>,
27    query: web::Query<UploadQuery>,
28    mut payload: Multipart,
29) -> HttpResponse {
30    let tmp = match tempdir() {
31        Ok(dir) => dir,
32        Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
33    };
34
35    let mut zip_bytes: Vec<u8> = Vec::new();
36
37    while let Some(item) = payload.next().await {
38        let mut field = match item {
39            Ok(f) => f,
40            Err(e) => return HttpResponse::BadRequest().body(e.to_string()),
41        };
42        while let Some(chunk) = field.next().await {
43            let chunk = match chunk {
44                Ok(c) => c,
45                Err(_) => break,
46            };
47            zip_bytes.extend_from_slice(&chunk);
48        }
49    }
50
51    if zip_bytes.is_empty() {
52        return HttpResponse::BadRequest().body("No ZIP file received");
53    }
54
55    let reader = Cursor::new(zip_bytes);
56    let mut archive = match ZipArchive::new(reader) {
57        Ok(a) => a,
58        Err(e) => return HttpResponse::BadRequest().body(format!("Invalid ZIP: {}", e)),
59    };
60
61    for i in 0..archive.len() {
62        let mut file = match archive.by_index(i) {
63            Ok(f) => f,
64            Err(_) => continue,
65        };
66
67        let name = file.name().to_string();
68        let path = tmp.path().join(&name);
69
70        if file.is_dir() {
71            std::fs::create_dir_all(&path).ok();
72        } else {
73            if let Some(parent) = path.parent() {
74                std::fs::create_dir_all(parent).ok();
75            }
76            let mut out = match std::fs::File::create(&path) {
77                Ok(f) => f,
78                Err(_) => continue,
79            };
80            std::io::copy(&mut file, &mut out).ok();
81        }
82    }
83
84    let graph_result = if let Some(langs) = &query.languages {
85        let languages: Vec<String> = langs.split(',').map(|s| s.trim().to_string()).collect();
86        graph_builder::build_or_load_graph_multi(
87            tmp.path(),
88            &languages,
89            None, // graph cache file
90            None, // incremental fact cache dir
91            true,
92        )
93    } else {
94        graph_builder::build_or_load_graph(tmp.path(), "auto", None, None, true)
95    };
96
97    drop(tmp);
98
99    match graph_result {
100        Ok(new_graph) => {
101            let nodes = new_graph.graph.node_count();
102            let edges = new_graph.graph.edge_count();
103            if let Ok(mut locked) = data.lock() {
104                *locked = new_graph;
105            }
106            HttpResponse::Ok().json(serde_json::json!({
107                "status": "ok",
108                "nodes": nodes,
109                "edges": edges,
110            }))
111        }
112        Err(e) => HttpResponse::BadRequest().body(e.to_string()),
113    }
114}