1
0
Fork 0

I did some mining off-camera

This commit is contained in:
Honbra 2024-04-19 10:05:43 +02:00
parent 0ec4d86221
commit a253f91884
Signed by: honbra
GPG key ID: B61CC9ADABE2D952
6 changed files with 123 additions and 28 deletions

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM file_key WHERE id = $1 AND file_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": []
},
"hash": "bdb8d776b5cbf82b35b2d24a3fdb4ca049d36d8370861ba4be25372e542a0ba1"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM file WHERE id = $1 RETURNING hash",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "hash",
"type_info": "Bytea"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "ed6ee326516d37d078ce80b39d769747a88683fab15feb466c848c2f2ba65c50"
}

View file

@ -12,5 +12,5 @@ CREATE TABLE IF NOT EXISTS file (
CREATE TABLE IF NOT EXISTS file_key ( CREATE TABLE IF NOT EXISTS file_key (
id UUID PRIMARY KEY, id UUID PRIMARY KEY,
file_id UUID REFERENCES file (id) NOT NULL file_id UUID REFERENCES file (id) ON DELETE CASCADE NOT NULL
); );

View file

@ -3,11 +3,13 @@ use std::path::PathBuf;
use axum::{ use axum::{
body::Body, body::Body,
extract::{Path, State}, extract::{Path, State},
Json, routing::{delete, get, post},
Json, Router,
}; };
use axum_extra::{routing::Resource, TypedHeader}; use axum_extra::TypedHeader;
use futures_util::TryStreamExt; use futures_util::TryStreamExt;
use headers::ContentType; use headers::ContentType;
use http::StatusCode;
use mime::Mime; use mime::Mime;
use serde::Serialize; use serde::Serialize;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@ -20,10 +22,13 @@ use uuid::Uuid;
use crate::{app::SharedState, error::AppError}; use crate::{app::SharedState, error::AppError};
pub fn resource() -> Resource<SharedState> { pub fn resource() -> Router<SharedState> {
Resource::named("files") Router::new()
.create(upload_file) .route("/files", post(upload_file))
.show(get_file_info) .route("/files/:file_id", get(get_file_info))
.route("/files/:file_id", delete(delete_file))
.route("/files/:file_id/keys/", post(create_file_key))
.route("/files/:file_id/keys/:key_id", delete(delete_file_key))
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -34,7 +39,7 @@ struct File {
keys: Vec<Ulid>, keys: Vec<Ulid>,
} }
#[instrument(skip(db, body))] #[instrument(skip_all)]
async fn upload_file( async fn upload_file(
State(SharedState { db, config }): State<SharedState>, State(SharedState { db, config }): State<SharedState>,
TypedHeader(content_type): TypedHeader<ContentType>, TypedHeader(content_type): TypedHeader<ContentType>,
@ -150,6 +155,64 @@ async fn get_file_info(
mime: r.mime, mime: r.mime,
keys: keys.into_iter().map(|r| r.id.into()).collect(), keys: keys.into_iter().map(|r| r.id.into()).collect(),
})), })),
None => Err(AppError::FileNotFoundId(id)), None => Err(AppError::FileNotFound(id)),
}
}
#[instrument(skip_all)]
async fn delete_file(
State(SharedState { db, config }): State<SharedState>,
Path(file_id): Path<Ulid>,
) -> Result<StatusCode, AppError> {
let file_hash = query!(
"DELETE FROM file WHERE id = $1 RETURNING hash",
Uuid::from(file_id)
)
.fetch_optional(&db)
.await?
.ok_or(AppError::FileNotFound(file_id))?
.hash;
let file_path = config.file_store_dir.join(hex::encode(file_hash));
if let Err(err) = fs::remove_file(file_path).await {
error!(err = field::display(err), "failed to remove file");
}
Ok(StatusCode::NO_CONTENT)
}
async fn create_file_key(
State(SharedState { db, .. }): State<SharedState>,
Path(file_id): Path<Ulid>,
) -> Result<(StatusCode, Json<Ulid>), AppError> {
let key_id = Ulid::new();
match query!(
"INSERT INTO file_key (id, file_id) VALUES ($1, $2)",
Uuid::from(key_id),
Uuid::from(file_id),
)
.execute(&db)
.await?
.rows_affected()
{
1 => Ok((StatusCode::CREATED, Json(key_id))),
rows => Err(AppError::ImpossibleAffectedRows(rows)),
}
}
async fn delete_file_key(
State(SharedState { db, .. }): State<SharedState>,
Path((file_id, key_id)): Path<(Ulid, Ulid)>,
) -> Result<StatusCode, AppError> {
match query!(
"DELETE FROM file_key WHERE id = $1 AND file_id = $2",
Uuid::from(key_id),
Uuid::from(file_id),
)
.execute(&db)
.await?
.rows_affected()
{
1 => Ok(StatusCode::NO_CONTENT),
0 => Err(AppError::FileKeyNotFound(key_id)),
rows => Err(AppError::ImpossibleAffectedRows(rows)),
} }
} }

