feat(backend): add opendal storage for file payload and postgres record for metadata, add CORS layer
This commit is contained in:
@@ -1,40 +1,69 @@
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, State},
|
||||
extract::{Multipart, Path, State},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
error::LoftError,
|
||||
model::{FileRecord, FileRepository, FileToCreate},
|
||||
model::{FileRecord, FileRepository},
|
||||
};
|
||||
|
||||
pub fn routes_file(file_repository: FileRepository) -> Router {
|
||||
Router::new()
|
||||
.route("/files", get(list_files).post(upload_file))
|
||||
.route("/files/{id}", get(download_file).delete(delete_file))
|
||||
.route("/files/{id}", get(get_file).delete(delete_file))
|
||||
.route("/files/{id}/download", get(download_file))
|
||||
.with_state(file_repository)
|
||||
}
|
||||
|
||||
async fn upload_file(
|
||||
State(file_repository): State<FileRepository>,
|
||||
Json(file_to_create): Json<FileToCreate>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<FileRecord>, LoftError> {
|
||||
info!("handler: upload_file");
|
||||
|
||||
let file = file_repository.upload_file(file_to_create).await?;
|
||||
Ok(Json(file))
|
||||
let mut file_name = None;
|
||||
let mut file_type: Option<String> = None;
|
||||
let mut bytes = None;
|
||||
|
||||
while let Some(field) = multipart.next_field().await.unwrap() {
|
||||
match field.name().unwrap() {
|
||||
// "file_type" => file_type = Some(field.text().await.unwrap().parse().unwrap()),
|
||||
"file" => {
|
||||
file_name = field.file_name().map(|s| s.to_string());
|
||||
bytes = Some(field.bytes().await.unwrap().to_vec())
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(bytes), Some(file_name)) = (bytes, file_name) {
|
||||
let file = file_repository.upload_file(bytes, file_name).await?;
|
||||
return Ok(Json(file));
|
||||
}
|
||||
|
||||
Err(LoftError::UndefinedErrorType)
|
||||
}
|
||||
|
||||
async fn download_file(
|
||||
#[axum::debug_handler]
|
||||
async fn get_file(
|
||||
State(file_repository): State<FileRepository>,
|
||||
Path(file_id): Path<u64>,
|
||||
) -> Result<Json<FileRecord>, LoftError> {
|
||||
info!("handler: download_file");
|
||||
let record = file_repository.get_file(file_id as i64).await?;
|
||||
Ok(Json(record))
|
||||
}
|
||||
|
||||
let file = file_repository.download_file(file_id as i64).await?;
|
||||
Ok(Json(file))
|
||||
#[axum::debug_handler]
|
||||
async fn download_file(
|
||||
State(file_repository): State<FileRepository>,
|
||||
Path(file_id): Path<u64>,
|
||||
) -> Result<impl IntoResponse, LoftError> {
|
||||
let bytes = file_repository.download_file(file_id as i64).await?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
async fn delete_file(
|
||||
@@ -60,8 +89,12 @@ async fn list_files(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use axum::{Router, middleware};
|
||||
use axum_test::TestServer;
|
||||
use axum_test::{
|
||||
TestServer,
|
||||
multipart::{MultipartForm, Part},
|
||||
};
|
||||
use serde_json::json;
|
||||
use sqlx::PgPool;
|
||||
use tower_cookies::CookieManagerLayer;
|
||||
|
||||
use crate::{
|
||||
@@ -90,6 +123,13 @@ mod tests {
|
||||
TestServer::new(app)
|
||||
}
|
||||
|
||||
async fn truncate(pool: &PgPool) {
|
||||
sqlx::query!("TRUNCATE TABLE file_records")
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_requires_auth() {
|
||||
let server = test_server().await;
|
||||
@@ -107,23 +147,28 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn test_requires_auth_post() {
|
||||
let file_repository = FileRepository::new().await.unwrap();
|
||||
truncate(&file_repository.pool).await;
|
||||
|
||||
let server = test_server().await;
|
||||
server
|
||||
.post("/api/files")
|
||||
.json(&json!({"name": "a", "file_type": "Document"}))
|
||||
.multipart(MultipartForm::new().add_part(
|
||||
"file",
|
||||
Part::bytes(b"fake_bytes".to_vec()).file_name("a.jpg"),
|
||||
))
|
||||
.await
|
||||
.assert_status_unauthorized();
|
||||
truncate(&file_repository.pool).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn test_list_files_empty() {
|
||||
let file_repository = FileRepository::new().await.unwrap();
|
||||
sqlx::query!("TRUNCATE TABLE file_records")
|
||||
.execute(&file_repository.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
truncate(&file_repository.pool).await;
|
||||
|
||||
let server = test_server().await;
|
||||
server
|
||||
@@ -138,47 +183,56 @@ mod tests {
|
||||
#[serial_test::serial]
|
||||
async fn test_upload_and_list_files() {
|
||||
let file_repository = FileRepository::new().await.unwrap();
|
||||
sqlx::query!("TRUNCATE TABLE file_records")
|
||||
.execute(&file_repository.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
truncate(&file_repository.pool).await;
|
||||
|
||||
let server = test_server().await;
|
||||
let res = server
|
||||
.post("/api/files")
|
||||
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
||||
.json(&json!({"name": "a", "file_type": "Document", "size": 10}))
|
||||
.multipart(MultipartForm::new().add_part(
|
||||
"file",
|
||||
Part::bytes(b"fake_bytes".to_vec()).file_name("a.jpg"),
|
||||
))
|
||||
.await;
|
||||
|
||||
res.assert_status_ok();
|
||||
let file = res.json::<serde_json::Value>();
|
||||
assert_eq!(file["name"], "a");
|
||||
assert_eq!(file["file_type"], "Document");
|
||||
assert_eq!(file["size"], 10);
|
||||
assert_eq!(file["name"], "a.jpg");
|
||||
|
||||
let list = server
|
||||
.get("/api/files")
|
||||
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
||||
.await
|
||||
.json::<serde_json::Value>();
|
||||
|
||||
assert_eq!(list.as_array().unwrap().len(), 1);
|
||||
truncate(&file_repository.pool).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn test_download_file() {
|
||||
let file_repository = FileRepository::new().await.unwrap();
|
||||
truncate(&file_repository.pool).await;
|
||||
|
||||
let server = test_server().await;
|
||||
let post_res = server
|
||||
.post("/api/files")
|
||||
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
||||
.json(&json!({"name": "b", "file_type": "Document", "size": 10}))
|
||||
.multipart(MultipartForm::new().add_part(
|
||||
"file",
|
||||
Part::bytes(b"fake_bytes".to_vec()).file_name("a.jpg"),
|
||||
))
|
||||
.await;
|
||||
let id = post_res.json::<serde_json::Value>()["id"].as_i64().unwrap();
|
||||
let res = server
|
||||
.get(&format!("/api/files/{id}"))
|
||||
.get(&format!("/api/files/{id}/download"))
|
||||
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
||||
.await;
|
||||
|
||||
res.assert_status_ok();
|
||||
assert_eq!(res.json::<serde_json::Value>()["name"], "b");
|
||||
assert_eq!(res.as_bytes(), b"fake_bytes".as_ref());
|
||||
truncate(&file_repository.pool).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -186,7 +240,7 @@ mod tests {
|
||||
async fn test_download_file_not_found() {
|
||||
let server = test_server().await;
|
||||
server
|
||||
.get("/api/files/99")
|
||||
.get("/api/files/99/download")
|
||||
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
||||
.await
|
||||
.assert_status_not_found();
|
||||
@@ -195,11 +249,17 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn test_delete_file() {
|
||||
let file_repository = FileRepository::new().await.unwrap();
|
||||
truncate(&file_repository.pool).await;
|
||||
|
||||
let server = test_server().await;
|
||||
let post_res = server
|
||||
.post("/api/files")
|
||||
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
||||
.json(&json!({"name": "c", "file_type": "Document", "size": 10}))
|
||||
.multipart(MultipartForm::new().add_part(
|
||||
"file",
|
||||
Part::bytes(b"fake_bytes".to_vec()).file_name("a.jpg"),
|
||||
))
|
||||
.await;
|
||||
let id = post_res.json::<serde_json::Value>()["id"].as_i64().unwrap();
|
||||
|
||||
@@ -214,6 +274,8 @@ mod tests {
|
||||
.add_header(axum::http::header::COOKIE, AUTH_COOKIE)
|
||||
.await
|
||||
.assert_status_not_found();
|
||||
|
||||
truncate(&file_repository.pool).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user