1
0
Fork 0

Add file uploads

Good luck getting them, though
This commit is contained in:
Honbra 2024-02-03 14:46:18 +01:00
parent 620963368a
commit 645c7f2d98
Signed by: honbra
GPG key ID: B61CC9ADABE2D952
6 changed files with 127 additions and 2 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
/target
/.direnv
/config.toml
/temp
/files

17
Cargo.lock generated
View file

@ -807,11 +807,15 @@ dependencies = [
"axum",
"eyre",
"figment",
"futures-util",
"hex",
"http",
"serde",
"sha2",
"sqlx",
"thiserror",
"tokio",
"tokio-util",
"tower-http",
"tracing",
"tracing-subscriber",
@ -1684,6 +1688,19 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "toml"
version = "0.8.8"

View file

@ -7,11 +7,15 @@ edition = "2021"
axum = { version = "0.6.20", default-features = false, features = ["http1", "json", "macros", "matched-path", "tokio", "tower-log", "tracing"] }
eyre = "0.6.8"
figment = { version = "0.10.11", features = ["env", "toml"] }
futures-util = { version = "0.3.30", default-features = false }
hex = "0.4.3"
http = "0.2.9"
serde = { version = "1.0.189", features = ["derive"] }
sha2 = "0.10.8"
sqlx = { version = "0.7.3", features = ["runtime-tokio", "postgres", "uuid"] }
thiserror = "1.0.51"
tokio = { version = "1.33.0", features = ["rt-multi-thread", "macros"] }
tokio = { version = "1.33.0", features = ["rt-multi-thread", "macros", "fs", "io-std"] }
tokio-util = { version = "0.7.10", features = ["io"] }
tower-http = { version = "0.4.4", features = ["trace"] }
tracing = "0.1.37"
tracing-subscriber = "0.3.17"

92
src/app/api/files.rs Normal file
View file

@ -0,0 +1,92 @@
use std::path::PathBuf;
use axum::{
extract::{BodyStream, State},
routing::post,
Json, Router,
};
use futures_util::TryStreamExt;
use serde::Serialize;
use sha2::{Digest, Sha256};
use sqlx::PgPool;
use tokio::{
fs::{self, File},
io,
};
use tokio_util::io::StreamReader;
use tracing::{error, field, info, instrument};
use ulid::Ulid;
use crate::error::AppError;
pub fn router(db: PgPool) -> Router {
Router::new().route("/", post(upload_file)).with_state(db)
}
#[derive(Debug, Serialize)]
struct UploadedFile {
id: Ulid,
hash: String,
}
#[instrument(skip(body))]
async fn upload_file(
State(_db): State<PgPool>,
body: BodyStream,
) -> Result<Json<UploadedFile>, AppError> {
let id_temp = Ulid::new();
let file_path_temp = PathBuf::from("temp").join(id_temp.to_string());
let mut hasher = Sha256::new();
{
let mut file_temp = File::create(&file_path_temp).await?;
let better_body = body
.inspect_ok(|b| hasher.update(b))
.map_err(|err| io::Error::new(io::ErrorKind::Other, err));
let mut reader = StreamReader::new(better_body);
if let Err(err) = io::copy(&mut reader, &mut file_temp).await {
error!(
err = field::display(&err),
file_path = field::debug(&file_path_temp),
"failed to copy file, removing",
);
drop(file_temp);
if let Err(err) = fs::remove_file(file_path_temp).await {
error!(
err = field::display(err),
"failed to remove failed upload file",
);
}
return Err(err.into());
}
}
let hash = hasher.finalize();
let hash_hex = hex::encode(hash);
let file_path_hash = PathBuf::from("files").join(&hash_hex);
if fs::try_exists(&file_path_hash).await? {
info!(hash = hash_hex, "file already exists");
if let Err(err) = fs::remove_file(&file_path_temp).await {
error!(err = field::display(&err), "failed to remove temp file");
}
} else if let Err(err) = fs::rename(&file_path_temp, &file_path_hash).await {
error!(err = field::display(&err), "failed to move finished file");
if let Err(err) = fs::remove_file(&file_path_temp).await {
error!(
err = field::display(&err),
"failed to remove file after failed rename",
);
}
return Err(err.into());
}
Ok(Json(UploadedFile {
id: id_temp,
hash: hash_hex,
}))
}

View file

@ -1,8 +1,11 @@
mod files;
mod links;
use axum::Router;
use sqlx::PgPool;
pub fn router(db: PgPool) -> Router {
Router::new().nest("/links", links::router(db))
Router::new()
.nest("/files", files::router(db.clone()))
.nest("/links", links::router(db))
}

View file

@ -14,6 +14,8 @@ pub enum AppError {
#[error("database error")]
Database(#[from] sqlx::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Other(#[from] eyre::Report),
}
@ -33,6 +35,11 @@ impl IntoResponse for AppError {
"A database error has occured",
)
.into_response(),
Self::Io(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"An I/O error has occured",
)
.into_response(),
Self::Other(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("An error has occured:\n{err:?}"),