270 lines
8.4 KiB
Rust
270 lines
8.4 KiB
Rust
use leptos::leptos_dom::error;
|
|
use leptos::logging::debug_log;
|
|
use leptos::prelude::*;
|
|
use server_fn::codec::{MultipartData, MultipartFormData};
|
|
|
|
#[cfg(feature = "ssr")]
|
|
mod imports {
|
|
pub use super::super::{MAIN_TABLE_NAME, S3_BUCKET_NAME};
|
|
pub use crate::pages::api::tokens::validate_and_get_token_info;
|
|
pub use crate::utils::server::create_aws_config;
|
|
pub use aws_sdk_dynamodb::{
|
|
error::ProvideErrorMetadata as _,
|
|
types::{AttributeValue, ReturnValue, Select},
|
|
};
|
|
pub use aws_sdk_s3::error::ProvideErrorMetadata as _;
|
|
}
|
|
|
|
#[server(input = MultipartFormData)]
|
|
pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
|
|
use imports::*;
|
|
|
|
let mut multipart = data.into_inner().unwrap();
|
|
|
|
let mut file_name = String::new();
|
|
let mut file_bytes = Vec::<u8>::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();
|
|
|
|
match field_name {
|
|
"access_token" => access_token = field.text().await?,
|
|
"form_id" => form_id = field.text().await?,
|
|
_ => {
|
|
// 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.
|
|
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 input_name.is_empty() {
|
|
return Err(ServerFnError::new("Missing input file"));
|
|
}
|
|
if file_bytes.is_empty() {
|
|
return Err(ServerFnError::new("Missing file contents"));
|
|
}
|
|
|
|
let user_claims = validate_and_get_token_info(access_token).await?;
|
|
let subject = user_claims.subject;
|
|
|
|
let key = format!("{form_id}/{subject}/{input_name}/{file_name}");
|
|
|
|
debug_log!("Adding file to S3: {:?}", key);
|
|
|
|
let s3_client = aws_sdk_s3::Client::new(&create_aws_config().await);
|
|
|
|
s3_client
|
|
.put_object()
|
|
.bucket(S3_BUCKET_NAME)
|
|
.key(&key)
|
|
.body(file_bytes.into())
|
|
.send()
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(|e| {
|
|
let msg = format!(
|
|
"Couldn't put file to S3: {}",
|
|
e.message().unwrap_or("Unknown error occurred")
|
|
);
|
|
error!("{}", msg);
|
|
ServerFnError::new(msg)
|
|
})?;
|
|
|
|
// 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}#{file_name}")),
|
|
)
|
|
.item(
|
|
"file_name".to_string(),
|
|
AttributeValue::S(file_name.clone()),
|
|
)
|
|
.item("file_key".to_string(), AttributeValue::S(key.clone()))
|
|
.send()
|
|
.await
|
|
.map_err(|err| {
|
|
let msg = format!(
|
|
"Couldn't put file to Dynamo, file rolled back: {}",
|
|
err.message().unwrap_or("Unknown error occurred")
|
|
);
|
|
error!("{}", msg);
|
|
ServerFnError::new(msg)
|
|
});
|
|
|
|
// If Dynamo fails, we want to handle the error by rolling back the file. Then, we return a failure.
|
|
if let Err(err) = dynamo_result {
|
|
s3_client
|
|
.delete_object()
|
|
.bucket("leptos-scholarships")
|
|
.key(&key)
|
|
.send()
|
|
.await
|
|
.map_err(|err| {
|
|
let msg = format!(
|
|
"Failed to rollback Dynamo entry: {}",
|
|
err.message().unwrap_or("Unknown error occurred")
|
|
);
|
|
error!("{}", msg);
|
|
ServerFnError::new(msg)
|
|
})?;
|
|
|
|
return Err(err);
|
|
}
|
|
|
|
Ok(file_name)
|
|
}
|
|
|
|
#[server]
|
|
pub async fn delete_file(
|
|
access_token: String,
|
|
form_id: String,
|
|
input_name: String,
|
|
file_name: String,
|
|
) -> Result<String, ServerFnError> {
|
|
use imports::*;
|
|
|
|
let user_claims = validate_and_get_token_info(access_token).await?;
|
|
let subject = user_claims.subject;
|
|
|
|
let entry_hk = format!("STUDENT#{subject}");
|
|
let entry_sk = format!("FILE#{form_id}#{input_name}#{file_name}");
|
|
|
|
let key = format!("{form_id}/{subject}/{input_name}/{file_name}");
|
|
|
|
let config = create_aws_config().await;
|
|
let s3_client = aws_sdk_s3::Client::new(&config);
|
|
let dynamo_client = aws_sdk_dynamodb::Client::new(&config);
|
|
|
|
let previous_values = dynamo_client
|
|
.delete_item()
|
|
.table_name(MAIN_TABLE_NAME)
|
|
.key("HK".to_string(), AttributeValue::S(entry_hk))
|
|
.key("SK".to_string(), AttributeValue::S(entry_sk))
|
|
.return_values(ReturnValue::AllOld)
|
|
.send()
|
|
.await?
|
|
.attributes
|
|
.unwrap_or_default();
|
|
|
|
let s3_result = s3_client
|
|
.delete_object()
|
|
.bucket(S3_BUCKET_NAME)
|
|
.key(key)
|
|
.send()
|
|
.await
|
|
.map_err(|err| {
|
|
let msg = format!(
|
|
"Failed to put file into S3: {}",
|
|
err.message().unwrap_or("Unknown error occurred")
|
|
);
|
|
error!("{}", msg);
|
|
ServerFnError::new(msg)
|
|
});
|
|
|
|
if let Err(err) = s3_result {
|
|
// Add the key back to Dynamo and return an error.
|
|
dynamo_client
|
|
.put_item()
|
|
.table_name(MAIN_TABLE_NAME)
|
|
.set_item(Some(previous_values))
|
|
.send()
|
|
.await
|
|
.map_err(|err| {
|
|
let msg = format!(
|
|
"Failed to rollback Dynamo entry: {}",
|
|
err.message().unwrap_or("Unknown error occurred")
|
|
);
|
|
error!("{}", msg);
|
|
ServerFnError::new(msg)
|
|
})?;
|
|
|
|
return Err(err);
|
|
}
|
|
|
|
Ok(file_name)
|
|
}
|
|
|
|
#[server]
|
|
pub async fn list_files(
|
|
access_token: String,
|
|
form_id: String,
|
|
input_name: String,
|
|
) -> Result<Vec<String>, ServerFnError> {
|
|
use imports::*;
|
|
|
|
let user_claims = validate_and_get_token_info(access_token).await?;
|
|
let subject = user_claims.subject;
|
|
|
|
let entry_hk = format!("STUDENT#{subject}");
|
|
let entry_sk = format!("FILE#{form_id}#{input_name}");
|
|
|
|
debug_log!("Getting file list: HK = {}, SK = {}", entry_hk, entry_sk);
|
|
|
|
let dynamo_client = aws_sdk_dynamodb::Client::new(&create_aws_config().await);
|
|
|
|
dynamo_client
|
|
.query()
|
|
.table_name(MAIN_TABLE_NAME)
|
|
.key_condition_expression("HK = :hk AND begins_with(SK, :sk)")
|
|
.expression_attribute_values(":hk".to_string(), AttributeValue::S(entry_hk))
|
|
.expression_attribute_values(":sk".to_string(), AttributeValue::S(entry_sk))
|
|
.send()
|
|
.await
|
|
.map(|output| {
|
|
let Some(items) = output.items else {
|
|
return vec![];
|
|
};
|
|
|
|
items
|
|
.into_iter()
|
|
.filter_map(|item| {
|
|
item.get("file_name")
|
|
.map(|v| v.as_s().ok().cloned())
|
|
.flatten()
|
|
})
|
|
.collect::<Vec<String>>()
|
|
})
|
|
.map_err(|err| {
|
|
let msg = format!(
|
|
"Failed to get files: {}",
|
|
err.message().unwrap_or("Unknown error occurred")
|
|
);
|
|
error!("{}", msg);
|
|
ServerFnError::new(msg)
|
|
})
|
|
}
|