use std::{ffi::OsStr, io::Write, path::Path};
use actix_multipart::Multipart;
use actix_web::{web, HttpResponse};
use atomic_lib::{
commit::CommitResponse, hierarchy::check_write, urls, utils::now, Resource, Storelike, Value,
};
use futures::{StreamExt, TryStreamExt};
use serde::Deserialize;
use crate::{appstate::AppState, errors::AtomicServerResult, helpers::get_client_agent};
#[derive(Deserialize, Debug)]
pub struct UploadQuery {
parent: String,
}
#[tracing::instrument(skip(appstate, req, body))]
pub async fn upload_handler(
mut body: Multipart,
appstate: web::Data<AppState>,
query: web::Query<UploadQuery>,
req: actix_web::HttpRequest,
) -> AtomicServerResult<HttpResponse> {
let store = &appstate.store;
let parent = store.get_resource(&query.parent)?;
let subject = format!(
"{}{}",
store.get_server_url(),
req.head()
.uri
.path_and_query()
.ok_or("Path must be given")?
);
let agent = get_client_agent(req.headers(), &appstate, subject)?;
check_write(store, &parent, &agent)?;
let mut created_resources: Vec<Resource> = Vec::new();
let mut commit_responses: Vec<CommitResponse> = Vec::new();
while let Ok(Some(mut field)) = body.try_next().await {
let content_type = field.content_disposition().clone();
let filename = content_type.get_filename().ok_or("Filename is missing")?;
std::fs::create_dir_all(&appstate.config.uploads_path)?;
let file_id = format!(
"{}-{}",
now(),
sanitize_filename::sanitize(filename)
.replace(' ', "-")
);
let mut file_path = appstate.config.uploads_path.clone();
file_path.push(&file_id);
let mut file = std::fs::File::create(file_path)?;
while let Some(chunk) = field.next().await {
let data = chunk.map_err(|e| format!("Error while reading multipart data. {}", e))?;
file.write_all(&data)?;
}
let byte_count: i64 = file
.metadata()?
.len()
.try_into()
.map_err(|_e| "Too large")?;
let subject_path = format!("files/{}", urlencoding::encode(&file_id));
let new_subject = format!("{}/{}", store.get_server_url(), subject_path);
let download_url = format!("{}/download/{}", store.get_server_url(), subject_path);
let mut resource = atomic_lib::Resource::new_instance(urls::FILE, store)?;
resource
.set_subject(new_subject)
.set_string(urls::PARENT.into(), &query.parent, store)?
.set_string(urls::INTERNAL_ID.into(), &file_id, store)?
.set(urls::FILESIZE.into(), Value::Integer(byte_count), store)?
.set_string(
urls::MIMETYPE.into(),
&guess_mime_for_filename(filename),
store,
)?
.set_string(urls::FILENAME.into(), filename, store)?
.set_string(urls::DOWNLOAD_URL.into(), &download_url, store)?;
commit_responses.push(resource.save(store)?);
created_resources.push(resource);
}
let created_file_subjects = created_resources
.iter()
.map(|r| r.get_subject().to_string())
.collect::<Vec<String>>();
let mut parent = store.get_resource(&query.parent)?;
for created in created_file_subjects {
parent.push(urls::ATTACHMENTS, created.into(), false)?;
}
commit_responses.push(parent.save(store)?);
let mut builder = HttpResponse::Ok();
Ok(builder.body(atomic_lib::serialize::resources_to_json_ad(
&created_resources,
)?))
}
fn guess_mime_for_filename(filename: &str) -> String {
if let Some(ext) = get_extension_from_filename(filename) {
actix_files::file_extension_to_mime(ext).to_string()
} else {
"application/octet-stream".to_string()
}
}
fn get_extension_from_filename(filename: &str) -> Option<&str> {
Path::new(filename).extension().and_then(OsStr::to_str)
}