I did some mining off-camera
This commit is contained in:
parent
0ec4d86221
commit
a253f91884
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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(_) => (
|
||||||
|
|
Loading…
Reference in a new issue