view image files
This commit is contained in:
2
backend/Cargo.lock
generated
2
backend/Cargo.lock
generated
@@ -2518,6 +2518,7 @@ dependencies = [
|
|||||||
"argon2",
|
"argon2",
|
||||||
"axum",
|
"axum",
|
||||||
"axum_typed_multipart",
|
"axum_typed_multipart",
|
||||||
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
@@ -2530,6 +2531,7 @@ dependencies = [
|
|||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ anyhow = "1.0.98"
|
|||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
axum = { version = "0.8.4", features = ["multipart", "query"] }
|
axum = { version = "0.8.4", features = ["multipart", "query"] }
|
||||||
axum_typed_multipart = "0.16.3"
|
axum_typed_multipart = "0.16.3"
|
||||||
|
base64 = "0.22.1"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
derive_more = { version = "2.0.1", features = ["display"] }
|
derive_more = { version = "2.0.1", features = ["display"] }
|
||||||
dotenv = "0.15.0"
|
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"] }
|
sqlx = { version = "0.8.6", features = ["chrono", "postgres", "runtime-tokio", "time", "uuid"] }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tokio = { version = "1.46.1", features = ["full"] }
|
tokio = { version = "1.46.1", features = ["full"] }
|
||||||
|
tokio-util = "0.7.15"
|
||||||
tower = "0.5.2"
|
tower = "0.5.2"
|
||||||
tower-http = { version = "0.6.6", features = ["cors", "fs", "trace"] }
|
tower-http = { version = "0.6.6", features = ["cors", "fs", "trace"] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
PostgresConfig::new(config.database_url.clone(), config.database_name.clone());
|
PostgresConfig::new(config.database_url.clone(), config.database_name.clone());
|
||||||
let postgres = Postgres::new(postgres_config).await?;
|
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 = FileSystem::new(fs_config)?;
|
||||||
let fs_service = domain::warren::service::file_system::Service::new(fs, metrics, notifier);
|
let fs_service = domain::warren::service::file_system::Service::new(fs, metrics, notifier);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
mod requests;
|
mod requests;
|
||||||
pub use requests::*;
|
pub use requests::*;
|
||||||
|
use tokio_util::io::ReaderStream;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -227,3 +228,22 @@ impl From<AbsoluteFilePath> for FilePath {
|
|||||||
Self(value.0)
|
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)]
|
#[error(transparent)]
|
||||||
Unknown(#[from] anyhow::Error),
|
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::{
|
use crate::domain::warren::models::file::{
|
||||||
AbsoluteFilePath, CreateDirectoryError, CreateDirectoryRequest, CreateFileError,
|
AbsoluteFilePath, CreateDirectoryError, CreateDirectoryRequest, CreateFileError,
|
||||||
CreateFileRequest, DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError,
|
CreateFileRequest, DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError,
|
||||||
DeleteFileRequest, File, FileName, FilePath, ListFilesError, ListFilesRequest,
|
DeleteFileRequest, FetchFileError, FetchFileRequest, File, FileName, FilePath, ListFilesError,
|
||||||
RelativeFilePath, RenameEntryError, RenameEntryRequest,
|
ListFilesRequest, RelativeFilePath, RenameEntryError, RenameEntryRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Warren, WarrenName};
|
use super::{Warren, WarrenName};
|
||||||
@@ -601,3 +601,44 @@ pub enum DeleteWarrenError {
|
|||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Unknown(#[from] anyhow::Error),
|
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_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_list_warren_files_failure(&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_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_warren_directory_creation_failure(&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_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_file_creation_failure(&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_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_file_deletion_failure(&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_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_auth_warren_directory_deletion_failure(&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
|
/// An upload succeeded fully
|
||||||
fn record_auth_warren_files_upload_success(&self) -> impl Future<Output = ()> + Send;
|
fn record_auth_warren_files_upload_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
/// An upload failed at least partially
|
/// An upload failed at least partially
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ use super::models::{
|
|||||||
},
|
},
|
||||||
file::{
|
file::{
|
||||||
CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest,
|
CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest,
|
||||||
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest, File,
|
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest,
|
||||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
FetchFileError, FetchFileRequest, File, FilePath, FileStream, ListFilesError,
|
||||||
|
ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||||
},
|
},
|
||||||
user::{
|
user::{
|
||||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||||
@@ -39,10 +40,11 @@ use super::models::{
|
|||||||
DeleteWarrenDirectoryRequest, DeleteWarrenDirectoryResponse, DeleteWarrenError,
|
DeleteWarrenDirectoryRequest, DeleteWarrenDirectoryResponse, DeleteWarrenError,
|
||||||
DeleteWarrenFileError, DeleteWarrenFileRequest, DeleteWarrenFileResponse,
|
DeleteWarrenFileError, DeleteWarrenFileRequest, DeleteWarrenFileResponse,
|
||||||
DeleteWarrenRequest, EditWarrenError, EditWarrenRequest, FetchWarrenError,
|
DeleteWarrenRequest, EditWarrenError, EditWarrenRequest, FetchWarrenError,
|
||||||
FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, ListWarrenFilesError,
|
FetchWarrenFileError, FetchWarrenFileRequest, FetchWarrenRequest, FetchWarrensError,
|
||||||
ListWarrenFilesRequest, ListWarrenFilesResponse, ListWarrensError, ListWarrensRequest,
|
FetchWarrensRequest, ListWarrenFilesError, ListWarrenFilesRequest, ListWarrenFilesResponse,
|
||||||
RenameWarrenEntryError, RenameWarrenEntryRequest, RenameWarrenEntryResponse,
|
ListWarrensError, ListWarrensRequest, RenameWarrenEntryError, RenameWarrenEntryRequest,
|
||||||
UploadWarrenFilesError, UploadWarrenFilesRequest, UploadWarrenFilesResponse, Warren,
|
RenameWarrenEntryResponse, UploadWarrenFilesError, UploadWarrenFilesRequest,
|
||||||
|
UploadWarrenFilesResponse, Warren,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,6 +75,11 @@ pub trait WarrenService: Clone + Send + Sync + 'static {
|
|||||||
request: FetchWarrenRequest,
|
request: FetchWarrenRequest,
|
||||||
) -> impl Future<Output = Result<Warren, FetchWarrenError>> + Send;
|
) -> 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(
|
fn list_warren_files(
|
||||||
&self,
|
&self,
|
||||||
request: ListWarrenFilesRequest,
|
request: ListWarrenFilesRequest,
|
||||||
@@ -122,6 +129,10 @@ pub trait FileSystemService: Clone + Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
request: CreateFileRequest,
|
request: CreateFileRequest,
|
||||||
) -> impl Future<Output = Result<FilePath, CreateFileError>> + Send;
|
) -> impl Future<Output = Result<FilePath, CreateFileError>> + Send;
|
||||||
|
fn fetch_file(
|
||||||
|
&self,
|
||||||
|
request: FetchFileRequest,
|
||||||
|
) -> impl Future<Output = Result<FileStream, FetchFileError>> + Send;
|
||||||
fn delete_file(
|
fn delete_file(
|
||||||
&self,
|
&self,
|
||||||
request: DeleteFileRequest,
|
request: DeleteFileRequest,
|
||||||
@@ -234,6 +245,12 @@ pub trait AuthService: Clone + Send + Sync + 'static {
|
|||||||
warren_service: &WS,
|
warren_service: &WS,
|
||||||
) -> impl Future<Output = Result<Warren, AuthError<FetchWarrenError>>> + Send;
|
) -> 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>(
|
fn list_warren_files<WS: WarrenService>(
|
||||||
&self,
|
&self,
|
||||||
request: AuthRequest<ListWarrenFilesRequest>,
|
request: AuthRequest<ListWarrenFilesRequest>,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::domain::warren::models::{
|
use crate::domain::warren::models::{
|
||||||
auth_session::requests::FetchAuthSessionResponse,
|
auth_session::requests::FetchAuthSessionResponse,
|
||||||
file::{File, FilePath},
|
file::{AbsoluteFilePath, File, FilePath},
|
||||||
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
||||||
user_warren::UserWarren,
|
user_warren::UserWarren,
|
||||||
warren::{
|
warren::{
|
||||||
@@ -31,6 +31,12 @@ pub trait WarrenNotifier: Clone + Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
response: &DeleteWarrenDirectoryResponse,
|
response: &DeleteWarrenDirectoryResponse,
|
||||||
) -> impl Future<Output = ()> + Send;
|
) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn warren_file_fetched(
|
||||||
|
&self,
|
||||||
|
warren: &Warren,
|
||||||
|
path: &AbsoluteFilePath,
|
||||||
|
) -> impl Future<Output = ()> + Send;
|
||||||
/// A single file was uploaded
|
/// A single file was uploaded
|
||||||
///
|
///
|
||||||
/// * `warren`: The warren the file was uploaded to
|
/// * `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 directory_deleted(&self, path: &FilePath) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
fn file_created(&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 file_deleted(&self, path: &FilePath) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
fn entry_renamed(
|
fn entry_renamed(
|
||||||
@@ -153,6 +160,14 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
|||||||
user: &User,
|
user: &User,
|
||||||
response: &DeleteWarrenDirectoryResponse,
|
response: &DeleteWarrenDirectoryResponse,
|
||||||
) -> impl Future<Output = ()> + Send;
|
) -> 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
|
/// A collection of files was uploaded
|
||||||
///
|
///
|
||||||
/// * `warren`: The warren the file was uploaded to
|
/// * `warren`: The warren the file was uploaded to
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ use crate::domain::warren::models::{
|
|||||||
},
|
},
|
||||||
file::{
|
file::{
|
||||||
CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest,
|
CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest,
|
||||||
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest, File,
|
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest,
|
||||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
FetchFileError, FetchFileRequest, File, FilePath, FileStream, ListFilesError,
|
||||||
|
ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||||
},
|
},
|
||||||
user::{
|
user::{
|
||||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||||
@@ -84,6 +85,10 @@ pub trait FileSystemRepository: Clone + Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
request: CreateFileRequest,
|
request: CreateFileRequest,
|
||||||
) -> impl Future<Output = Result<FilePath, CreateFileError>> + Send;
|
) -> impl Future<Output = Result<FilePath, CreateFileError>> + Send;
|
||||||
|
fn fetch_file(
|
||||||
|
&self,
|
||||||
|
request: FetchFileRequest,
|
||||||
|
) -> impl Future<Output = Result<FileStream, FetchFileError>> + Send;
|
||||||
fn delete_file(
|
fn delete_file(
|
||||||
&self,
|
&self,
|
||||||
request: DeleteFileRequest,
|
request: DeleteFileRequest,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::{
|
|||||||
FetchAuthSessionRequest, FetchAuthSessionResponse, SessionExpirationTime,
|
FetchAuthSessionRequest, FetchAuthSessionResponse, SessionExpirationTime,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
file::FileStream,
|
||||||
user::{
|
user::{
|
||||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest,
|
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest,
|
||||||
EditUserError, EditUserRequest, ListAllUsersAndWarrensError,
|
EditUserError, EditUserRequest, ListAllUsersAndWarrensError,
|
||||||
@@ -30,11 +31,12 @@ use crate::{
|
|||||||
DeleteWarrenDirectoryError, DeleteWarrenDirectoryRequest,
|
DeleteWarrenDirectoryError, DeleteWarrenDirectoryRequest,
|
||||||
DeleteWarrenDirectoryResponse, DeleteWarrenError, DeleteWarrenFileError,
|
DeleteWarrenDirectoryResponse, DeleteWarrenError, DeleteWarrenFileError,
|
||||||
DeleteWarrenFileRequest, DeleteWarrenFileResponse, DeleteWarrenRequest,
|
DeleteWarrenFileRequest, DeleteWarrenFileResponse, DeleteWarrenRequest,
|
||||||
EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest,
|
EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenFileError,
|
||||||
FetchWarrensRequest, ListWarrenFilesError, ListWarrenFilesRequest,
|
FetchWarrenFileRequest, FetchWarrenRequest, FetchWarrensRequest,
|
||||||
ListWarrenFilesResponse, RenameWarrenEntryError, RenameWarrenEntryRequest,
|
ListWarrenFilesError, ListWarrenFilesRequest, ListWarrenFilesResponse,
|
||||||
RenameWarrenEntryResponse, UploadWarrenFilesError, UploadWarrenFilesRequest,
|
RenameWarrenEntryError, RenameWarrenEntryRequest, RenameWarrenEntryResponse,
|
||||||
UploadWarrenFilesResponse, Warren,
|
UploadWarrenFilesError, UploadWarrenFilesRequest, UploadWarrenFilesResponse,
|
||||||
|
Warren,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService},
|
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService},
|
||||||
@@ -580,6 +582,47 @@ where
|
|||||||
result.map_err(|e| AuthError::Custom(e.into()))
|
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>(
|
async fn list_warren_files<WS: WarrenService>(
|
||||||
&self,
|
&self,
|
||||||
request: AuthRequest<ListWarrenFilesRequest>,
|
request: AuthRequest<ListWarrenFilesRequest>,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::domain::warren::{
|
use crate::domain::warren::{
|
||||||
models::file::{
|
models::file::{
|
||||||
CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest,
|
CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest,
|
||||||
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest, File,
|
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest,
|
||||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
FetchFileError, FetchFileRequest, File, FilePath, FileStream, ListFilesError,
|
||||||
|
ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||||
},
|
},
|
||||||
ports::{FileSystemMetrics, FileSystemNotifier, FileSystemRepository, FileSystemService},
|
ports::{FileSystemMetrics, FileSystemNotifier, FileSystemRepository, FileSystemService},
|
||||||
};
|
};
|
||||||
@@ -98,6 +99,20 @@ where
|
|||||||
result
|
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> {
|
async fn delete_file(&self, request: DeleteFileRequest) -> Result<FilePath, DeleteFileError> {
|
||||||
let result = self.repository.delete_file(request).await;
|
let result = self.repository.delete_file(request).await;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
use crate::domain::warren::{
|
use crate::domain::warren::{
|
||||||
models::warren::{
|
models::{
|
||||||
CreateWarrenDirectoryResponse, CreateWarrenError, CreateWarrenRequest,
|
file::FileStream,
|
||||||
DeleteWarrenDirectoryResponse, DeleteWarrenError, DeleteWarrenFileResponse,
|
warren::{
|
||||||
DeleteWarrenRequest, EditWarrenError, EditWarrenRequest, FetchWarrensError,
|
CreateWarrenDirectoryResponse, CreateWarrenError, CreateWarrenRequest,
|
||||||
FetchWarrensRequest, ListWarrenFilesResponse, ListWarrensError, ListWarrensRequest,
|
DeleteWarrenDirectoryResponse, DeleteWarrenError, DeleteWarrenFileResponse,
|
||||||
RenameWarrenEntryError, RenameWarrenEntryRequest, RenameWarrenEntryResponse,
|
DeleteWarrenRequest, EditWarrenError, EditWarrenRequest, FetchWarrenFileError,
|
||||||
UploadWarrenFilesResponse,
|
FetchWarrenFileRequest, FetchWarrensError, FetchWarrensRequest,
|
||||||
|
ListWarrenFilesResponse, ListWarrensError, ListWarrensRequest, RenameWarrenEntryError,
|
||||||
|
RenameWarrenEntryRequest, RenameWarrenEntryResponse, UploadWarrenFilesResponse,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ports::FileSystemService,
|
ports::FileSystemService,
|
||||||
};
|
};
|
||||||
@@ -146,6 +149,30 @@ where
|
|||||||
result
|
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(
|
async fn list_warren_files(
|
||||||
&self,
|
&self,
|
||||||
request: ListWarrenFilesRequest,
|
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 create_warren_directory;
|
||||||
mod delete_warren_directory;
|
mod delete_warren_directory;
|
||||||
mod delete_warren_file;
|
mod delete_warren_file;
|
||||||
|
mod fetch_file;
|
||||||
mod fetch_warren;
|
mod fetch_warren;
|
||||||
mod list_warren_files;
|
mod list_warren_files;
|
||||||
mod list_warrens;
|
mod list_warrens;
|
||||||
@@ -26,6 +27,7 @@ use create_warren_directory::create_warren_directory;
|
|||||||
use delete_warren_directory::delete_warren_directory;
|
use delete_warren_directory::delete_warren_directory;
|
||||||
|
|
||||||
use delete_warren_file::delete_warren_file;
|
use delete_warren_file::delete_warren_file;
|
||||||
|
use fetch_file::fetch_file;
|
||||||
use rename_warren_entry::rename_warren_entry;
|
use rename_warren_entry::rename_warren_entry;
|
||||||
use upload_warren_files::upload_warren_files;
|
use upload_warren_files::upload_warren_files;
|
||||||
|
|
||||||
@@ -33,6 +35,7 @@ pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>>
|
|||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(list_warrens))
|
.route("/", get(list_warrens))
|
||||||
.route("/", post(fetch_warren))
|
.route("/", post(fetch_warren))
|
||||||
|
.route("/files/fetch", get(fetch_file))
|
||||||
.route("/files", post(list_warren_files))
|
.route("/files", post(list_warren_files))
|
||||||
.route("/files/directory", post(create_warren_directory))
|
.route("/files/directory", post(create_warren_directory))
|
||||||
.route("/files/directory", delete(delete_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 anyhow::{Context, anyhow, bail};
|
||||||
use rustix::fs::statx;
|
use rustix::fs::statx;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs,
|
fs,
|
||||||
io::{self, AsyncWriteExt as _},
|
io::{self, AsyncWriteExt as _},
|
||||||
};
|
};
|
||||||
|
use tokio_util::io::ReaderStream;
|
||||||
|
|
||||||
use crate::domain::warren::{
|
use crate::{
|
||||||
models::file::{
|
config::Config,
|
||||||
AbsoluteFilePath, CreateDirectoryError, CreateDirectoryRequest, CreateFileError,
|
domain::warren::{
|
||||||
CreateFileRequest, DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError,
|
models::file::{
|
||||||
DeleteFileRequest, File, FileMimeType, FileName, FilePath, FileType, ListFilesError,
|
AbsoluteFilePath, CreateDirectoryError, CreateDirectoryRequest, CreateFileError,
|
||||||
ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FileSystemConfig {
|
pub struct FileSystemConfig {
|
||||||
base_directory: String,
|
base_directory: String,
|
||||||
|
max_file_fetch_bytes: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileSystemConfig {
|
impl FileSystemConfig {
|
||||||
pub fn new(base_directory: String) -> Self {
|
pub fn new(base_directory: String, max_file_fetch_bytes: u64) -> Self {
|
||||||
Self { base_directory }
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FileSystem {
|
pub struct FileSystem {
|
||||||
base_directory: FilePath,
|
base_directory: FilePath,
|
||||||
|
max_file_fetch_bytes: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileSystem {
|
impl FileSystem {
|
||||||
pub fn new(config: FileSystemConfig) -> anyhow::Result<Self> {
|
pub fn new(config: FileSystemConfig) -> anyhow::Result<Self> {
|
||||||
let file_system = Self {
|
let file_system = Self {
|
||||||
base_directory: FilePath::new(&config.base_directory)?,
|
base_directory: FilePath::new(&config.base_directory)?,
|
||||||
|
max_file_fetch_bytes: config.max_file_fetch_bytes,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(file_system)
|
Ok(file_system)
|
||||||
@@ -146,6 +171,27 @@ impl FileSystem {
|
|||||||
Ok(path)
|
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
|
/// 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`)
|
/// * `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)
|
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> {
|
async fn delete_file(&self, request: DeleteFileRequest) -> Result<FilePath, DeleteFileError> {
|
||||||
let deleted_path = self
|
let deleted_path = self
|
||||||
.remove_file(request.path())
|
.remove_file(request.path())
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ impl WarrenMetrics for MetricsDebugLogger {
|
|||||||
tracing::debug!("[Metrics] Fetch warrens failed");
|
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) {
|
async fn record_list_warren_files_success(&self) {
|
||||||
tracing::debug!("[Metrics] Warren list files succeeded");
|
tracing::debug!("[Metrics] Warren list files succeeded");
|
||||||
}
|
}
|
||||||
@@ -134,6 +141,13 @@ impl FileSystemMetrics for MetricsDebugLogger {
|
|||||||
tracing::debug!("[Metrics] File creation failed");
|
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) {
|
async fn record_file_deletion_success(&self) {
|
||||||
tracing::debug!("[Metrics] File deletion succeeded");
|
tracing::debug!("[Metrics] File deletion succeeded");
|
||||||
}
|
}
|
||||||
@@ -255,6 +269,13 @@ impl AuthMetrics for MetricsDebugLogger {
|
|||||||
tracing::debug!("[Metrics] User warren deletion failed");
|
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) {
|
async fn record_auth_fetch_user_warren_list_success(&self) {
|
||||||
tracing::debug!("[Metrics] Auth warren list succeeded");
|
tracing::debug!("[Metrics] Auth warren list succeeded");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
|||||||
use crate::domain::warren::{
|
use crate::domain::warren::{
|
||||||
models::{
|
models::{
|
||||||
auth_session::requests::FetchAuthSessionResponse,
|
auth_session::requests::FetchAuthSessionResponse,
|
||||||
file::{File, FilePath},
|
file::{AbsoluteFilePath, File, FilePath},
|
||||||
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
||||||
user_warren::UserWarren,
|
user_warren::UserWarren,
|
||||||
warren::{
|
warren::{
|
||||||
@@ -46,6 +46,14 @@ impl WarrenNotifier for NotifierDebugLogger {
|
|||||||
tracing::debug!("[Notifier] Fetched warren {}", warren.name());
|
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) {
|
async fn warren_files_listed(&self, response: &ListWarrenFilesResponse) {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"[Notifier] Listed {} file(s) in warren {}",
|
"[Notifier] Listed {} file(s) in warren {}",
|
||||||
@@ -121,6 +129,10 @@ impl FileSystemNotifier for NotifierDebugLogger {
|
|||||||
tracing::debug!("[Notifier] Created file {}", path);
|
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) {
|
async fn file_deleted(&self, path: &FilePath) {
|
||||||
tracing::debug!("[Notifier] Deleted file {}", path);
|
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) {
|
async fn auth_warren_files_listed(&self, user: &User, response: &ListWarrenFilesResponse) {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"[Notifier] Listed {} file(s) in warren {} for authenticated user {}",
|
"[Notifier] Listed {} file(s) in warren {} for authenticated user {}",
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import {
|
|||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuSeparator,
|
ContextMenuSeparator,
|
||||||
} from '@/components/ui/context-menu';
|
} from '@/components/ui/context-menu';
|
||||||
import { deleteWarrenDirectory, deleteWarrenFile } from '~/lib/api/warrens';
|
import {
|
||||||
|
deleteWarrenDirectory,
|
||||||
|
deleteWarrenFile,
|
||||||
|
fetchFile,
|
||||||
|
} from '~/lib/api/warrens';
|
||||||
import type { DirectoryEntry } from '#shared/types';
|
import type { DirectoryEntry } from '#shared/types';
|
||||||
|
|
||||||
const warrenStore = useWarrenStore();
|
const warrenStore = useWarrenStore();
|
||||||
@@ -49,11 +53,30 @@ async function openRenameDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onClick() {
|
async function onClick() {
|
||||||
if (warrenStore.loading) {
|
if (warrenStore.loading || warrenStore.current == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
warrenStore.addToCurrentWarrenPath(entry.name);
|
if (entry.fileType === 'directory') {
|
||||||
|
warrenStore.addToCurrentWarrenPath(entry.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.mimeType == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.mimeType.startsWith('image/')) {
|
||||||
|
const result = await fetchFile(
|
||||||
|
warrenStore.current.warrenId,
|
||||||
|
warrenStore.current.path,
|
||||||
|
entry.name
|
||||||
|
);
|
||||||
|
if (result.success) {
|
||||||
|
const url = URL.createObjectURL(result.data);
|
||||||
|
warrenStore.imageViewer.src = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -62,12 +85,7 @@ async function onClick() {
|
|||||||
<ContextMenuTrigger>
|
<ContextMenuTrigger>
|
||||||
<button
|
<button
|
||||||
:disabled="warrenStore.loading || disabled"
|
:disabled="warrenStore.loading || disabled"
|
||||||
:class="[
|
class="bg-accent/30 border-border select-none, flex w-52 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2"
|
||||||
'bg-accent/30 border-border flex w-52 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2 select-none',
|
|
||||||
{
|
|
||||||
'pointer-events-none': entry.fileType === 'file',
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
>
|
>
|
||||||
<div class="flex flex-row items-center">
|
<div class="flex flex-row items-center">
|
||||||
|
|||||||
29
frontend/components/ImageViewer.vue
Normal file
29
frontend/components/ImageViewer.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const warrenStore = useWarrenStore();
|
||||||
|
|
||||||
|
function onOpenUpdate(state: boolean) {
|
||||||
|
if (!state) {
|
||||||
|
warrenStore.imageViewer.src = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
:open="warrenStore.imageViewer.src != null"
|
||||||
|
@update:open="onOpenUpdate"
|
||||||
|
>
|
||||||
|
<DialogTrigger>
|
||||||
|
<slot />
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
class="w-full overflow-hidden p-0 sm:!max-h-[90vh] sm:!max-w-[90vw]"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="warrenStore.imageViewer.src"
|
||||||
|
class="h-full w-full overflow-hidden !object-contain"
|
||||||
|
:src="warrenStore.imageViewer.src"
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -16,6 +16,7 @@ await useAsyncData('warrens', async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
|
<ImageViewer />
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<SidebarInset class="flex flex-col-reverse md:flex-col">
|
<SidebarInset class="flex flex-col-reverse md:flex-col">
|
||||||
<header
|
<header
|
||||||
|
|||||||
@@ -265,3 +265,35 @@ export async function renameWarrenEntry(
|
|||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchFile(
|
||||||
|
warrenId: string,
|
||||||
|
path: string,
|
||||||
|
fileName: string
|
||||||
|
): Promise<{ success: true; data: Blob } | { success: false }> {
|
||||||
|
if (!path.endsWith('/')) {
|
||||||
|
path += '/';
|
||||||
|
}
|
||||||
|
path += fileName;
|
||||||
|
|
||||||
|
const { data, error } = await useFetch<Blob>(
|
||||||
|
getApiUrl(`warrens/files/fetch?warrenId=${warrenId}&path=${path}`),
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: getApiHeaders(),
|
||||||
|
responseType: 'blob',
|
||||||
|
cache: 'default',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.value == null || error.value != null) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: data.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import type { WarrenData } from '#shared/types/warrens';
|
|||||||
export const useWarrenStore = defineStore('warrens', {
|
export const useWarrenStore = defineStore('warrens', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
warrens: {} as Record<string, WarrenData>,
|
warrens: {} as Record<string, WarrenData>,
|
||||||
|
imageViewer: {
|
||||||
|
src: null as string | null,
|
||||||
|
},
|
||||||
current: null as { warrenId: string; path: string } | null,
|
current: null as { warrenId: string; path: string } | null,
|
||||||
loading: false,
|
loading: false,
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user