View file

@ -41,17 +41,15 @@ async fn download_file(
Path(key): Path<Ulid>, Path(key): Path<Ulid>,
request: Request<Body>, request: Request<Body>,
) -> Result<Response<UnsyncBoxBody<Bytes, BoxError>>, AppError> { ) -> Result<Response<UnsyncBoxBody<Bytes, BoxError>>, AppError> {
match query!( let file = query!(
"SELECT hash, mime FROM file_key JOIN file ON file_id = file.id WHERE file_key.id = $1", "SELECT hash, mime FROM file_key JOIN file ON file_id = file.id WHERE file_key.id = $1",
Uuid::from(key), Uuid::from(key),
) )
.fetch_optional(&db) .fetch_optional(&db)
.await? .await?
.map(|r| (r.hash, r.mime)) .ok_or(AppError::FileKeyNotFound(key))?;
{ let mime: Option<Mime> = file.mime.parse().ok();
Some((hash, mime)) => { let path = config.file_store_dir.join(hex::encode(file.hash));
let mime: Option<Mime> = mime.parse().ok();
let path = config.file_store_dir.join(hex::encode(hash));
let mut sf = match mime { let mut sf = match mime {
Some(mime) => ServeFile::new_with_mime(path, &mime), Some(mime) => ServeFile::new_with_mime(path, &mime),
None => ServeFile::new(path), None => ServeFile::new(path),
@ -61,6 +59,3 @@ async fn download_file(
Err(err) => Err(AppError::Io(err)), Err(err) => Err(AppError::Io(err)),
} }
} }
None => Err(AppError::FileKeyNotFound(key)),
}
}

View file

@ -14,14 +14,14 @@ pub enum AppError {
#[error("link not found ({0})")] #[error("link not found ({0})")]
LinkNotFoundSlug(String), LinkNotFoundSlug(String),
#[error("file not found ({0})")] #[error("file not found ({0})")]
FileNotFoundId(Ulid), FileNotFound(Ulid),
#[error("file key not found ({0})")] #[error("file key not found ({0})")]
FileKeyNotFound(Ulid), FileKeyNotFound(Ulid),
#[error("file is missing ({0})")] #[error("file is missing ({0})")]
FileMissing(PathBuf), FileMissing(PathBuf),
#[error("database returned an impossible number of affected rows ({0})")] #[error("database returned an impossible number of affected rows ({0})")]
ImpossibleAffectedRows(u64), ImpossibleAffectedRows(u64),
#[error("database error")] #[error("database error: {0}")]
Database(#[from] sqlx::Error), Database(#[from] sqlx::Error),
#[error(transparent)] #[error(transparent)]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
@ -37,7 +37,7 @@ impl IntoResponse for AppError {
Self::LinkNotFoundId(_) | Self::LinkNotFoundSlug(_) => { Self::LinkNotFoundId(_) | Self::LinkNotFoundSlug(_) => {
(StatusCode::NOT_FOUND, "Link not found") (StatusCode::NOT_FOUND, "Link not found")
} }
Self::FileNotFoundId(_) => (StatusCode::NOT_FOUND, "File not found"), Self::FileNotFound(_) => (StatusCode::NOT_FOUND, "File not found"),
Self::FileKeyNotFound(_) => (StatusCode::NOT_FOUND, "File key not found"), Self::FileKeyNotFound(_) => (StatusCode::NOT_FOUND, "File key not found"),
Self::FileMissing(_) => (StatusCode::INTERNAL_SERVER_ERROR, "File is missing"), Self::FileMissing(_) => (StatusCode::INTERNAL_SERVER_ERROR, "File is missing"),
Self::ImpossibleAffectedRows(_) => ( Self::ImpossibleAffectedRows(_) => (