1
0
Fork 0
ncpn/src/app/api/files.rs

156 lines
4.2 KiB
Rust
Raw Normal View History

2024-04-16 22:07:00 +02:00
use std::path::PathBuf;
2024-04-16 22:07:00 +02:00
use axum::{
body::Body,
extract::{Path, State},
Json,
};
use axum_extra::{routing::Resource, TypedHeader};
use futures_util::TryStreamExt;
2024-04-16 22:07:00 +02:00
use headers::ContentType;
use mime::Mime;
use serde::Serialize;
use sha2::{Digest, Sha256};
2024-04-16 22:07:00 +02:00
use sqlx::query;
use tokio::{fs, io};
use tokio_util::io::StreamReader;
use tracing::{error, field, info, instrument};
use ulid::Ulid;
use uuid::Uuid;
2024-04-16 22:07:00 +02:00
use crate::{app::SharedState, error::AppError};
2024-04-16 22:07:00 +02:00
pub fn resource() -> Resource<SharedState> {
Resource::named("files")
.create(upload_file)
.show(get_file_info)
}
2024-04-16 22:07:00 +02:00
#[derive(Serialize)]
struct File {
id: Ulid,
hash: String,
2024-04-16 22:07:00 +02:00
mime: String,
keys: Vec<Ulid>,
}
#[instrument(skip(db, body))]
async fn upload_file(
State(SharedState { db, config }): State<SharedState>,
2024-04-16 22:07:00 +02:00
TypedHeader(content_type): TypedHeader<ContentType>,
body: Body,
2024-04-16 22:07:00 +02:00
) -> Result<Json<File>, AppError> {
let id = Ulid::new();
let path_temp = config.file_temp_dir.join(id.to_string());
let mut hasher = Sha256::new();
{
2024-04-16 22:07:00 +02:00
let mut file_temp = fs::File::create(&path_temp).await?;
let better_body = body
.into_data_stream()
.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),
2024-04-16 22:07:00 +02:00
file_path = field::debug(&path_temp),
"failed to copy file, removing",
);
drop(file_temp);
2024-04-16 22:07:00 +02:00
if let Err(err) = fs::remove_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);
2024-04-16 22:07:00 +02:00
let path_hash = PathBuf::from("files").join(&hash_hex);
2024-04-16 22:07:00 +02:00
if fs::try_exists(&path_hash).await? {
info!(hash = hash_hex, "file already exists");
2024-04-16 22:07:00 +02:00
if let Err(err) = fs::remove_file(&path_temp).await {
error!(err = field::display(&err), "failed to remove temp file");
}
2024-04-16 22:07:00 +02:00
} else if let Err(err) = fs::rename(&path_temp, &path_hash).await {
error!(err = field::display(&err), "failed to move finished file");
2024-04-16 22:07:00 +02:00
if let Err(err) = fs::remove_file(&path_temp).await {
error!(
err = field::display(&err),
2024-02-03 14:50:53 +01:00
"failed to remove file after failed move",
);
}
return Err(err.into());
}
2024-04-16 22:07:00 +02:00
let mime = Into::<Mime>::into(content_type);
let mime_str = mime.to_string();
match query!(
"INSERT INTO file (id, hash, mime) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
Uuid::from(id),
&hash[..],
2024-04-16 22:07:00 +02:00
mime_str,
)
.execute(&db)
2024-04-16 22:07:00 +02:00
.await?
.rows_affected()
{
0 | 1 => {}
rows => return Err(AppError::ImpossibleAffectedRows(rows)),
}
let key = Ulid::new();
match query!(
"INSERT INTO file_key (id, file_id) VALUES ($1, $2)",
Uuid::from(key),
2024-04-16 22:07:00 +02:00
Uuid::from(id),
)
.execute(&db)
2024-04-16 22:07:00 +02:00
.await?
.rows_affected()
{
1 => Ok(Json(File {
id,
hash: hash_hex,
2024-04-16 22:07:00 +02:00
mime: mime_str,
keys: vec![key],
})),
rows => Err(AppError::ImpossibleAffectedRows(rows)),
}
}
2024-04-16 22:07:00 +02:00
async fn get_file_info(
State(SharedState { db, .. }): State<SharedState>,
Path(id): Path<Ulid>,
) -> Result<Json<File>, AppError> {
let (file, keys) = tokio::try_join!(
query!(
"SELECT id, hash, mime FROM file WHERE id = $1",
Uuid::from(id),
)
.fetch_optional(&db),
query!("SELECT id FROM file_key WHERE file_id = $1", Uuid::from(id)).fetch_all(&db),
)?;
match file {
Some(r) => Ok(Json(File {
id,
hash: hex::encode(r.hash),
mime: r.mime,
keys: keys.into_iter().map(|r| r.id.into()).collect(),
})),
None => Err(AppError::FileNotFoundId(id)),
}
}