I did some mining off-camera
This commit is contained in:
		
							parent
							
								
									0ec4d86221
								
							
						
					
					
						commit
						a253f91884
					
				
					 6 changed files with 123 additions and 28 deletions
				
			
		
							
								
								
									
										15
									
								
								.sqlx/query-bdb8d776b5cbf82b35b2d24a3fdb4ca049d36d8370861ba4be25372e542a0ba1.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.sqlx/query-bdb8d776b5cbf82b35b2d24a3fdb4ca049d36d8370861ba4be25372e542a0ba1.json
									
										
									
										generated
									
									
									
										Normal 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" | ||||||
|  | } | ||||||
							
								
								
									
										22
									
								
								.sqlx/query-ed6ee326516d37d078ce80b39d769747a88683fab15feb466c848c2f2ba65c50.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.sqlx/query-ed6ee326516d37d078ce80b39d769747a88683fab15feb466c848c2f2ba65c50.json
									
										
									
										generated
									
									
									
										Normal 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" | ||||||
|  | } | ||||||
|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue