view image files
This commit is contained in:
2
backend/Cargo.lock
generated
2
backend/Cargo.lock
generated
@@ -2518,6 +2518,7 @@ dependencies = [
|
||||
"argon2",
|
||||
"axum",
|
||||
"axum_typed_multipart",
|
||||
"base64",
|
||||
"chrono",
|
||||
"derive_more",
|
||||
"dotenv",
|
||||
@@ -2530,6 +2531,7 @@ dependencies = [
|
||||
"sqlx",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
|
||||
@@ -16,6 +16,7 @@ anyhow = "1.0.98"
|
||||
argon2 = "0.5.3"
|
||||
axum = { version = "0.8.4", features = ["multipart", "query"] }
|
||||
axum_typed_multipart = "0.16.3"
|
||||
base64 = "0.22.1"
|
||||
chrono = "0.4.41"
|
||||
derive_more = { version = "2.0.1", features = ["display"] }
|
||||
dotenv = "0.15.0"
|
||||
@@ -28,6 +29,7 @@ serde_json = "1.0.140"
|
||||
sqlx = { version = "0.8.6", features = ["chrono", "postgres", "runtime-tokio", "time", "uuid"] }
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.46.1", features = ["full"] }
|
||||
tokio-util = "0.7.15"
|
||||
tower = "0.5.2"
|
||||
tower-http = { version = "0.6.6", features = ["cors", "fs", "trace"] }
|
||||
tracing = "0.1.41"
|
||||
|
||||
@@ -28,7 +28,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
PostgresConfig::new(config.database_url.clone(), config.database_name.clone());
|
||||
let postgres = Postgres::new(postgres_config).await?;
|
||||
|
||||
let fs_config = FileSystemConfig::new(config.serve_dir);
|
||||
let fs_config = FileSystemConfig::from_env(config.serve_dir.clone())?;
|
||||
let fs = FileSystem::new(fs_config)?;
|
||||
let fs_service = domain::warren::service::file_system::Service::new(fs, metrics, notifier);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod requests;
|
||||
pub use requests::*;
|
||||
use tokio_util::io::ReaderStream;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
@@ -227,3 +228,22 @@ impl From<AbsoluteFilePath> for FilePath {
|
||||
Self(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FileStream(ReaderStream<tokio::fs::File>);
|
||||
|
||||
impl FileStream {
|
||||
pub fn new(stream: ReaderStream<tokio::fs::File>) -> Self {
|
||||
Self(stream)
|
||||
}
|
||||
|
||||
pub fn stream(&self) -> &ReaderStream<tokio::fs::File> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileStream> for ReaderStream<tokio::fs::File> {
|
||||
fn from(value: FileStream) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,3 +150,26 @@ pub enum RenameEntryError {
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct FetchFileRequest {
|
||||
path: AbsoluteFilePath,
|
||||
}
|
||||
|
||||
impl FetchFileRequest {
|
||||
pub fn new(path: AbsoluteFilePath) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &AbsoluteFilePath {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FetchFileError {
|
||||
#[error("The file does not exist")]
|
||||
NotFound,
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ use uuid::Uuid;
|
||||
use crate::domain::warren::models::file::{
|
||||
AbsoluteFilePath, CreateDirectoryError, CreateDirectoryRequest, CreateFileError,
|
||||
CreateFileRequest, DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError,
|
||||
DeleteFileRequest, File, FileName, FilePath, ListFilesError, ListFilesRequest,
|
||||
RelativeFilePath, RenameEntryError, RenameEntryRequest,
|
||||
DeleteFileRequest, FetchFileError, FetchFileRequest, File, FileName, FilePath, ListFilesError,
|
||||
ListFilesRequest, RelativeFilePath, RenameEntryError, RenameEntryRequest,
|
||||
};
|
||||
|
||||
use super::{Warren, WarrenName};
|
||||
@@ -601,3 +601,44 @@ pub enum DeleteWarrenError {
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct FetchWarrenFileRequest {
|
||||
warren_id: Uuid,
|
||||
path: AbsoluteFilePath,
|
||||
}
|
||||
|
||||
impl FetchWarrenFileRequest {
|
||||
pub fn new(warren_id: Uuid, path: AbsoluteFilePath) -> Self {
|
||||
Self { warren_id, path }
|
||||
}
|
||||
|
||||
pub fn warren_id(&self) -> &Uuid {
|
||||
&self.warren_id
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &AbsoluteFilePath {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn to_fs_request(self, warren: &Warren) -> FetchFileRequest {
|
||||
let path = warren.path().clone().join(&self.path.to_relative());
|
||||
FetchFileRequest::new(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<FetchWarrenRequest> for &FetchWarrenFileRequest {
|
||||
fn into(self) -> FetchWarrenRequest {
|
||||
FetchWarrenRequest::new(self.warren_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FetchWarrenFileError {
|
||||
#[error(transparent)]
|
||||
FetchWarren(#[from] FetchWarrenError),
|
||||
#[error(transparent)]
|
||||
Fs(#[from] FetchFileError),
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ pub trait WarrenMetrics: Clone + Send + Sync + 'static {
|
||||
fn record_list_warren_files_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_list_warren_files_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_warren_fetch_file_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_warren_fetch_file_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_warren_directory_creation_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_warren_directory_creation_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
@@ -54,6 +57,10 @@ pub trait FileSystemMetrics: Clone + Send + Sync + 'static {
|
||||
|
||||
fn record_file_creation_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_file_creation_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_file_fetch_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_file_fetch_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_file_deletion_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_file_deletion_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
@@ -125,6 +132,9 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static {
|
||||
fn record_auth_warren_directory_deletion_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_auth_warren_directory_deletion_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_auth_warren_fetch_file_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_auth_warren_fetch_file_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
/// An upload succeeded fully
|
||||
fn record_auth_warren_files_upload_success(&self) -> impl Future<Output = ()> + Send;
|
||||
/// An upload failed at least partially
|
||||
|
||||
@@ -16,8 +16,9 @@ use super::models::{
|
||||
},
|
||||
file::{
|
||||
CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest,
|
||||
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest, File,
|
||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest,
|
||||
FetchFileError, FetchFileRequest, File, FilePath, FileStream, ListFilesError,
|
||||
ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||
},
|
||||
user::{
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||
@@ -39,10 +40,11 @@ use super::models::{
|
||||
DeleteWarrenDirectoryRequest, DeleteWarrenDirectoryResponse, DeleteWarrenError,
|
||||
DeleteWarrenFileError, DeleteWarrenFileRequest, DeleteWarrenFileResponse,
|
||||
DeleteWarrenRequest, EditWarrenError, EditWarrenRequest, FetchWarrenError,
|
||||
FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, ListWarrenFilesError,
|
||||
ListWarrenFilesRequest, ListWarrenFilesResponse, ListWarrensError, ListWarrensRequest,
|
||||
RenameWarrenEntryError, RenameWarrenEntryRequest, RenameWarrenEntryResponse,
|
||||
UploadWarrenFilesError, UploadWarrenFilesRequest, UploadWarrenFilesResponse, Warren,
|
||||
FetchWarrenFileError, FetchWarrenFileRequest, FetchWarrenRequest, FetchWarrensError,
|
||||
FetchWarrensRequest, ListWarrenFilesError, ListWarrenFilesRequest, ListWarrenFilesResponse,
|
||||
ListWarrensError, ListWarrensRequest, RenameWarrenEntryError, RenameWarrenEntryRequest,
|
||||
RenameWarrenEntryResponse, UploadWarrenFilesError, UploadWarrenFilesRequest,
|
||||
UploadWarrenFilesResponse, Warren,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -73,6 +75,11 @@ pub trait WarrenService: Clone + Send + Sync + 'static {
|
||||
request: FetchWarrenRequest,
|
||||
) -> impl Future<Output = Result<Warren, FetchWarrenError>> + Send;
|
||||
|
||||
fn fetch_warren_file(
|
||||
&self,
|
||||
request: FetchWarrenFileRequest,
|
||||
) -> impl Future<Output = Result<FileStream, FetchWarrenFileError>> + Send;
|
||||
|
||||
fn list_warren_files(
|
||||
&self,
|
||||
request: ListWarrenFilesRequest,
|
||||
@@ -122,6 +129,10 @@ pub trait FileSystemService: Clone + Send + Sync + 'static {
|
||||
&self,
|
||||
request: CreateFileRequest,
|
||||
) -> impl Future<Output = Result<FilePath, CreateFileError>> + Send;
|
||||
fn fetch_file(
|
||||
&self,
|
||||
request: FetchFileRequest,
|
||||
) -> impl Future<Output = Result<FileStream, FetchFileError>> + Send;
|
||||
fn delete_file(
|
||||
&self,
|
||||
request: DeleteFileRequest,
|
||||
@@ -234,6 +245,12 @@ pub trait AuthService: Clone + Send + Sync + 'static {
|
||||
warren_service: &WS,
|
||||
) -> impl Future<Output = Result<Warren, AuthError<FetchWarrenError>>> + Send;
|
||||
|
||||
fn fetch_warren_file<WS: WarrenService>(
|
||||
&self,
|
||||
request: AuthRequest<FetchWarrenFileRequest>,
|
||||
warren_service: &WS,
|
||||
) -> impl Future<Output = Result<FileStream, AuthError<FetchWarrenFileError>>> + Send;
|
||||
|
||||
fn list_warren_files<WS: WarrenService>(
|
||||
&self,
|
||||
request: AuthRequest<ListWarrenFilesRequest>,
|
||||
|
||||
@@ -2,7 +2,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::domain::warren::models::{
|
||||
auth_session::requests::FetchAuthSessionResponse,
|
||||
file::{File, FilePath},
|
||||
file::{AbsoluteFilePath, File, FilePath},
|
||||
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
||||
user_warren::UserWarren,
|
||||
warren::{
|
||||
@@ -31,6 +31,12 @@ pub trait WarrenNotifier: Clone + Send + Sync + 'static {
|
||||
&self,
|
||||
response: &DeleteWarrenDirectoryResponse,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn warren_file_fetched(
|
||||
&self,
|
||||
warren: &Warren,
|
||||
path: &AbsoluteFilePath,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
/// A single file was uploaded
|
||||
///
|
||||
/// * `warren`: The warren the file was uploaded to
|
||||
@@ -66,6 +72,7 @@ pub trait FileSystemNotifier: Clone + Send + Sync + 'static {
|
||||
fn directory_deleted(&self, path: &FilePath) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn file_created(&self, path: &FilePath) -> impl Future<Output = ()> + Send;
|
||||
fn file_fetched(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
|
||||
fn file_deleted(&self, path: &FilePath) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn entry_renamed(
|
||||
@@ -153,6 +160,14 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
||||
user: &User,
|
||||
response: &DeleteWarrenDirectoryResponse,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn auth_warren_file_fetched(
|
||||
&self,
|
||||
user: &User,
|
||||
warren_id: &Uuid,
|
||||
path: &AbsoluteFilePath,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
|
||||
/// A collection of files was uploaded
|
||||
///
|
||||
/// * `warren`: The warren the file was uploaded to
|
||||
|
||||
@@ -8,8 +8,9 @@ use crate::domain::warren::models::{
|
||||
},
|
||||
file::{
|
||||
CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest,
|
||||
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest, File,
|
||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest,
|
||||
FetchFileError, FetchFileRequest, File, FilePath, FileStream, ListFilesError,
|
||||
ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||
},
|
||||
user::{
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||
@@ -84,6 +85,10 @@ pub trait FileSystemRepository: Clone + Send + Sync + 'static {
|
||||
&self,
|
||||
request: CreateFileRequest,
|
||||
) -> impl Future<Output = Result<FilePath, CreateFileError>> + Send;
|
||||
fn fetch_file(
|
||||
&self,
|
||||
request: FetchFileRequest,
|
||||
) -> impl Future<Output = Result<FileStream, FetchFileError>> + Send;
|
||||
fn delete_file(
|
||||
&self,
|
||||
request: DeleteFileRequest,
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::{
|
||||
FetchAuthSessionRequest, FetchAuthSessionResponse, SessionExpirationTime,
|
||||
},
|
||||
},
|
||||
file::FileStream,
|
||||
user::{
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest,
|
||||
EditUserError, EditUserRequest, ListAllUsersAndWarrensError,
|
||||
@@ -30,11 +31,12 @@ use crate::{
|
||||
DeleteWarrenDirectoryError, DeleteWarrenDirectoryRequest,
|
||||
DeleteWarrenDirectoryResponse, DeleteWarrenError, DeleteWarrenFileError,
|
||||
DeleteWarrenFileRequest, DeleteWarrenFileResponse, DeleteWarrenRequest,
|
||||
EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest,
|
||||
FetchWarrensRequest, ListWarrenFilesError, ListWarrenFilesRequest,
|
||||
ListWarrenFilesResponse, RenameWarrenEntryError, RenameWarrenEntryRequest,
|
||||
RenameWarrenEntryResponse, UploadWarrenFilesError, UploadWarrenFilesRequest,
|
||||
UploadWarrenFilesResponse, Warren,
|
||||
EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenFileError,
|
||||
FetchWarrenFileRequest, FetchWarrenRequest, FetchWarrensRequest,
|
||||
ListWarrenFilesError, ListWarrenFilesRequest, ListWarrenFilesResponse,
|
||||
RenameWarrenEntryError, RenameWarrenEntryRequest, RenameWarrenEntryResponse,
|
||||
UploadWarrenFilesError, UploadWarrenFilesRequest, UploadWarrenFilesResponse,
|
||||
Warren,
|
||||
},
|
||||
},
|
||||
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService},
|
||||
@@ -580,6 +582,47 @@ where
|
||||
result.map_err(|e| AuthError::Custom(e.into()))
|
||||
}
|
||||
|
||||
async fn fetch_warren_file<WS: WarrenService>(
|
||||
&self,
|
||||
request: AuthRequest<FetchWarrenFileRequest>,
|
||||
warren_service: &WS,
|
||||
) -> Result<FileStream, AuthError<FetchWarrenFileError>> {
|
||||
let (session, request) = request.unpack();
|
||||
|
||||
let path = request.path().clone();
|
||||
let session_response = self
|
||||
.fetch_auth_session(FetchAuthSessionRequest::new(session.session_id().clone()))
|
||||
.await?;
|
||||
|
||||
let user_warren = self
|
||||
.repository
|
||||
.fetch_user_warren(FetchUserWarrenRequest::new(
|
||||
session_response.user().id().clone(),
|
||||
request.warren_id().clone(),
|
||||
))
|
||||
.await?;
|
||||
|
||||
if !user_warren.can_read_files() {
|
||||
return Err(AuthError::InsufficientPermissions);
|
||||
}
|
||||
|
||||
let result = warren_service
|
||||
.fetch_warren_file(request)
|
||||
.await
|
||||
.map_err(AuthError::Custom);
|
||||
|
||||
if let Ok(_stream) = result.as_ref() {
|
||||
self.metrics.record_auth_warren_fetch_file_success().await;
|
||||
self.notifier
|
||||
.auth_warren_file_fetched(session_response.user(), user_warren.warren_id(), &path)
|
||||
.await;
|
||||
} else {
|
||||
self.metrics.record_auth_warren_fetch_file_failure().await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn list_warren_files<WS: WarrenService>(
|
||||
&self,
|
||||
request: AuthRequest<ListWarrenFilesRequest>,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::domain::warren::{
|
||||
models::file::{
|
||||
CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest,
|
||||
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest, File,
|
||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest,
|
||||
FetchFileError, FetchFileRequest, File, FilePath, FileStream, ListFilesError,
|
||||
ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||
},
|
||||
ports::{FileSystemMetrics, FileSystemNotifier, FileSystemRepository, FileSystemService},
|
||||
};
|
||||
@@ -98,6 +99,20 @@ where
|
||||
result
|
||||
}
|
||||
|
||||
async fn fetch_file(&self, request: FetchFileRequest) -> Result<FileStream, FetchFileError> {
|
||||
let path = request.path().clone();
|
||||
let result = self.repository.fetch_file(request).await;
|
||||
|
||||
if let Ok(_stream) = result.as_ref() {
|
||||
self.metrics.record_file_fetch_success().await;
|
||||
self.notifier.file_fetched(&path).await;
|
||||
} else {
|
||||
self.metrics.record_file_fetch_failure().await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn delete_file(&self, request: DeleteFileRequest) -> Result<FilePath, DeleteFileError> {
|
||||
let result = self.repository.delete_file(request).await;
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use crate::domain::warren::{
|
||||
models::warren::{
|
||||
CreateWarrenDirectoryResponse, CreateWarrenError, CreateWarrenRequest,
|
||||
DeleteWarrenDirectoryResponse, DeleteWarrenError, DeleteWarrenFileResponse,
|
||||
DeleteWarrenRequest, EditWarrenError, EditWarrenRequest, FetchWarrensError,
|
||||
FetchWarrensRequest, ListWarrenFilesResponse, ListWarrensError, ListWarrensRequest,
|
||||
RenameWarrenEntryError, RenameWarrenEntryRequest, RenameWarrenEntryResponse,
|
||||
UploadWarrenFilesResponse,
|
||||
models::{
|
||||
file::FileStream,
|
||||
warren::{
|
||||
CreateWarrenDirectoryResponse, CreateWarrenError, CreateWarrenRequest,
|
||||
DeleteWarrenDirectoryResponse, DeleteWarrenError, DeleteWarrenFileResponse,
|
||||
DeleteWarrenRequest, EditWarrenError, EditWarrenRequest, FetchWarrenFileError,
|
||||
FetchWarrenFileRequest, FetchWarrensError, FetchWarrensRequest,
|
||||
ListWarrenFilesResponse, ListWarrensError, ListWarrensRequest, RenameWarrenEntryError,
|
||||
RenameWarrenEntryRequest, RenameWarrenEntryResponse, UploadWarrenFilesResponse,
|
||||
},
|
||||
},
|
||||
ports::FileSystemService,
|
||||
};
|
||||
@@ -146,6 +149,30 @@ where
|
||||
result
|
||||
}
|
||||
|
||||
async fn fetch_warren_file(
|
||||
&self,
|
||||
request: FetchWarrenFileRequest,
|
||||
) -> Result<FileStream, FetchWarrenFileError> {
|
||||
let warren = self.repository.fetch_warren((&request).into()).await?;
|
||||
|
||||
let path = request.path().clone();
|
||||
|
||||
let result = self
|
||||
.fs_service
|
||||
.fetch_file(request.to_fs_request(&warren))
|
||||
.await
|
||||
.map_err(Into::into);
|
||||
|
||||
if let Ok(_stream) = result.as_ref() {
|
||||
self.metrics.record_warren_fetch_file_success().await;
|
||||
self.notifier.warren_file_fetched(&warren, &path).await;
|
||||
} else {
|
||||
self.metrics.record_warren_fetch_file_failure().await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn list_warren_files(
|
||||
&self,
|
||||
request: ListWarrenFilesRequest,
|
||||
|
||||
102
backend/src/lib/inbound/http/handlers/warrens/fetch_file.rs
Normal file
102
backend/src/lib/inbound/http/handlers/warrens/fetch_file.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Query, State},
|
||||
};
|
||||
use base64::Engine as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use tokio_util::io::ReaderStream;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
domain::warren::{
|
||||
models::{
|
||||
auth_session::AuthRequest,
|
||||
file::{AbsoluteFilePathError, FilePath, FilePathError, FileStream},
|
||||
warren::FetchWarrenFileRequest,
|
||||
},
|
||||
ports::{AuthService, WarrenService},
|
||||
},
|
||||
inbound::http::{AppState, handlers::extractors::SessionIdHeader, responses::ApiError},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Hash)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct FetchWarrenFileHttpResponseBody {
|
||||
// base64 encoded file content bytes
|
||||
contents: String,
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for FetchWarrenFileHttpResponseBody {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
Self {
|
||||
contents: base64::prelude::BASE64_STANDARD.encode(&value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FetchWarrenFileHttpRequestBody {
|
||||
warren_id: Uuid,
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum ParseFetchWarrenFileHttpRequestError {
|
||||
#[error(transparent)]
|
||||
FilePath(#[from] FilePathError),
|
||||
#[error(transparent)]
|
||||
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||
}
|
||||
|
||||
impl From<ParseFetchWarrenFileHttpRequestError> for ApiError {
|
||||
fn from(value: ParseFetchWarrenFileHttpRequestError) -> Self {
|
||||
match value {
|
||||
ParseFetchWarrenFileHttpRequestError::FilePath(err) => match err {
|
||||
FilePathError::InvalidPath => {
|
||||
ApiError::BadRequest("The file path must be valid".to_string())
|
||||
}
|
||||
},
|
||||
ParseFetchWarrenFileHttpRequestError::AbsoluteFilePath(err) => match err {
|
||||
AbsoluteFilePathError::NotAbsolute => {
|
||||
ApiError::BadRequest("The file path must be absolute".to_string())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FetchWarrenFileHttpRequestBody {
|
||||
fn try_into_domain(
|
||||
self,
|
||||
) -> Result<FetchWarrenFileRequest, ParseFetchWarrenFileHttpRequestError> {
|
||||
let path = FilePath::new(&self.path)?.try_into()?;
|
||||
|
||||
Ok(FetchWarrenFileRequest::new(self.warren_id, path))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileStream> for Body {
|
||||
fn from(value: FileStream) -> Self {
|
||||
Body::from_stream::<ReaderStream<tokio::fs::File>>(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_file<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
SessionIdHeader(session): SessionIdHeader,
|
||||
Query(request): Query<FetchWarrenFileHttpRequestBody>,
|
||||
) -> Result<Body, ApiError> {
|
||||
let domain_request = request.try_into_domain()?;
|
||||
|
||||
state
|
||||
.auth_service
|
||||
.fetch_warren_file(
|
||||
AuthRequest::new(session, domain_request),
|
||||
state.warren_service.as_ref(),
|
||||
)
|
||||
.await
|
||||
.map(|contents| contents.into())
|
||||
.map_err(ApiError::from)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod create_warren_directory;
|
||||
mod delete_warren_directory;
|
||||
mod delete_warren_file;
|
||||
mod fetch_file;
|
||||
mod fetch_warren;
|
||||
mod list_warren_files;
|
||||
mod list_warrens;
|
||||
@@ -26,6 +27,7 @@ use create_warren_directory::create_warren_directory;
|
||||
use delete_warren_directory::delete_warren_directory;
|
||||
|
||||
use delete_warren_file::delete_warren_file;
|
||||
use fetch_file::fetch_file;
|
||||
use rename_warren_entry::rename_warren_entry;
|
||||
use upload_warren_files::upload_warren_files;
|
||||
|
||||
@@ -33,6 +35,7 @@ pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>>
|
||||
Router::new()
|
||||
.route("/", get(list_warrens))
|
||||
.route("/", post(fetch_warren))
|
||||
.route("/files/fetch", get(fetch_file))
|
||||
.route("/files", post(list_warren_files))
|
||||
.route("/files/directory", post(create_warren_directory))
|
||||
.route("/files/directory", delete(delete_warren_directory))
|
||||
|
||||
@@ -1,40 +1,65 @@
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use anyhow::{Context, anyhow, bail};
|
||||
use rustix::fs::statx;
|
||||
use tokio::{
|
||||
fs,
|
||||
io::{self, AsyncWriteExt as _},
|
||||
};
|
||||
use tokio_util::io::ReaderStream;
|
||||
|
||||
use crate::domain::warren::{
|
||||
models::file::{
|
||||
AbsoluteFilePath, CreateDirectoryError, CreateDirectoryRequest, CreateFileError,
|
||||
CreateFileRequest, DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError,
|
||||
DeleteFileRequest, File, FileMimeType, FileName, FilePath, FileType, ListFilesError,
|
||||
ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||
use crate::{
|
||||
config::Config,
|
||||
domain::warren::{
|
||||
models::file::{
|
||||
AbsoluteFilePath, CreateDirectoryError, CreateDirectoryRequest, CreateFileError,
|
||||
CreateFileRequest, DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError,
|
||||
DeleteFileRequest, FetchFileError, FetchFileRequest, File, FileMimeType, FileName,
|
||||
FilePath, FileStream, FileType, ListFilesError, ListFilesRequest, RenameEntryError,
|
||||
RenameEntryRequest,
|
||||
},
|
||||
ports::FileSystemRepository,
|
||||
},
|
||||
ports::FileSystemRepository,
|
||||
};
|
||||
|
||||
const MAX_FILE_FETCH_BYTES: &str = "MAX_FILE_FETCH_BYTES";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileSystemConfig {
|
||||
base_directory: String,
|
||||
max_file_fetch_bytes: u64,
|
||||
}
|
||||
|
||||
impl FileSystemConfig {
|
||||
pub fn new(base_directory: String) -> Self {
|
||||
Self { base_directory }
|
||||
pub fn new(base_directory: String, max_file_fetch_bytes: u64) -> Self {
|
||||
Self {
|
||||
base_directory,
|
||||
max_file_fetch_bytes,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_env(serve_dir: String) -> anyhow::Result<Self> {
|
||||
// 268435456 bytes = 0.25GB
|
||||
let max_file_fetch_bytes: u64 = match Config::load_env(MAX_FILE_FETCH_BYTES) {
|
||||
Ok(value) => value.parse()?,
|
||||
Err(_) => 268435456,
|
||||
};
|
||||
|
||||
Ok(Self::new(serve_dir, max_file_fetch_bytes))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileSystem {
|
||||
base_directory: FilePath,
|
||||
max_file_fetch_bytes: u64,
|
||||
}
|
||||
|
||||
impl FileSystem {
|
||||
pub fn new(config: FileSystemConfig) -> anyhow::Result<Self> {
|
||||
let file_system = Self {
|
||||
base_directory: FilePath::new(&config.base_directory)?,
|
||||
max_file_fetch_bytes: config.max_file_fetch_bytes,
|
||||
};
|
||||
|
||||
Ok(file_system)
|
||||
@@ -146,6 +171,27 @@ impl FileSystem {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
async fn fetch_file(&self, path: &AbsoluteFilePath) -> anyhow::Result<FileStream> {
|
||||
let path = self.get_target_path(path);
|
||||
|
||||
let file = fs::OpenOptions::new()
|
||||
.create(false)
|
||||
.write(false)
|
||||
.read(true)
|
||||
.open(&path)
|
||||
.await?;
|
||||
|
||||
let file_size = file.metadata().await?.size();
|
||||
|
||||
if file_size > self.max_file_fetch_bytes {
|
||||
bail!("File size exceeds configured limit");
|
||||
}
|
||||
|
||||
let stream = FileStream::new(ReaderStream::new(file));
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
/// Actually removes a file from the underlying file system
|
||||
///
|
||||
/// * `path`: The file's absolute path (absolute not in relation to the root file system but `self.base_directory`)
|
||||
@@ -237,6 +283,15 @@ impl FileSystemRepository for FileSystem {
|
||||
Ok(file_path)
|
||||
}
|
||||
|
||||
async fn fetch_file(&self, request: FetchFileRequest) -> Result<FileStream, FetchFileError> {
|
||||
let contents = self
|
||||
.fetch_file(request.path())
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch file {}: {e:?}", request.path()))?;
|
||||
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
async fn delete_file(&self, request: DeleteFileRequest) -> Result<FilePath, DeleteFileError> {
|
||||
let deleted_path = self
|
||||
.remove_file(request.path())
|
||||
|
||||
@@ -52,6 +52,13 @@ impl WarrenMetrics for MetricsDebugLogger {
|
||||
tracing::debug!("[Metrics] Fetch warrens failed");
|
||||
}
|
||||
|
||||
async fn record_warren_fetch_file_success(&self) {
|
||||
tracing::debug!("[Metrics] Fetch warren file succeeded");
|
||||
}
|
||||
async fn record_warren_fetch_file_failure(&self) {
|
||||
tracing::debug!("[Metrics] Fetch warren file failed");
|
||||
}
|
||||
|
||||
async fn record_list_warren_files_success(&self) {
|
||||
tracing::debug!("[Metrics] Warren list files succeeded");
|
||||
}
|
||||
@@ -134,6 +141,13 @@ impl FileSystemMetrics for MetricsDebugLogger {
|
||||
tracing::debug!("[Metrics] File creation failed");
|
||||
}
|
||||
|
||||
async fn record_file_fetch_success(&self) {
|
||||
tracing::debug!("[Metrics] File fetch succeeded");
|
||||
}
|
||||
async fn record_file_fetch_failure(&self) {
|
||||
tracing::debug!("[Metrics] File fetch failed");
|
||||
}
|
||||
|
||||
async fn record_file_deletion_success(&self) {
|
||||
tracing::debug!("[Metrics] File deletion succeeded");
|
||||
}
|
||||
@@ -255,6 +269,13 @@ impl AuthMetrics for MetricsDebugLogger {
|
||||
tracing::debug!("[Metrics] User warren deletion failed");
|
||||
}
|
||||
|
||||
async fn record_auth_warren_fetch_file_success(&self) {
|
||||
tracing::debug!("[Metrics] Warren file fetch succeeded");
|
||||
}
|
||||
async fn record_auth_warren_fetch_file_failure(&self) {
|
||||
tracing::debug!("[Metrics] Warren file fetch failed");
|
||||
}
|
||||
|
||||
async fn record_auth_fetch_user_warren_list_success(&self) {
|
||||
tracing::debug!("[Metrics] Auth warren list succeeded");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
||||
use crate::domain::warren::{
|
||||
models::{
|
||||
auth_session::requests::FetchAuthSessionResponse,
|
||||
file::{File, FilePath},
|
||||
file::{AbsoluteFilePath, File, FilePath},
|
||||
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
||||
user_warren::UserWarren,
|
||||
warren::{
|
||||
@@ -46,6 +46,14 @@ impl WarrenNotifier for NotifierDebugLogger {
|
||||
tracing::debug!("[Notifier] Fetched warren {}", warren.name());
|
||||
}
|
||||
|
||||
async fn warren_file_fetched(&self, warren: &Warren, path: &AbsoluteFilePath) {
|
||||
tracing::debug!(
|
||||
"[Notifier] Fetched file {} in warren {}",
|
||||
path,
|
||||
warren.name(),
|
||||
);
|
||||
}
|
||||
|
||||
async fn warren_files_listed(&self, response: &ListWarrenFilesResponse) {
|
||||
tracing::debug!(
|
||||
"[Notifier] Listed {} file(s) in warren {}",
|
||||
@@ -121,6 +129,10 @@ impl FileSystemNotifier for NotifierDebugLogger {
|
||||
tracing::debug!("[Notifier] Created file {}", path);
|
||||
}
|
||||
|
||||
async fn file_fetched(&self, path: &AbsoluteFilePath) {
|
||||
tracing::debug!("[Notifier] Fetched file {path}");
|
||||
}
|
||||
|
||||
async fn file_deleted(&self, path: &FilePath) {
|
||||
tracing::debug!("[Notifier] Deleted file {}", path);
|
||||
}
|
||||
@@ -269,6 +281,18 @@ impl AuthNotifier for NotifierDebugLogger {
|
||||
);
|
||||
}
|
||||
|
||||
async fn auth_warren_file_fetched(
|
||||
&self,
|
||||
user: &User,
|
||||
warren_id: &Uuid,
|
||||
path: &AbsoluteFilePath,
|
||||
) {
|
||||
tracing::debug!(
|
||||
"[Notifier] User {} fetched file {path} in warren {warren_id}",
|
||||
user.id(),
|
||||
);
|
||||
}
|
||||
|
||||
async fn auth_warren_files_listed(&self, user: &User, response: &ListWarrenFilesResponse) {
|
||||
tracing::debug!(
|
||||
"[Notifier] Listed {} file(s) in warren {} for authenticated user {}",
|
||||
|
||||
Reference in New Issue
Block a user