diff --git a/src/components/file_drop.rs b/src/components/file_drop.rs index ae07e4f..ac09fdc 100644 --- a/src/components/file_drop.rs +++ b/src/components/file_drop.rs @@ -1,3 +1,4 @@ +use crate::pages::api::files::upload_file; use leptos::html::{Input, Label}; use leptos::logging::debug_log; use leptos::prelude::*; @@ -5,13 +6,9 @@ use leptos::web_sys::{File, FormData}; use leptos_icons::Icon; use leptos_oidc::AuthSignal; use leptos_use::{UseDropZoneOptions, UseDropZoneReturn, use_drop_zone_with_options}; -use crate::pages::api::files::upload_file; #[component] -pub fn FileDrop( - #[prop(into)] name: String, - #[prop(into)] form_id: String, -) -> impl IntoView { +pub fn FileDrop(#[prop(into)] name: String, #[prop(into)] form_id: String) -> impl IntoView { let zone_ref = NodeRef::
diff --git a/src/pages/api/files.rs b/src/pages/api/files.rs index b36249f..3aa6d88 100644 --- a/src/pages/api/files.rs +++ b/src/pages/api/files.rs @@ -1,13 +1,13 @@ -use leptos::prelude::*; -use server_fn::codec::{MultipartFormData, MultipartData}; -use leptos::logging::debug_log; +use leptos::logging::debug_log; +use leptos::prelude::*; +use server_fn::codec::{MultipartData, MultipartFormData}; #[server(input = MultipartFormData)] -pub async fn upload_file( - data: MultipartData -) -> Result<(), ServerFnError> { +pub async fn upload_file(data: MultipartData) -> Result<(), ServerFnError> { + use super::{MAIN_TABLE_NAME, S3_BUCKET_NAME}; use crate::pages::api::tokens::validate_and_get_token_info; use crate::utils::server::create_aws_config; + use aws_sdk_dynamodb::types::AttributeValue; let mut multipart = data.into_inner().unwrap(); @@ -15,6 +15,7 @@ pub async fn upload_file( let mut file_bytes = Vec::::new(); let mut form_id = String::new(); let mut access_token = String::new(); + let mut input_name = String::new(); while let Ok(Some(field)) = multipart.next_field().await { let field_name = field.name().unwrap_or_default(); @@ -26,46 +27,104 @@ pub async fn upload_file( // NOTE: If there are multiple files put here the function will only upload the last // one in the data. // We should likely use chunk streaming in the future, for safety. - file_name = field.file_name() + input_name = field + .name() + .ok_or(ServerFnError::new("Couldn't find input name"))? + .to_owned(); + file_name = field + .file_name() .ok_or(ServerFnError::new("Couldn't find file name"))? .to_owned(); file_bytes = field.bytes().await?.to_vec(); - }, + } } } - if access_token.is_empty() { return Err(ServerFnError::new("Missing access token")); } - if form_id.is_empty() { return Err(ServerFnError::new("Missing form ID")); } - if file_name.is_empty() { return Err(ServerFnError::new("Missing file")); } + if access_token.is_empty() { + return Err(ServerFnError::new("Missing access token")); + } + if form_id.is_empty() { + return Err(ServerFnError::new("Missing form ID")); + } + if file_name.is_empty() { + return Err(ServerFnError::new("Missing file")); + } + if input_name.is_empty() { + return Err(ServerFnError::new("Missing input file")); + } let user_claims = validate_and_get_token_info(access_token).await?; let subject = user_claims.subject; - let key = format!("{form_id}/{subject}/{file_name}"); + let key = format!("{form_id}/{subject}/{input_name}/{file_name}"); debug_log!("Adding file to S3: {:?}", key); - let client = aws_sdk_s3::Client::new(&create_aws_config().await); + let s3_client = aws_sdk_s3::Client::new(&create_aws_config().await); - client + s3_client .put_object() - .bucket("leptos-scholarships") - .key(key) + .bucket(S3_BUCKET_NAME) + .key(&key) .body(file_bytes.into()) .send() .await .map(|_| ()) - .map_err(ServerFnError::from) + .map_err(|e| ServerFnError::new(format!("Couldn't put file to S3: {}", e.to_string())))?; + + // Besides uploading the file to S3, we also want to store a FILE entry in Dynamo. + // This will allow the server to quickly return a list of all files that a specific user has + // uploaded, or a list of all files submitted for a specific question with their keys for + // additional access. + let dynamo_client = aws_sdk_dynamodb::Client::new(&create_aws_config().await); + + let dynamo_result = dynamo_client + .put_item() + .table_name(MAIN_TABLE_NAME) + .item( + "HK".to_string(), + AttributeValue::S(format!("STUDENT#{subject}")), + ) + .item( + "SK".to_string(), + AttributeValue::S(format!("FILE#{form_id}#{input_name}")), + ) + .item("file_name".to_string(), AttributeValue::S(file_name)) + .item("file_key".to_string(), AttributeValue::S(key.clone())) + .send() + .await + .map(|_| ()) + .map_err(|e| ServerFnError::new(format!("Couldn't put file to Dynamo: {}", e.to_string()))); + + if let Err(_) = dynamo_result { + // Roll back file insertion to S3. Only attempted on Dynamo insertion failure. + s3_client + .delete_object() + .bucket("leptos-scholarships") + .key(&key) + .send() + .await + .map(|_| ()) + .map_err(|e| { + ServerFnError::new(format!( + "Couldn't roll back S3 operation: {}", + e.to_string() + )) + }) + } else { + dynamo_result + } } #[server] pub async fn delete_file( access_token: String, form_id: String, - file_name: String + file_name: String, ) -> Result<(), ServerFnError> { - use crate::utils::server::create_aws_config; + use super::S3_BUCKET_NAME; use crate::pages::api::tokens::validate_and_get_token_info; + use crate::utils::server::create_aws_config; let user_claims = validate_and_get_token_info(access_token).await?; let subject = user_claims.subject; @@ -76,7 +135,7 @@ pub async fn delete_file( client .delete_object() - .bucket("leptos-scholarhips") + .bucket(S3_BUCKET_NAME) .key(key) .send() .await diff --git a/src/pages/student/additional_page.rs b/src/pages/student/additional_page.rs index d9cc319..ee6bdea 100644 --- a/src/pages/student/additional_page.rs +++ b/src/pages/student/additional_page.rs @@ -1,5 +1,5 @@ -use leptos::prelude::*; use crate::components::FileDrop; +use leptos::prelude::*; #[component] pub fn AdditionalPage() -> impl IntoView { @@ -10,7 +10,7 @@ pub fn AdditionalPage() -> impl IntoView { "This is just a placeholder for now. We'll show redirect buttons to each form."
"Here's a testing file input!"
- + } -} \ No newline at end of file +}