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
+}