diff --git a/forms/multipart-s3/README.md b/forms/multipart-s3/README.md index f0a169cc..99d19e88 100644 --- a/forms/multipart-s3/README.md +++ b/forms/multipart-s3/README.md @@ -14,6 +14,8 @@ cd forms/multipart-s3 1. edit `.env` key `AWS_SECRET_ACCESS_KEY` = your_key_secret 1. edit `.env` key `AWS_S3_BUCKET_NAME` = your_bucket_name +The AWS SDK automatically reads these environment variables to configure the S3 client. + ```sh cargo run ``` @@ -25,6 +27,8 @@ Or, start the upload using [HTTPie]: ```sh http --form POST :8080/ file@Cargo.toml http --form POST :8080/ file@Cargo.toml file@README.md meta='{"namespace":"foo"}' + +http GET :8080/file/ ``` Or, using cURL: @@ -32,6 +36,8 @@ Or, using cURL: ```sh curl -X POST http://localhost:8080/ -F 'file=@Cargo.toml' curl -X POST http://localhost:8080/ -F 'file=@Cargo.toml' -F 'file=@README.md' -F 'meta={"namespace":"foo"}' + +curl http://localhost:8080/file/ ``` [httpie]: https://httpie.org diff --git a/forms/multipart-s3/src/client.rs b/forms/multipart-s3/src/client.rs index 42de2cf0..9c563b08 100644 --- a/forms/multipart-s3/src/client.rs +++ b/forms/multipart-s3/src/client.rs @@ -1,9 +1,9 @@ use std::env; -use actix_web::Error; +use actix_web::{error, web::Bytes, Error}; use aws_config::SdkConfig as AwsConfig; use aws_sdk_s3::{types::ByteStream, Client as S3Client}; -use futures_util::{stream, StreamExt as _}; +use futures_util::{stream, Stream, StreamExt as _, TryStreamExt as _}; use tokio::{fs, io::AsyncReadExt as _}; use crate::{TempFile, UploadedFile}; @@ -32,6 +32,30 @@ impl Client { ) } + pub async fn fetch_file( + &self, + key: &str, + ) -> Option<(u64, impl Stream>)> { + let object = self + .s3 + .get_object() + .bucket(&self.bucket_name) + .key(key) + .send() + .await + .ok()?; + + Some(( + object + .content_length() + .try_into() + .expect("file has invalid size"), + object + .body + .map_err(|err| error::ErrorInternalServerError(err)), + )) + } + pub async fn upload_files( &self, temp_files: Vec, @@ -82,24 +106,19 @@ impl Client { .body(ByteStream::from(contents)) .send() .await - .expect("Failed to put test object"); + .expect("Failed to put object"); self.url(key) } - pub async fn delete_files(&self, keys: Vec<&str>) { - for key in keys { - self.delete_object(key).await; - } - } - - async fn delete_object(&self, key: &str) { + /// Attempts to deletes object from S3. Returns true if successful. + pub async fn delete_file(&self, key: &str) -> bool { self.s3 .delete_object() .bucket(&self.bucket_name) .key(key) .send() .await - .expect("Couldn't delete object"); + .is_ok() } } diff --git a/forms/multipart-s3/src/index.html b/forms/multipart-s3/src/index.html index ad28b343..1bdb47df 100644 --- a/forms/multipart-s3/src/index.html +++ b/forms/multipart-s3/src/index.html @@ -1,35 +1,38 @@ + + - Upload Test - -
- - - -
- - +S3 Upload Test + +
+ + + +
+ + diff --git a/forms/multipart-s3/src/main.rs b/forms/multipart-s3/src/main.rs index cc6aa3d7..5634cf31 100644 --- a/forms/multipart-s3/src/main.rs +++ b/forms/multipart-s3/src/main.rs @@ -1,9 +1,12 @@ use std::fs; use actix_multipart::Multipart; +use actix_web::body::SizedStream; +use actix_web::{delete, error}; use actix_web::{ get, middleware::Logger, post, web, App, Error, HttpResponse, HttpServer, Responder, }; +use actix_web_lab::extract::Path; use actix_web_lab::respond::Html; use aws_config::meta::region::RegionProviderChain; use dotenv::dotenv; @@ -58,6 +61,31 @@ async fn upload_to_s3( }))) } +#[get("/file/{s3_key}*")] +async fn fetch_from_s3( + s3_client: web::Data, + Path((s3_key,)): Path<(String,)>, +) -> Result { + let (file_size, file_stream) = s3_client + .fetch_file(&s3_key) + .await + .ok_or_else(|| error::ErrorNotFound("file with specified key not found"))?; + + Ok(HttpResponse::Ok().body(SizedStream::new(file_size, file_stream))) +} + +#[delete("/file/{s3_key}*")] +async fn delete_from_s3( + s3_client: web::Data, + Path((s3_key,)): Path<(String,)>, +) -> Result { + if s3_client.delete_file(&s3_key).await { + Ok(HttpResponse::NoContent().finish()) + } else { + Err(error::ErrorNotFound("file with specified key not found")) + } +} + #[get("/")] async fn index() -> impl Responder { Html(include_str!("./index.html").to_owned()) @@ -87,6 +115,8 @@ async fn main() -> std::io::Result<()> { App::new() .service(index) .service(upload_to_s3) + .service(fetch_from_s3) + .service(delete_from_s3) .wrap(Logger::default()) .app_data(web::Data::new(s3_client.clone())) })