diff --git a/src/auth/github.rs b/src/auth/github.rs index 33366f0..86f7d78 100644 --- a/src/auth/github.rs +++ b/src/auth/github.rs @@ -1,10 +1,10 @@ use crate::auth::AuthenticationError; use crate::database::repository::github_login_attempts; use crate::types::models::github_login_attempt::StoredLoginAttempt; -use reqwest::{Client, header::HeaderValue}; +use reqwest::{header::HeaderValue, Client}; use serde::{Deserialize, Serialize}; use serde_json::json; -use sqlx::{PgConnection, types::ipnetwork::IpNetwork}; +use sqlx::{types::ipnetwork::IpNetwork, PgConnection}; use uuid::Uuid; #[derive(Debug, Deserialize, Serialize)] @@ -16,7 +16,7 @@ pub struct GithubStartAuth { interval: i32, } -#[derive(Deserialize)] +#[derive(Deserialize, Default)] pub enum GithubDeviceFlowErrorString { #[serde(rename(deserialize = "authorization_pending"))] AuthorizationPending, @@ -34,15 +34,10 @@ pub enum GithubDeviceFlowErrorString { AccessDenied, #[serde(rename(deserialize = "device_flow_disabled"))] DeviceFlowDisabled, + #[default] Unknown, } -impl Default for GithubDeviceFlowErrorString { - fn default() -> Self { - GithubDeviceFlowErrorString::Unknown - } -} - #[derive(Deserialize)] pub struct GithubErrorResponse { error: GithubDeviceFlowErrorString, @@ -108,9 +103,7 @@ impl GithubClient { })) .send() .await - .inspect_err(|e| { - tracing::error!("Failed to start OAuth device flow with GitHub: {e}") - })?; + .inspect_err(|e| tracing::error!("Failed to start OAuth device flow with GitHub: {e}"))?; if !res.status().is_success() { tracing::error!( @@ -261,9 +254,7 @@ impl GithubClient { let body = resp .json::() .await - .inspect_err(|e| { - tracing::error!("github::get_installation: failed to parse response: {e}") - }) + .inspect_err(|e| tracing::error!("github::get_installation: failed to parse response: {e}")) .or(Err(AuthenticationError::InternalError( "Failed to parse response from GitHub".into(), )))?; diff --git a/src/config.rs b/src/config.rs index cfda4ee..2c34e47 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,9 @@ -use std::time::Duration; +use std::{sync::Arc, time::Duration}; use moka::future::Cache; -use crate::storage::{PrivateStorage, PublicStorage, StaticStorage}; use crate::{ - endpoints::mods::IndexQueryParams, - types::{ + endpoints::mods::IndexQueryParams, storage::{LocalBackend, PrivateDisk, PublicDisk}, types::{ api::{ApiResponse, PaginatedData}, models::mod_entity::Mod, }, @@ -19,9 +17,9 @@ pub struct AppData { github: GitHubClientData, webhook_url: String, index_admin_webhook_url: String, - static_storage: StaticStorage, - public_storage: PublicStorage, - private_storage: PrivateStorage, + static_storage: PublicDisk, + public_storage: PublicDisk, + private_storage: PrivateDisk, disable_downloads: bool, max_download_mb: u32, port: u16, @@ -78,9 +76,9 @@ pub async fn build_config() -> anyhow::Result { }, webhook_url, index_admin_webhook_url, - static_storage: StaticStorage::new(app_url.clone()), - public_storage: PublicStorage::new(app_url.clone()), - private_storage: PrivateStorage::new(), + static_storage: PublicDisk::new(Arc::new(LocalBackend::new("static")), format!("{app_url}/static")), + public_storage: PublicDisk::new(Arc::new(LocalBackend::new("storage/public")), format!("{app_url}/storage")), + private_storage: PrivateDisk::new(Arc::new(LocalBackend::new("storage/private"))), disable_downloads, max_download_mb, port, @@ -140,15 +138,15 @@ impl AppData { self.debug } - pub fn static_storage(&self) -> &StaticStorage { + pub fn static_storage(&self) -> &PublicDisk { &self.static_storage } - pub fn public_storage(&self) -> &PublicStorage { + pub fn public_storage(&self) -> &PublicDisk { &self.public_storage } - pub fn private_storage(&self) -> &PrivateStorage { + pub fn private_storage(&self) -> &PrivateDisk { &self.private_storage } diff --git a/src/database/repository/mods.rs b/src/database/repository/mods.rs index 9b55058..5191855 100644 --- a/src/database/repository/mods.rs +++ b/src/database/repository/mods.rs @@ -254,37 +254,6 @@ pub async fn increment_downloads(id: &str, conn: &mut PgConnection) -> Result<() Ok(()) } -#[tracing::instrument(skip_all, fields(mod_id = %the_mod.id))] -pub async fn update_with_json( - mut the_mod: Mod, - json: &ModJson, - conn: &mut PgConnection, -) -> Result { - sqlx::query!( - "UPDATE mods - SET repository = $1, - about = $2, - changelog = $3, - image = $4, - updated_at = NOW() - WHERE id = $5", - json.repository, - json.about, - json.changelog, - json.logo, - the_mod.id - ) - .execute(conn) - .await - .inspect_err(|e| tracing::error!("{:?}", e))?; - - the_mod.repository = json.repository.clone(); - the_mod.about = json.about.clone(); - the_mod.changelog = json.changelog.clone(); - - Ok(the_mod) -} - #[tracing::instrument(skip_all, fields(mod_id = %the_mod.id))] pub async fn update_with_json_moved( mut the_mod: Mod, diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 581fd26..afdc079 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -25,6 +25,8 @@ pub enum ApiError { #[error("{0}")] Database(#[from] crate::database::DatabaseError), #[error("{0}")] + Storage(#[from] crate::storage::StorageError), + #[error("{0}")] ModZip(#[from] ModZipError), #[error("Database error")] SqlxError(#[from] sqlx::Error), diff --git a/src/endpoints/mod_status_badge.rs b/src/endpoints/mod_status_badge.rs index 75f1778..1112aad 100644 --- a/src/endpoints/mod_status_badge.rs +++ b/src/endpoints/mod_status_badge.rs @@ -4,7 +4,6 @@ use actix_web::{HttpResponse, Responder, get, web}; use serde::Deserialize; use utoipa::{IntoParams, ToSchema}; -use crate::storage::StorageDisk; use urlencoding; const LABEL_COLOR: &str = "#0c0811"; diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 630b208..2e49ddc 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -4,7 +4,6 @@ use crate::database::DatabaseError; use crate::database::repository::{developers, mod_version_submissions, mod_versions, mods}; use crate::events::thread_comment::NewThreadComment; use crate::extractors::auth::Auth; -use crate::storage::StorageDisk; use crate::types::api::{ApiResponse, PaginatedData}; use crate::types::models::audit_actions::{AuditAction, AuditActionRow}; use crate::types::models::developer::Developer; @@ -863,7 +862,7 @@ pub async fn upload_attachments( for webp_bytes in &processed { let filename = data .public_storage() - .store_hashed_with_extension("submission-attachments", webp_bytes, Some("webp")) + .store_hashed("submission-attachments", webp_bytes, Some("webp")) .await?; stored_filenames.push(filename.clone()); let row = diff --git a/src/main.rs b/src/main.rs index c21a909..fdacc3f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ use crate::openapi::ApiDoc; -use crate::storage::StorageDisk; use crate::types::api; use actix_cors::Cors; use actix_web::{ diff --git a/src/storage.rs b/src/storage.rs index cd2a3c3..e7705ab 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1,112 +1,99 @@ -use std::path::{Path, PathBuf}; +use std::{path::PathBuf, pin::Pin, sync::Arc}; -#[derive(Clone, Debug)] -pub struct StaticStorage { - base_path: PathBuf, - app_url: String, +#[derive(thiserror::Error, Debug)] +pub enum StorageError { + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), } -impl StaticStorage { - pub fn new(app_url: String) -> Self { - Self { - base_path: PathBuf::from("static"), - app_url, - } - } +pub type StorageResult = Result; +type BoxFuture<'a, T> = Pin + Send + 'a>>; - pub fn asset_url(&self, relative_path: &str) -> String { - format!( - "{}/static/{}", - self.app_url.trim_end_matches('/'), - relative_path - ) - } -} - -impl StorageDisk for StaticStorage { - fn base_path(&self) -> &Path { - &self.base_path - } +pub trait StorageBackend: Send + Sync { + fn init(&self) -> BoxFuture<'_, StorageResult<()>> { + Box::pin(async { Ok(()) }) + } + fn store<'a>(&'a self, path: &'a str, data: &'a [u8]) -> BoxFuture<'a, StorageResult<()>>; + fn read<'a>(&'a self, path: &'a str) -> BoxFuture<'a, StorageResult>>; + fn exists<'a>(&'a self, path: &'a str) -> BoxFuture<'a, StorageResult>; + fn delete<'a>(&'a self, path: &'a str) -> BoxFuture<'a, StorageResult<()>>; } -#[derive(Clone, Debug)] -pub struct PublicStorage { +pub struct LocalBackend { base_path: PathBuf, - app_url: String, } -impl PublicStorage { - pub fn new(app_url: String) -> Self { - Self { - base_path: PathBuf::from("storage/public"), - app_url, +impl LocalBackend { + pub fn new(base_path: impl Into) -> LocalBackend { + LocalBackend { + base_path: base_path.into(), } } - - pub fn asset_url(&self, relative_path: &str) -> String { - format!( - "{}/storage/{}", - self.app_url.trim_end_matches('/'), - relative_path - ) - } } -impl StorageDisk for PublicStorage { - fn base_path(&self) -> &Path { - &self.base_path +impl StorageBackend for LocalBackend { + fn store<'a>( + &'a self, + relative_path: &'a str, + data: &'a [u8], + ) -> BoxFuture<'a, StorageResult<()>> { + Box::pin(async move { + let path = self.base_path.join(relative_path); + if let Some(parent) = path.parent() { + tokio::fs::create_dir_all(parent).await?; + } + + tokio::fs::write(path, data).await.map_err(|e| e.into()) + }) } -} -#[derive(Clone, Debug)] -pub struct PrivateStorage { - base_path: PathBuf, -} + fn read<'a>(&'a self, path: &'a str) -> BoxFuture<'a, StorageResult>> { + Box::pin(async move { + let path = self.base_path.join(path); + match tokio::fs::read(path).await { + Ok(data) => Ok(data), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(vec![]), + Err(e) => Err(e), + } + .map_err(|e| e.into()) + }) + } -impl PrivateStorage { - pub fn new() -> Self { - Self { - base_path: PathBuf::from("storage/private"), - } + fn exists<'a>(&'a self, path: &'a str) -> BoxFuture<'a, StorageResult> { + Box::pin(async move { + let path = self.base_path.join(path); + Ok(tokio::fs::metadata(path).await.is_ok()) + }) } -} -impl StorageDisk for PrivateStorage { - fn base_path(&self) -> &Path { - &self.base_path + fn delete<'a>(&'a self, path: &'a str) -> BoxFuture<'a, StorageResult<()>> { + Box::pin(async move { + let path = self.base_path.join(path); + match tokio::fs::remove_file(path).await { + Ok(()) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(e), + } + .map_err(|e| e.into()) + }) } } -pub trait StorageDisk { - async fn init(&self) -> std::io::Result<()> { - tokio::fs::create_dir_all(self.base_path()).await?; - Ok(()) - } - fn base_path(&self) -> &Path; - fn path(&self, relative_path: &str) -> PathBuf { - self.base_path().join(relative_path) - } - async fn store(&self, relative_path: &str, data: &[u8]) -> std::io::Result<()> { - let path = self.path(relative_path); - if let Some(parent) = path.parent() { - tokio::fs::create_dir_all(parent).await?; - } +#[derive(Clone)] +struct DiskCore { + backend: Arc, +} - tokio::fs::write(path, data).await - } - /// Store data at a path calculated from the hash of the data. Uses content-addressable storage with 2 levels - async fn store_hashed(&self, relative_path: &str, data: &[u8]) -> std::io::Result { - self.store_hashed_with_extension(relative_path, data, None) - .await +impl DiskCore { + pub async fn init(&self) -> StorageResult<()> { + self.backend.init().await } - /// Store data at a path calculated from the hash of the data. Uses content-addressable storage with 2 levels. - /// Extension should not include the dot, and will be added to the end of the filename if provided. - async fn store_hashed_with_extension( + pub async fn store_hashed( &self, relative_path: &str, data: &[u8], extension: Option<&str>, - ) -> std::io::Result { + ) -> StorageResult { let hash = sha256::digest(data); let hashed_path = format!( @@ -122,32 +109,93 @@ pub trait StorageDisk { self.store(&hashed_path, data).await?; Ok(hashed_path) } - async fn read(&self, relative_path: &str) -> std::io::Result> { - match tokio::fs::read(self.path(relative_path)).await { - Ok(data) => Ok(data), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(vec![]), - Err(e) => Err(e), - } + pub async fn store(&self, path: &str, data: &[u8]) -> StorageResult<()> { + self.backend.store(path, data).await } - async fn read_to_string(&self, relative_path: &str) -> std::io::Result { - match tokio::fs::read_to_string(self.path(relative_path)).await { - Ok(data) => Ok(data), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(String::new()), - Err(e) => Err(e), - } + pub async fn read(&self, path: &str) -> StorageResult> { + self.backend.read(path).await } - async fn read_stream(&self, relative_path: &str) -> std::io::Result { - tokio::fs::File::open(self.path(relative_path)).await + pub async fn exists(&self, path: &str) -> StorageResult { + self.backend.exists(path).await } - async fn exists(&self, relative_path: &str) -> std::io::Result { - let path = self.path(relative_path); - Ok(tokio::fs::metadata(path).await.is_ok()) + pub async fn delete(&self, path: &str) -> StorageResult<()> { + self.backend.delete(path).await } - async fn delete(&self, relative_path: &str) -> std::io::Result<()> { - match tokio::fs::remove_file(self.path(relative_path)).await { - Ok(()) => Ok(()), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(e) => Err(e), +} + +#[derive(Clone)] +pub struct PublicDisk { + core: DiskCore, + public_url: String, +} + +impl PublicDisk { + pub fn new(backend: Arc, public_url: String) -> PublicDisk { + PublicDisk { + core: DiskCore { backend }, + public_url, } } + pub fn asset_url(&self, path: &str) -> String { + format!("{}/{}", self.public_url, path) + } + pub async fn init(&self) -> StorageResult<()> { + self.core.init().await + } + pub async fn store_hashed( + &self, + relative_path: &str, + data: &[u8], + extension: Option<&str>, + ) -> StorageResult { + self.core.store_hashed(relative_path, data, extension).await + } + pub async fn store(&self, path: &str, data: &[u8]) -> StorageResult<()> { + self.core.store(path, data).await + } + pub async fn read(&self, path: &str) -> StorageResult> { + self.core.read(path).await + } + pub async fn exists(&self, path: &str) -> StorageResult { + self.core.exists(path).await + } + pub async fn delete(&self, path: &str) -> StorageResult<()> { + self.core.delete(path).await + } +} + +#[derive(Clone)] +pub struct PrivateDisk { + core: DiskCore, } + +impl PrivateDisk { + pub fn new(backend: Arc) -> PrivateDisk { + PrivateDisk { + core: DiskCore { backend }, + } + } + pub async fn init(&self) -> StorageResult<()> { + self.core.init().await + } + pub async fn store_hashed( + &self, + relative_path: &str, + data: &[u8], + extension: Option<&str>, + ) -> StorageResult { + self.core.store_hashed(relative_path, data, extension).await + } + pub async fn store(&self, path: &str, data: &[u8]) -> StorageResult<()> { + self.core.store(path, data).await + } + pub async fn read(&self, path: &str) -> StorageResult> { + self.core.read(path).await + } + pub async fn exists(&self, path: &str) -> StorageResult { + self.core.exists(path).await + } + pub async fn delete(&self, path: &str) -> StorageResult<()> { + self.core.delete(path).await + } +} \ No newline at end of file