use actix_multipart::Multipart;
use actix_web::Result as AwResult;
use actix_web::{HttpResponse, post, web};
use futures_util::StreamExt;
use log::{error, info};
use maud::html;
use crate::{ConnectionStore, db};
#[post("/upload/csv")]
pub async fn upload_csv(
mut payload: Multipart,
store: web::Data<ConnectionStore>,
) -> AwResult<HttpResponse> {
let Some(ch) = store.active_client() else {
let markup = html! {
div class="bg-dark-red white pa3 br2" {
p class="f5 fw6 ma0 mb2" { "No active connection" }
p class="f6 ma0" {
"Please configure a database connection in "
a href="/connections" class="white underline" { "Connections" }
" first."
}
}
};
return Ok(HttpResponse::ServiceUnavailable()
.content_type("text/html")
.body(markup.into_string()));
};
let mut file_name: Option<String> = None;
let mut file_data: Vec<u8> = Vec::new();
while let Some(field) = payload.next().await {
let mut field = match field {
Ok(f) => f,
Err(e) => {
error!("Error reading multipart field: {}", e);
let markup = html! {
div class="bg-dark-red white pa3 br2" {
p class="f5 fw6 ma0 mb2" { "Upload Error" }
p class="f6 ma0" { "Error reading file: " (e) }
}
};
return Ok(HttpResponse::BadRequest()
.content_type("text/html")
.body(markup.into_string()));
}
};
let content_disposition = field.content_disposition();
let field_name = content_disposition
.and_then(|cd| cd.get_name())
.unwrap_or("");
if field_name == "file" {
if let Some(fname) = content_disposition.and_then(|cd| cd.get_filename()) {
file_name = Some(
fname
.trim_end_matches(".csv")
.replace(['-', ' ', '.'], "_")
.to_string(),
);
}
while let Some(chunk) = field.next().await {
let chunk = match chunk {
Ok(c) => c,
Err(e) => {
error!("Error reading file chunk: {}", e);
let markup = html! {
div class="bg-dark-red white pa3 br2" {
p class="f5 fw6 ma0 mb2" { "Upload Error" }
p class="f6 ma0" { "Error reading file data: " (e) }
}
};
return Ok(HttpResponse::BadRequest()
.content_type("text/html")
.body(markup.into_string()));
}
};
file_data.extend_from_slice(&chunk);
}
}
}
if file_data.is_empty() {
let markup = html! {
div class="bg-orange white pa3 br2" {
p class="f5 fw6 ma0 mb2" { "No File" }
p class="f6 ma0" { "Please select a CSV file to upload." }
}
};
return Ok(HttpResponse::BadRequest()
.content_type("text/html")
.body(markup.into_string()));
}
let table_name = file_name.unwrap_or_else(|| "uploaded_table".to_string());
info!(
"Processing CSV upload: {} ({} bytes)",
table_name,
file_data.len()
);
match db::create_table_from_csv(ch, &table_name, &file_data).await {
Ok(_) => {
info!("Successfully created table: {}", table_name);
let markup = html! {
div class="bg-dark-green white pa3 br2" {
p class="f5 fw6 ma0 mb2" { "Success!" }
p class="f6 ma0 mb2" {
"Table '" (table_name) "' has been created successfully."
}
p class="f6 ma0" {
"You can now query it using SQL."
}
}
};
Ok(HttpResponse::Ok()
.content_type("text/html")
.body(markup.into_string()))
}
Err(e) => {
error!("Failed to create table from CSV: {}", e);
let markup = html! {
div class="bg-dark-red white pa3 br2" {
p class="f5 fw6 ma0 mb2" { "Database Error" }
p class="f6 ma0" { "Failed to create table: " (e) }
}
};
Ok(HttpResponse::InternalServerError()
.content_type("text/html")
.body(markup.into_string()))
}
}
}