Compare commits
	
		
			2 commits
		
	
	
		
			ed1bd84a26
			...
			645c7f2d98
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 645c7f2d98 | |||
| 620963368a | 
					 7 changed files with 128 additions and 2 deletions
				
			
		
							
								
								
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -1,3 +1,5 @@
 | 
				
			||||||
/target
 | 
					/target
 | 
				
			||||||
/.direnv
 | 
					/.direnv
 | 
				
			||||||
/config.toml
 | 
					/config.toml
 | 
				
			||||||
 | 
					/temp
 | 
				
			||||||
 | 
					/files
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										17
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										17
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							| 
						 | 
					@ -807,11 +807,15 @@ dependencies = [
 | 
				
			||||||
 "axum",
 | 
					 "axum",
 | 
				
			||||||
 "eyre",
 | 
					 "eyre",
 | 
				
			||||||
 "figment",
 | 
					 "figment",
 | 
				
			||||||
 | 
					 "futures-util",
 | 
				
			||||||
 | 
					 "hex",
 | 
				
			||||||
 "http",
 | 
					 "http",
 | 
				
			||||||
 "serde",
 | 
					 "serde",
 | 
				
			||||||
 | 
					 "sha2",
 | 
				
			||||||
 "sqlx",
 | 
					 "sqlx",
 | 
				
			||||||
 "thiserror",
 | 
					 "thiserror",
 | 
				
			||||||
 "tokio",
 | 
					 "tokio",
 | 
				
			||||||
 | 
					 "tokio-util",
 | 
				
			||||||
 "tower-http",
 | 
					 "tower-http",
 | 
				
			||||||
 "tracing",
 | 
					 "tracing",
 | 
				
			||||||
 "tracing-subscriber",
 | 
					 "tracing-subscriber",
 | 
				
			||||||
| 
						 | 
					@ -1684,6 +1688,19 @@ dependencies = [
 | 
				
			||||||
 "tokio",
 | 
					 "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]]
 | 
					[[package]]
 | 
				
			||||||
name = "toml"
 | 
					name = "toml"
 | 
				
			||||||
version = "0.8.8"
 | 
					version = "0.8.8"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,11 +7,15 @@ edition = "2021"
 | 
				
			||||||
axum = { version = "0.6.20", default-features = false, features = ["http1", "json", "macros", "matched-path", "tokio", "tower-log", "tracing"] }
 | 
					axum = { version = "0.6.20", default-features = false, features = ["http1", "json", "macros", "matched-path", "tokio", "tower-log", "tracing"] }
 | 
				
			||||||
eyre = "0.6.8"
 | 
					eyre = "0.6.8"
 | 
				
			||||||
figment = { version = "0.10.11", features = ["env", "toml"] }
 | 
					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"
 | 
					http = "0.2.9"
 | 
				
			||||||
serde = { version = "1.0.189", features = ["derive"] }
 | 
					serde = { version = "1.0.189", features = ["derive"] }
 | 
				
			||||||
 | 
					sha2 = "0.10.8"
 | 
				
			||||||
sqlx = { version = "0.7.3", features = ["runtime-tokio", "postgres", "uuid"] }
 | 
					sqlx = { version = "0.7.3", features = ["runtime-tokio", "postgres", "uuid"] }
 | 
				
			||||||
thiserror = "1.0.51"
 | 
					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"] }
 | 
					tower-http = { version = "0.4.4", features = ["trace"] }
 | 
				
			||||||
tracing = "0.1.37"
 | 
					tracing = "0.1.37"
 | 
				
			||||||
tracing-subscriber = "0.3.17"
 | 
					tracing-subscriber = "0.3.17"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,6 +37,7 @@
 | 
				
			||||||
          buildInputs = [
 | 
					          buildInputs = [
 | 
				
			||||||
            cargo
 | 
					            cargo
 | 
				
			||||||
            rustc
 | 
					            rustc
 | 
				
			||||||
 | 
					            clippy
 | 
				
			||||||
            fenix.packages.${system}.latest.rustfmt
 | 
					            fenix.packages.${system}.latest.rustfmt
 | 
				
			||||||
            rust-analyzer
 | 
					            rust-analyzer
 | 
				
			||||||
            pkg-config
 | 
					            pkg-config
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										92
									
								
								src/app/api/files.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/app/api/files.rs
									
										
									
									
									
										Normal 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,
 | 
				
			||||||
 | 
					    }))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,11 @@
 | 
				
			||||||
 | 
					mod files;
 | 
				
			||||||
mod links;
 | 
					mod links;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use axum::Router;
 | 
					use axum::Router;
 | 
				
			||||||
use sqlx::PgPool;
 | 
					use sqlx::PgPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn router(db: PgPool) -> Router {
 | 
					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))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,8 @@ pub enum AppError {
 | 
				
			||||||
    #[error("database error")]
 | 
					    #[error("database error")]
 | 
				
			||||||
    Database(#[from] sqlx::Error),
 | 
					    Database(#[from] sqlx::Error),
 | 
				
			||||||
    #[error(transparent)]
 | 
					    #[error(transparent)]
 | 
				
			||||||
 | 
					    Io(#[from] std::io::Error),
 | 
				
			||||||
 | 
					    #[error(transparent)]
 | 
				
			||||||
    Other(#[from] eyre::Report),
 | 
					    Other(#[from] eyre::Report),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,6 +35,11 @@ impl IntoResponse for AppError {
 | 
				
			||||||
                "A database error has occured",
 | 
					                "A database error has occured",
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
                .into_response(),
 | 
					                .into_response(),
 | 
				
			||||||
 | 
					            Self::Io(_) => (
 | 
				
			||||||
 | 
					                StatusCode::INTERNAL_SERVER_ERROR,
 | 
				
			||||||
 | 
					                "An I/O error has occured",
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					                .into_response(),
 | 
				
			||||||
            Self::Other(err) => (
 | 
					            Self::Other(err) => (
 | 
				
			||||||
                StatusCode::INTERNAL_SERVER_ERROR,
 | 
					                StatusCode::INTERNAL_SERVER_ERROR,
 | 
				
			||||||
                format!("An error has occured:\n{err:?}"),
 | 
					                format!("An error has occured:\n{err:?}"),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue