completely refactor the backend
This commit is contained in:
677
backend/Cargo.lock
generated
677
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -12,11 +12,15 @@ name = "warren_backend"
|
||||
path = "src/bin/backend/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.98"
|
||||
axum = { version = "0.8.4", features = ["multipart", "query"] }
|
||||
axum_typed_multipart = "0.16.3"
|
||||
derive_more = { version = "2.0.1", features = ["display"] }
|
||||
dotenv = "0.15.0"
|
||||
env_logger = "0.11.8"
|
||||
log = "0.4.27"
|
||||
mime_guess = "2.0.5"
|
||||
regex = "1.11.1"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "uuid"] }
|
||||
|
||||
@@ -1,19 +1,51 @@
|
||||
use warren::{server, Result};
|
||||
use warren::{
|
||||
config::Config,
|
||||
domain,
|
||||
inbound::http::{HttpServer, HttpServerConfig},
|
||||
outbound::{
|
||||
file_system::{FileSystem, FileSystemConfig},
|
||||
metrics_debug_logger::MetricsDebugLogger,
|
||||
notifier_debug_logger::NotifierDebugLogger,
|
||||
postgres::{Postgres, PostgresConfig},
|
||||
},
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
let config = Config::from_env()?;
|
||||
|
||||
env_logger::builder()
|
||||
.format_target(false)
|
||||
.filter_level(log::LevelFilter::Info)
|
||||
.parse_env("LOG_LEVEL")
|
||||
.parse_default_env()
|
||||
.filter_level(config.log_level)
|
||||
.init();
|
||||
|
||||
let pool = warren::db::get_postgres_pool().await?;
|
||||
let metrics = MetricsDebugLogger::new();
|
||||
let notifier = NotifierDebugLogger::new();
|
||||
|
||||
server::start(pool).await?;
|
||||
let postgres_config =
|
||||
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 = FileSystem::new(fs_config)?;
|
||||
let fs_service = domain::file_system::service::Service::new(fs, metrics, notifier);
|
||||
|
||||
let warren_service =
|
||||
domain::warren::service::Service::new(postgres, metrics, notifier, fs_service.clone());
|
||||
|
||||
let server_config = HttpServerConfig::new(
|
||||
&config.server_address,
|
||||
&config.server_port,
|
||||
&config.cors_allow_origin,
|
||||
config.static_frontend_dir.as_deref(),
|
||||
);
|
||||
|
||||
let http_server = HttpServer::new(warren_service, server_config).await?;
|
||||
http_server.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
use crate::server::Router;
|
||||
|
||||
mod state;
|
||||
mod warrens;
|
||||
|
||||
pub use state::AppState;
|
||||
|
||||
pub(super) fn router() -> Router {
|
||||
Router::new().nest("/warrens", warrens::router())
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppState {
|
||||
pool: Pool<Postgres>,
|
||||
serve_dir: String,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(pool: Pool<Postgres>, serve_dir: String) -> Self {
|
||||
Self { pool, serve_dir }
|
||||
}
|
||||
|
||||
pub fn pool(&self) -> &Pool<Postgres> {
|
||||
&self.pool
|
||||
}
|
||||
|
||||
pub fn serve_dir(&self) -> &str {
|
||||
&self.serve_dir
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
use axum::extract::{Path, State};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{Result, api::AppState, warrens::Warren};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct CreateWarrenDirectoryPath {
|
||||
warren_id: Uuid,
|
||||
rest: String,
|
||||
}
|
||||
|
||||
pub(super) async fn route(
|
||||
State(state): State<AppState>,
|
||||
Path(path): Path<CreateWarrenDirectoryPath>,
|
||||
) -> Result<()> {
|
||||
let warren = Warren::get(state.pool(), &path.warren_id).await?;
|
||||
|
||||
warren
|
||||
.create_directory(state.serve_dir(), path.rest)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
use axum::extract::{Path, Query, State};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{Result, api::AppState, fs::FileType, warrens::Warren};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct DeleteWarrenDirectoryPath {
|
||||
warren_id: Uuid,
|
||||
rest: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteWarrenParams {
|
||||
file_type: FileType,
|
||||
}
|
||||
|
||||
pub(super) async fn route(
|
||||
State(state): State<AppState>,
|
||||
Path(path): Path<DeleteWarrenDirectoryPath>,
|
||||
Query(DeleteWarrenParams { file_type }): Query<DeleteWarrenParams>,
|
||||
) -> Result<()> {
|
||||
let warren = Warren::get(state.pool(), &path.warren_id).await?;
|
||||
|
||||
warren
|
||||
.delete_entry(state.serve_dir(), path.rest, file_type)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::Result;
|
||||
use crate::api::AppState;
|
||||
use crate::warrens::Warren;
|
||||
|
||||
use crate::fs::DirectoryEntry;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct WarrenRequestPath {
|
||||
warren_id: Uuid,
|
||||
rest: Option<String>,
|
||||
}
|
||||
|
||||
pub(super) async fn route(
|
||||
State(state): State<AppState>,
|
||||
Path(WarrenRequestPath { warren_id, rest }): Path<WarrenRequestPath>,
|
||||
) -> Result<Json<Vec<DirectoryEntry>>> {
|
||||
let warren = Warren::get(state.pool(), &warren_id).await?;
|
||||
|
||||
let entries = warren.read_path(state.serve_dir(), rest).await?;
|
||||
|
||||
Ok(Json(entries))
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
use axum::{Json, extract::State};
|
||||
|
||||
use crate::{Result, api::AppState, warrens::Warren};
|
||||
|
||||
pub(super) async fn route(State(state): State<AppState>) -> Result<Json<Vec<Warren>>> {
|
||||
let warrens = Warren::list(state.pool()).await?;
|
||||
|
||||
Ok(Json(warrens))
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
mod create_directory;
|
||||
mod delete_directory;
|
||||
mod get_warren_path;
|
||||
mod list_warrens;
|
||||
mod upload_files;
|
||||
|
||||
use axum::{
|
||||
extract::DefaultBodyLimit,
|
||||
routing::{delete, get, post},
|
||||
};
|
||||
|
||||
use crate::server::Router;
|
||||
|
||||
pub(super) fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", get(list_warrens::route))
|
||||
.route("/{warren_id}/get", get(get_warren_path::route))
|
||||
.route("/{warren_id}/get/{*rest}", get(get_warren_path::route))
|
||||
.route("/{warren_id}/create/{*rest}", post(create_directory::route))
|
||||
.route(
|
||||
"/{warren_id}/delete/{*rest}",
|
||||
delete(delete_directory::route),
|
||||
)
|
||||
.route(
|
||||
"/{warren_id}/upload",
|
||||
// 536870912 bytes = 0.5GB
|
||||
post(upload_files::route).route_layer(DefaultBodyLimit::max(536870912)),
|
||||
)
|
||||
.route(
|
||||
"/{warren_id}/upload/{*rest}",
|
||||
// 536870912 bytes = 0.5GB
|
||||
post(upload_files::route).route_layer(DefaultBodyLimit::max(536870912)),
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
use axum::extract::{Multipart, Path, State};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{Result, api::AppState, warrens::Warren};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct UploadPath {
|
||||
warren_id: Uuid,
|
||||
rest: Option<String>,
|
||||
}
|
||||
|
||||
pub(super) async fn route(
|
||||
State(state): State<AppState>,
|
||||
Path(UploadPath { warren_id, rest }): Path<UploadPath>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<()> {
|
||||
let warren = Warren::get(state.pool(), &warren_id).await?;
|
||||
|
||||
while let Ok(Some(field)) = multipart.next_field().await {
|
||||
if field.name().is_none_or(|name| name != "files") {
|
||||
continue;
|
||||
};
|
||||
|
||||
let file_name = field.file_name().map(str::to_owned).unwrap();
|
||||
let data = field.bytes().await?;
|
||||
|
||||
warren
|
||||
.upload(state.serve_dir(), rest.as_deref(), &file_name, &data)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
68
backend/src/lib/config.rs
Normal file
68
backend/src/lib/config.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use std::{env, str::FromStr as _};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use log::LevelFilter;
|
||||
|
||||
const DATABASE_URL_KEY: &str = "DATABASE_URL";
|
||||
const DATABASE_NAME_KEY: &str = "DATABASE_NAME";
|
||||
|
||||
const SERVER_ADDRESS_KEY: &str = "SERVER_ADDRESS";
|
||||
const SERVER_PORT_KEY: &str = "SERVER_PORT";
|
||||
const CORS_ALLOW_ORIGIN_KEY: &str = "CORS_ALLOW_ORIGIN";
|
||||
|
||||
const SERVE_DIRECTORY_KEY: &str = "SERVE_DIRECTORY";
|
||||
|
||||
const STATIC_FRONTEND_DIRECTORY: &str = "STATIC_FRONTEND_DIRECTORY";
|
||||
|
||||
const LOG_LEVEL_KEY: &str = "LOG_LEVEL";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Config {
|
||||
pub server_address: String,
|
||||
pub server_port: u16,
|
||||
pub cors_allow_origin: String,
|
||||
|
||||
pub serve_dir: String,
|
||||
pub static_frontend_dir: Option<String>,
|
||||
|
||||
pub database_url: String,
|
||||
pub database_name: String,
|
||||
|
||||
pub log_level: LevelFilter,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_env() -> anyhow::Result<Self> {
|
||||
let server_address = load_env(SERVER_ADDRESS_KEY)?;
|
||||
let server_port = load_env(SERVER_PORT_KEY)?.parse()?;
|
||||
let cors_allow_origin = load_env(CORS_ALLOW_ORIGIN_KEY)?;
|
||||
|
||||
let serve_dir = load_env(SERVE_DIRECTORY_KEY)?;
|
||||
let static_frontend_dir = load_env(STATIC_FRONTEND_DIRECTORY).ok();
|
||||
|
||||
let database_url = load_env(DATABASE_URL_KEY)?;
|
||||
let database_name = load_env(DATABASE_NAME_KEY)?;
|
||||
|
||||
let log_level =
|
||||
LevelFilter::from_str(&load_env(LOG_LEVEL_KEY).unwrap_or("INFO".to_string()))
|
||||
.context("Failed to convert the value of LOG_LEVEL to a LevelFilter")?;
|
||||
|
||||
Ok(Self {
|
||||
server_address,
|
||||
server_port,
|
||||
cors_allow_origin,
|
||||
|
||||
serve_dir,
|
||||
static_frontend_dir,
|
||||
|
||||
database_url,
|
||||
database_name,
|
||||
|
||||
log_level,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn load_env(key: &str) -> anyhow::Result<String> {
|
||||
env::var(key).context(format!("Failed to get environment variable: {key}"))
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
use std::env;
|
||||
|
||||
use sqlx::{Connection, PgConnection, Pool, Postgres, postgres::PgPoolOptions};
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
pub async fn get_postgres_pool() -> Result<Pool<Postgres>, AppError> {
|
||||
let host = env::var("POSTGRES_HOST")?;
|
||||
let port = env::var("POSTGRES_PORT")?;
|
||||
let user = env::var("POSTGRES_USER")?;
|
||||
let password = env::var("POSTGRES_PASSWORD")?;
|
||||
let database = env::var("POSTGRES_DATABASE")?;
|
||||
|
||||
let base_url = format!("postgres://{user}:{password}@{host}:{port}");
|
||||
|
||||
let mut connection = PgConnection::connect(&base_url).await?;
|
||||
let _ = sqlx::query(&format!("CREATE DATABASE {database}"))
|
||||
.execute(&mut connection)
|
||||
.await;
|
||||
connection.close().await?;
|
||||
|
||||
let url = format!("{base_url}/{database}");
|
||||
|
||||
let pool = PgPoolOptions::new().connect(&url).await?;
|
||||
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
3
backend/src/lib/domain/file_system/mod.rs
Normal file
3
backend/src/lib/domain/file_system/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod models;
|
||||
pub mod ports;
|
||||
pub mod service;
|
||||
332
backend/src/lib/domain/file_system/models/file.rs
Normal file
332
backend/src/lib/domain/file_system/models/file.rs
Normal file
@@ -0,0 +1,332 @@
|
||||
use std::{fmt::Display, path::Path};
|
||||
|
||||
use derive_more::Display;
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct File {
|
||||
name: FileName,
|
||||
file_type: FileType,
|
||||
mime_type: Option<FileMimeType>,
|
||||
}
|
||||
|
||||
impl File {
|
||||
pub fn new(name: FileName, file_type: FileType, mime_type: Option<FileMimeType>) -> Self {
|
||||
Self {
|
||||
name,
|
||||
file_type,
|
||||
mime_type,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &FileName {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn file_type(&self) -> &FileType {
|
||||
&self.file_type
|
||||
}
|
||||
|
||||
pub fn mime_type(&self) -> Option<&FileMimeType> {
|
||||
self.mime_type.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// A valid file name
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display)]
|
||||
pub struct FileName(String);
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum FileNameError {
|
||||
#[error("A file name must not contain a slash")]
|
||||
Slash,
|
||||
#[error("A file name must not be empty")]
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl FileName {
|
||||
pub fn new(raw: &str) -> Result<Self, FileNameError> {
|
||||
let trimmed = raw.trim();
|
||||
|
||||
if trimmed.is_empty() {
|
||||
return Err(FileNameError::Empty);
|
||||
}
|
||||
|
||||
if trimmed.contains("/") {
|
||||
return Err(FileNameError::Slash);
|
||||
}
|
||||
|
||||
Ok(Self(trimmed.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
/// A valid file type
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum FileType {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
impl Display for FileType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::File => "File",
|
||||
Self::Directory => "Directory",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A valid file mime type
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display)]
|
||||
pub struct FileMimeType(String);
|
||||
|
||||
impl FileMimeType {
|
||||
pub fn from_name(name: &str) -> Option<Self> {
|
||||
mime_guess::from_path(name)
|
||||
.first_raw()
|
||||
.map(|s| Self(s.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
/// A valid file path that might start with a slash
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display)]
|
||||
pub struct FilePath(String);
|
||||
|
||||
/// A valid file path that does not start or end with a slash
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display)]
|
||||
pub struct RelativeFilePath(String);
|
||||
|
||||
/// A valid file path that starts with a slash
|
||||
/// In the context of this app absolute does not refer to the underlying file system but rather the
|
||||
/// specified base directory
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display, sqlx::Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct AbsoluteFilePath(String);
|
||||
|
||||
impl FilePath {
|
||||
pub fn new(raw: &str) -> Result<FilePath, FilePathError> {
|
||||
if raw.contains("//") || (raw.len() > 1 && raw.ends_with("/")) {
|
||||
return Err(FilePathError::InvalidPath);
|
||||
}
|
||||
|
||||
Ok(Self(raw.to_owned()))
|
||||
}
|
||||
|
||||
pub fn join(&self, other: &RelativeFilePath) -> Self {
|
||||
let mut path = self.0.clone();
|
||||
path.push('/');
|
||||
path.push_str(&other.0);
|
||||
|
||||
Self(path)
|
||||
}
|
||||
|
||||
pub fn push(&mut self, other: &RelativeFilePath) {
|
||||
self.0.push('/');
|
||||
self.0.push_str(&other.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for FilePath {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl RelativeFilePath {
|
||||
/* pub fn new(path: FilePath) -> Self {
|
||||
Self(path.0.trim_start_matches('/').to_owned())
|
||||
} */
|
||||
|
||||
pub fn join(mut self, other: &RelativeFilePath) -> Self {
|
||||
self.0.push('/');
|
||||
self.0.push_str(&other.0);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AbsoluteFilePath {
|
||||
/// Just trims the leading slash
|
||||
pub fn to_relative(mut self) -> RelativeFilePath {
|
||||
self.0.remove(0);
|
||||
|
||||
RelativeFilePath(self.0)
|
||||
}
|
||||
|
||||
pub fn as_relative(&self) -> RelativeFilePath {
|
||||
let mut path = self.0.clone();
|
||||
path.remove(0);
|
||||
|
||||
RelativeFilePath(path)
|
||||
}
|
||||
|
||||
pub fn join(mut self, other: &RelativeFilePath) -> Self {
|
||||
if !other.0.is_empty() {
|
||||
self.0.push('/');
|
||||
self.0.push_str(&other.0);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum FilePathError {
|
||||
#[error("The provided path is invalid")]
|
||||
InvalidPath,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum RelativeFilePathError {
|
||||
#[error("A relative file path must not with a slash")]
|
||||
Absolute,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum AbsoluteFilePathError {
|
||||
#[error("An absolute file path must start with a slash")]
|
||||
NotAbsolute,
|
||||
}
|
||||
|
||||
impl TryInto<AbsoluteFilePath> for FilePath {
|
||||
type Error = AbsoluteFilePathError;
|
||||
|
||||
fn try_into(self) -> Result<AbsoluteFilePath, Self::Error> {
|
||||
if !self.0.starts_with("/") {
|
||||
return Err(AbsoluteFilePathError::NotAbsolute);
|
||||
}
|
||||
|
||||
Ok(AbsoluteFilePath(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<RelativeFilePath> for FilePath {
|
||||
type Error = RelativeFilePathError;
|
||||
|
||||
fn try_into(self) -> Result<RelativeFilePath, Self::Error> {
|
||||
if self.0.starts_with("/") {
|
||||
return Err(RelativeFilePathError::Absolute);
|
||||
}
|
||||
|
||||
Ok(RelativeFilePath(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AbsoluteFilePath> for FilePath {
|
||||
fn from(value: AbsoluteFilePath) -> Self {
|
||||
Self(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ListFilesRequest {
|
||||
path: AbsoluteFilePath,
|
||||
}
|
||||
|
||||
impl ListFilesRequest {
|
||||
pub fn new(path: AbsoluteFilePath) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &AbsoluteFilePath {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ListFilesError {
|
||||
#[error("Directory at path {0} does not exist")]
|
||||
NotFound(String),
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct DeleteDirectoryRequest {
|
||||
path: AbsoluteFilePath,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct DeleteFileRequest {
|
||||
path: AbsoluteFilePath,
|
||||
}
|
||||
|
||||
impl DeleteDirectoryRequest {
|
||||
pub fn new(path: AbsoluteFilePath) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &AbsoluteFilePath {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl DeleteFileRequest {
|
||||
pub fn new(path: AbsoluteFilePath) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &AbsoluteFilePath {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DeleteDirectoryError {
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DeleteFileError {
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct CreateDirectoryRequest {
|
||||
path: AbsoluteFilePath,
|
||||
}
|
||||
|
||||
impl CreateDirectoryRequest {
|
||||
pub fn new(path: AbsoluteFilePath) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &AbsoluteFilePath {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CreateDirectoryError {
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct CreateFileRequest {
|
||||
path: AbsoluteFilePath,
|
||||
data: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl CreateFileRequest {
|
||||
pub fn new(path: AbsoluteFilePath, data: Box<[u8]>) -> Self {
|
||||
Self { path, data }
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &AbsoluteFilePath {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CreateFileError {
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
1
backend/src/lib/domain/file_system/models/mod.rs
Normal file
1
backend/src/lib/domain/file_system/models/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod file;
|
||||
80
backend/src/lib/domain/file_system/ports.rs
Normal file
80
backend/src/lib/domain/file_system/ports.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use super::models::file::{
|
||||
CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest,
|
||||
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest, File,
|
||||
FilePath, ListFilesError, ListFilesRequest,
|
||||
};
|
||||
|
||||
pub trait FileSystemService: Clone + Send + Sync + 'static {
|
||||
fn list_files(
|
||||
&self,
|
||||
request: ListFilesRequest,
|
||||
) -> impl Future<Output = Result<Vec<File>, ListFilesError>> + Send;
|
||||
|
||||
fn create_directory(
|
||||
&self,
|
||||
request: CreateDirectoryRequest,
|
||||
) -> impl Future<Output = Result<FilePath, CreateDirectoryError>> + Send;
|
||||
fn delete_directory(
|
||||
&self,
|
||||
request: DeleteDirectoryRequest,
|
||||
) -> impl Future<Output = Result<FilePath, DeleteDirectoryError>> + Send;
|
||||
|
||||
fn create_file(
|
||||
&self,
|
||||
request: CreateFileRequest,
|
||||
) -> impl Future<Output = Result<FilePath, CreateFileError>> + Send;
|
||||
fn delete_file(
|
||||
&self,
|
||||
request: DeleteFileRequest,
|
||||
) -> impl Future<Output = Result<FilePath, DeleteFileError>> + Send;
|
||||
}
|
||||
|
||||
pub trait FileSystemRepository: Clone + Send + Sync + 'static {
|
||||
fn list_files(
|
||||
&self,
|
||||
request: ListFilesRequest,
|
||||
) -> impl Future<Output = Result<Vec<File>, ListFilesError>> + Send;
|
||||
|
||||
fn create_directory(
|
||||
&self,
|
||||
request: CreateDirectoryRequest,
|
||||
) -> impl Future<Output = Result<FilePath, CreateDirectoryError>> + Send;
|
||||
fn delete_directory(
|
||||
&self,
|
||||
request: DeleteDirectoryRequest,
|
||||
) -> impl Future<Output = Result<FilePath, DeleteDirectoryError>> + Send;
|
||||
|
||||
fn create_file(
|
||||
&self,
|
||||
request: CreateFileRequest,
|
||||
) -> impl Future<Output = Result<FilePath, CreateFileError>> + Send;
|
||||
fn delete_file(
|
||||
&self,
|
||||
request: DeleteFileRequest,
|
||||
) -> impl Future<Output = Result<FilePath, DeleteFileError>> + Send;
|
||||
}
|
||||
|
||||
pub trait FileSystemMetrics: Clone + Send + Sync + 'static {
|
||||
fn record_list_files_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_list_files_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_directory_creation_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_directory_creation_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_directory_deletion_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_directory_deletion_failure(&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_deletion_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_file_deletion_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
pub trait FileSystemNotifier: Clone + Send + Sync + 'static {
|
||||
fn files_listed(&self, files: &Vec<File>) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn directory_created(&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_deleted(&self, path: &FilePath) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
116
backend/src/lib/domain/file_system/service.rs
Normal file
116
backend/src/lib/domain/file_system/service.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use super::{
|
||||
models::file::{
|
||||
CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest,
|
||||
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest, FilePath,
|
||||
ListFilesError, ListFilesRequest,
|
||||
},
|
||||
ports::{FileSystemMetrics, FileSystemNotifier, FileSystemRepository, FileSystemService},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Service<R, M, N>
|
||||
where
|
||||
R: FileSystemRepository,
|
||||
M: FileSystemMetrics,
|
||||
N: FileSystemNotifier,
|
||||
{
|
||||
repository: R,
|
||||
metrics: M,
|
||||
notifier: N,
|
||||
}
|
||||
|
||||
impl<R, M, N> Service<R, M, N>
|
||||
where
|
||||
R: FileSystemRepository,
|
||||
M: FileSystemMetrics,
|
||||
N: FileSystemNotifier,
|
||||
{
|
||||
pub fn new(repository: R, metrics: M, notifier: N) -> Self {
|
||||
Self {
|
||||
repository,
|
||||
metrics,
|
||||
notifier,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, M, N> FileSystemService for Service<R, M, N>
|
||||
where
|
||||
R: FileSystemRepository,
|
||||
M: FileSystemMetrics,
|
||||
N: FileSystemNotifier,
|
||||
{
|
||||
async fn list_files(
|
||||
&self,
|
||||
request: ListFilesRequest,
|
||||
) -> Result<Vec<super::models::file::File>, ListFilesError> {
|
||||
let result = self.repository.list_files(request).await;
|
||||
|
||||
if let Ok(files) = result.as_ref() {
|
||||
self.metrics.record_list_files_success().await;
|
||||
self.notifier.files_listed(files).await;
|
||||
} else {
|
||||
self.metrics.record_list_files_failure().await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn create_directory(
|
||||
&self,
|
||||
request: CreateDirectoryRequest,
|
||||
) -> Result<FilePath, CreateDirectoryError> {
|
||||
let result = self.repository.create_directory(request).await;
|
||||
|
||||
if let Ok(path) = result.as_ref() {
|
||||
self.metrics.record_directory_creation_success().await;
|
||||
self.notifier.directory_created(path).await;
|
||||
} else {
|
||||
self.metrics.record_directory_creation_failure().await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn delete_directory(
|
||||
&self,
|
||||
request: DeleteDirectoryRequest,
|
||||
) -> Result<FilePath, DeleteDirectoryError> {
|
||||
let result = self.repository.delete_directory(request).await;
|
||||
|
||||
if let Ok(path) = result.as_ref() {
|
||||
self.metrics.record_directory_deletion_success().await;
|
||||
self.notifier.directory_deleted(path).await;
|
||||
} else {
|
||||
self.metrics.record_directory_deletion_failure().await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn create_file(&self, request: CreateFileRequest) -> Result<FilePath, CreateFileError> {
|
||||
let result = self.repository.create_file(request).await;
|
||||
|
||||
if let Ok(path) = result.as_ref() {
|
||||
self.metrics.record_file_creation_success().await;
|
||||
self.notifier.file_created(path).await;
|
||||
} else {
|
||||
self.metrics.record_file_creation_failure().await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn delete_file(&self, request: DeleteFileRequest) -> Result<FilePath, DeleteFileError> {
|
||||
let result = self.repository.delete_file(request).await;
|
||||
|
||||
if let Ok(path) = result.as_ref() {
|
||||
self.metrics.record_file_deletion_success().await;
|
||||
self.notifier.file_deleted(path).await;
|
||||
} else {
|
||||
self.metrics.record_file_deletion_failure().await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
2
backend/src/lib/domain/mod.rs
Normal file
2
backend/src/lib/domain/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod file_system;
|
||||
pub mod warren;
|
||||
3
backend/src/lib/domain/warren/mod.rs
Normal file
3
backend/src/lib/domain/warren/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod models;
|
||||
pub mod ports;
|
||||
pub mod service;
|
||||
1
backend/src/lib/domain/warren/models/mod.rs
Normal file
1
backend/src/lib/domain/warren/models/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod warren;
|
||||
359
backend/src/lib/domain/warren/models/warren.rs
Normal file
359
backend/src/lib/domain/warren/models/warren.rs
Normal file
@@ -0,0 +1,359 @@
|
||||
use derive_more::Display;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::domain::file_system::models::file::{
|
||||
AbsoluteFilePath, CreateDirectoryError, CreateDirectoryRequest, CreateFileError,
|
||||
CreateFileRequest, DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError,
|
||||
DeleteFileRequest, FileName, FilePath, ListFilesError, ListFilesRequest, RelativeFilePath,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::FromRow)]
|
||||
pub struct Warren {
|
||||
id: Uuid,
|
||||
name: WarrenName,
|
||||
path: AbsoluteFilePath,
|
||||
}
|
||||
|
||||
impl Warren {
|
||||
pub fn new(id: Uuid, name: WarrenName, path: AbsoluteFilePath) -> Self {
|
||||
Self { id, name, path }
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &Uuid {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &WarrenName {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &AbsoluteFilePath {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
/// A valid warren name
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display, sqlx::Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct WarrenName(String);
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum WarrenNameError {
|
||||
#[error("A warren name must not be empty")]
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl WarrenName {
|
||||
pub fn new(raw: &str) -> Result<Self, WarrenNameError> {
|
||||
let trimmed = raw.trim();
|
||||
|
||||
if trimmed.is_empty() {
|
||||
return Err(WarrenNameError::Empty);
|
||||
}
|
||||
|
||||
Ok(Self(trimmed.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct FetchWarrenRequest {
|
||||
id: Uuid,
|
||||
}
|
||||
|
||||
impl FetchWarrenRequest {
|
||||
pub fn new(id: Uuid) -> Self {
|
||||
Self { id }
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &Uuid {
|
||||
&self.id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FetchWarrenError {
|
||||
#[error("Warren with id {0} does not exist")]
|
||||
NotFound(Uuid),
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ListWarrensRequest;
|
||||
|
||||
impl ListWarrensRequest {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ListWarrensError {
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ListWarrenFilesRequest {
|
||||
warren_id: Uuid,
|
||||
path: AbsoluteFilePath,
|
||||
}
|
||||
|
||||
impl ListWarrenFilesRequest {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<FetchWarrenRequest> for ListWarrenFilesRequest {
|
||||
fn into(self) -> FetchWarrenRequest {
|
||||
FetchWarrenRequest::new(self.warren_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl ListWarrenFilesRequest {
|
||||
pub fn to_fs_request(self, warren: &Warren) -> ListFilesRequest {
|
||||
let path = warren.path().clone().join(&self.path.to_relative());
|
||||
ListFilesRequest::new(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ListWarrenFilesError {
|
||||
#[error(transparent)]
|
||||
FileSystem(#[from] ListFilesError),
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct CreateWarrenDirectoryRequest {
|
||||
warren_id: Uuid,
|
||||
path: AbsoluteFilePath,
|
||||
}
|
||||
|
||||
impl CreateWarrenDirectoryRequest {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<FetchWarrenRequest> for CreateWarrenDirectoryRequest {
|
||||
fn into(self) -> FetchWarrenRequest {
|
||||
FetchWarrenRequest::new(self.warren_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl CreateWarrenDirectoryRequest {
|
||||
pub fn to_fs_request(self, warren: &Warren) -> CreateDirectoryRequest {
|
||||
let path = warren.path().clone().join(&self.path.to_relative());
|
||||
CreateDirectoryRequest::new(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CreateWarrenDirectoryError {
|
||||
#[error(transparent)]
|
||||
FileSystem(#[from] CreateDirectoryError),
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct DeleteWarrenDirectoryRequest {
|
||||
warren_id: Uuid,
|
||||
path: AbsoluteFilePath,
|
||||
force: bool,
|
||||
}
|
||||
|
||||
impl DeleteWarrenDirectoryRequest {
|
||||
pub fn new(warren_id: Uuid, path: AbsoluteFilePath, force: bool) -> Self {
|
||||
Self {
|
||||
warren_id,
|
||||
path,
|
||||
force,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_fs_request(self, warren: &Warren) -> DeleteDirectoryRequest {
|
||||
let path = warren.path().clone().join(&self.path.to_relative());
|
||||
DeleteDirectoryRequest::new(path)
|
||||
}
|
||||
|
||||
pub fn warren_id(&self) -> &Uuid {
|
||||
&self.warren_id
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &AbsoluteFilePath {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn force(&self) -> bool {
|
||||
self.force
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<FetchWarrenRequest> for &DeleteWarrenDirectoryRequest {
|
||||
fn into(self) -> FetchWarrenRequest {
|
||||
FetchWarrenRequest::new(self.warren_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DeleteWarrenDirectoryError {
|
||||
#[error(transparent)]
|
||||
FileSystem(#[from] DeleteDirectoryError),
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct DeleteWarrenFileRequest {
|
||||
warren_id: Uuid,
|
||||
path: AbsoluteFilePath,
|
||||
}
|
||||
|
||||
impl DeleteWarrenFileRequest {
|
||||
pub fn new(warren_id: Uuid, path: AbsoluteFilePath) -> Self {
|
||||
Self { warren_id, path }
|
||||
}
|
||||
|
||||
pub fn to_fs_request(self, warren: &Warren) -> DeleteFileRequest {
|
||||
let path = warren.path().clone().join(&self.path.to_relative());
|
||||
DeleteFileRequest::new(path)
|
||||
}
|
||||
|
||||
pub fn warren_id(&self) -> &Uuid {
|
||||
&self.warren_id
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &AbsoluteFilePath {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<FetchWarrenRequest> for &DeleteWarrenFileRequest {
|
||||
fn into(self) -> FetchWarrenRequest {
|
||||
FetchWarrenRequest::new(self.warren_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DeleteWarrenFileError {
|
||||
#[error(transparent)]
|
||||
FileSystem(#[from] DeleteFileError),
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct UploadWarrenFilesRequest {
|
||||
warren_id: Uuid,
|
||||
path: AbsoluteFilePath,
|
||||
files: UploadFileList,
|
||||
}
|
||||
|
||||
impl UploadWarrenFilesRequest {
|
||||
pub fn new(warren_id: Uuid, path: AbsoluteFilePath, files: UploadFileList) -> Self {
|
||||
Self {
|
||||
warren_id,
|
||||
path,
|
||||
files,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn warren_id(&self) -> &Uuid {
|
||||
&self.warren_id
|
||||
}
|
||||
|
||||
pub fn to_fs_requests(self, warren: &Warren) -> Vec<CreateFileRequest> {
|
||||
let base_upload_path = self.path.as_relative();
|
||||
|
||||
self.files
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|f| {
|
||||
let file_name = FilePath::new(&f.file_name.to_string()).unwrap();
|
||||
let relative_file_path: RelativeFilePath = file_name.try_into().unwrap();
|
||||
let absolute_file_path = warren
|
||||
.path()
|
||||
.clone()
|
||||
.join(&base_upload_path)
|
||||
.join(&relative_file_path);
|
||||
CreateFileRequest::new(absolute_file_path, f.data)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<FetchWarrenRequest> for &UploadWarrenFilesRequest {
|
||||
fn into(self) -> FetchWarrenRequest {
|
||||
FetchWarrenRequest::new(self.warren_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum UploadWarrenFilesError {
|
||||
#[error("Failed to upload the file at index {fail_index}")]
|
||||
Partial { fail_index: usize },
|
||||
#[error(transparent)]
|
||||
FileSystem(#[from] CreateFileError),
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct UploadFileList(Vec<UploadFile>);
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum UploadFileListError {
|
||||
#[error("The file list must not be empty")]
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl UploadFileList {
|
||||
pub fn new(files: Vec<UploadFile>) -> Result<Self, UploadFileListError> {
|
||||
if files.len() < 1 {
|
||||
return Err(UploadFileListError::Empty);
|
||||
}
|
||||
|
||||
Ok(Self(files))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct UploadFile {
|
||||
file_name: FileName,
|
||||
data: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl UploadFile {
|
||||
pub fn new(file_name: FileName, data: Box<[u8]>) -> Self {
|
||||
Self { file_name, data }
|
||||
}
|
||||
|
||||
pub fn file_name(&self) -> &FileName {
|
||||
&self.file_name
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
129
backend/src/lib/domain/warren/ports.rs
Normal file
129
backend/src/lib/domain/warren/ports.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use crate::domain::file_system::models::file::{File, FilePath};
|
||||
|
||||
use super::models::warren::{
|
||||
CreateWarrenDirectoryError, CreateWarrenDirectoryRequest, DeleteWarrenDirectoryError,
|
||||
DeleteWarrenDirectoryRequest, DeleteWarrenFileError, DeleteWarrenFileRequest, FetchWarrenError,
|
||||
FetchWarrenRequest, ListWarrenFilesError, ListWarrenFilesRequest, ListWarrensError,
|
||||
ListWarrensRequest, UploadWarrenFilesError, UploadWarrenFilesRequest, Warren,
|
||||
};
|
||||
|
||||
pub trait WarrenService: Clone + Send + Sync + 'static {
|
||||
fn list_warrens(
|
||||
&self,
|
||||
request: ListWarrensRequest,
|
||||
) -> impl Future<Output = Result<Vec<Warren>, ListWarrensError>> + Send;
|
||||
|
||||
fn fetch_warren(
|
||||
&self,
|
||||
request: FetchWarrenRequest,
|
||||
) -> impl Future<Output = Result<Warren, FetchWarrenError>> + Send;
|
||||
|
||||
fn list_files(
|
||||
&self,
|
||||
request: ListWarrenFilesRequest,
|
||||
) -> impl Future<Output = Result<Vec<File>, ListWarrenFilesError>> + Send;
|
||||
|
||||
fn create_warren_directory(
|
||||
&self,
|
||||
request: CreateWarrenDirectoryRequest,
|
||||
) -> impl Future<Output = Result<FilePath, CreateWarrenDirectoryError>> + Send;
|
||||
|
||||
fn delete_warren_directory(
|
||||
&self,
|
||||
request: DeleteWarrenDirectoryRequest,
|
||||
) -> impl Future<Output = Result<FilePath, DeleteWarrenDirectoryError>> + Send;
|
||||
|
||||
fn upload_warren_files(
|
||||
&self,
|
||||
request: UploadWarrenFilesRequest,
|
||||
) -> impl Future<Output = Result<Vec<FilePath>, UploadWarrenFilesError>> + Send;
|
||||
fn delete_warren_file(
|
||||
&self,
|
||||
request: DeleteWarrenFileRequest,
|
||||
) -> impl Future<Output = Result<FilePath, DeleteWarrenFileError>> + Send;
|
||||
}
|
||||
|
||||
pub trait WarrenRepository: Clone + Send + Sync + 'static {
|
||||
fn list_warrens(
|
||||
&self,
|
||||
request: ListWarrensRequest,
|
||||
) -> impl Future<Output = Result<Vec<Warren>, ListWarrensError>> + Send;
|
||||
|
||||
fn fetch_warren(
|
||||
&self,
|
||||
request: FetchWarrenRequest,
|
||||
) -> impl Future<Output = Result<Warren, FetchWarrenError>> + Send;
|
||||
}
|
||||
|
||||
pub trait WarrenMetrics: Clone + Send + Sync + 'static {
|
||||
fn record_warren_list_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_warren_list_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_warren_fetch_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_warren_fetch_failure(&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_warren_directory_creation_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_warren_directory_creation_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_warren_directory_deletion_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_warren_directory_deletion_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
/// A single file upload succeeded
|
||||
fn record_warren_file_upload_success(&self) -> impl Future<Output = ()> + Send;
|
||||
/// A single file upload failed
|
||||
fn record_warren_file_upload_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
/// An upload succeeded fully
|
||||
fn record_warren_files_upload_success(&self) -> impl Future<Output = ()> + Send;
|
||||
/// An upload failed at least partially
|
||||
fn record_warren_files_upload_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_warren_file_deletion_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_warren_file_deletion_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
pub trait WarrenNotifier: Clone + Send + Sync + 'static {
|
||||
fn warrens_listed(&self, warrens: &Vec<Warren>) -> impl Future<Output = ()> + Send;
|
||||
fn warren_fetched(&self, warren: &Warren) -> impl Future<Output = ()> + Send;
|
||||
fn warren_files_listed(
|
||||
&self,
|
||||
warren: &Warren,
|
||||
files: &Vec<File>,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
fn warren_directory_created(
|
||||
&self,
|
||||
warren: &Warren,
|
||||
path: &FilePath,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
fn warren_directory_deleted(
|
||||
&self,
|
||||
warren: &Warren,
|
||||
path: &FilePath,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
/// A single file was uploaded
|
||||
///
|
||||
/// * `warren`: The warren the file was uploaded to
|
||||
/// * `path`: The file's path
|
||||
fn warren_file_uploaded(
|
||||
&self,
|
||||
warren: &Warren,
|
||||
path: &FilePath,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
/// A collection of files was uploaded
|
||||
///
|
||||
/// * `warren`: The warren the file was uploaded to
|
||||
/// * `files`: The files' paths
|
||||
fn warren_files_uploaded(
|
||||
&self,
|
||||
warren: &Warren,
|
||||
files: &[FilePath],
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
fn warren_file_deleted(
|
||||
&self,
|
||||
warren: &Warren,
|
||||
path: &FilePath,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
223
backend/src/lib/domain/warren/service.rs
Normal file
223
backend/src/lib/domain/warren/service.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::domain::file_system::{
|
||||
models::file::{File, FilePath},
|
||||
ports::FileSystemService,
|
||||
};
|
||||
|
||||
use super::{
|
||||
models::warren::{
|
||||
CreateWarrenDirectoryError, CreateWarrenDirectoryRequest, DeleteWarrenDirectoryError,
|
||||
DeleteWarrenDirectoryRequest, DeleteWarrenFileError, DeleteWarrenFileRequest,
|
||||
FetchWarrenError, FetchWarrenRequest, ListWarrenFilesError, ListWarrenFilesRequest,
|
||||
UploadWarrenFilesError, UploadWarrenFilesRequest, Warren,
|
||||
},
|
||||
ports::{WarrenMetrics, WarrenNotifier, WarrenRepository, WarrenService},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Service<R, M, N, FSS>
|
||||
where
|
||||
R: WarrenRepository,
|
||||
M: WarrenMetrics,
|
||||
N: WarrenNotifier,
|
||||
FSS: FileSystemService,
|
||||
{
|
||||
repository: R,
|
||||
metrics: M,
|
||||
notifier: N,
|
||||
fs_service: FSS,
|
||||
}
|
||||
|
||||
impl<R, M, N, FSS> Service<R, M, N, FSS>
|
||||
where
|
||||
R: WarrenRepository,
|
||||
M: WarrenMetrics,
|
||||
N: WarrenNotifier,
|
||||
FSS: FileSystemService,
|
||||
{
|
||||
pub fn new(repository: R, metrics: M, notifier: N, fs_service: FSS) -> Self {
|
||||
Self {
|
||||
repository,
|
||||
metrics,
|
||||
notifier,
|
||||
fs_service,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, M, N, FSS> WarrenService for Service<R, M, N, FSS>
|
||||
where
|
||||
R: WarrenRepository,
|
||||
M: WarrenMetrics,
|
||||
N: WarrenNotifier,
|
||||
FSS: FileSystemService,
|
||||
{
|
||||
async fn list_warrens(
|
||||
&self,
|
||||
request: super::models::warren::ListWarrensRequest,
|
||||
) -> Result<Vec<Warren>, super::models::warren::ListWarrensError> {
|
||||
let result = self.repository.list_warrens(request).await;
|
||||
|
||||
if let Ok(warren) = result.as_ref() {
|
||||
self.metrics.record_warren_list_success().await;
|
||||
self.notifier.warrens_listed(warren).await;
|
||||
} else {
|
||||
self.metrics.record_warren_list_failure().await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn fetch_warren(&self, request: FetchWarrenRequest) -> Result<Warren, FetchWarrenError> {
|
||||
let result = self.repository.fetch_warren(request).await;
|
||||
|
||||
if let Ok(warren) = result.as_ref() {
|
||||
self.metrics.record_warren_fetch_success().await;
|
||||
self.notifier.warren_fetched(warren).await;
|
||||
} else {
|
||||
self.metrics.record_warren_fetch_failure().await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn list_files(
|
||||
&self,
|
||||
request: ListWarrenFilesRequest,
|
||||
) -> Result<Vec<File>, ListWarrenFilesError> {
|
||||
let warren = self
|
||||
.repository
|
||||
.fetch_warren(request.clone().into())
|
||||
.await
|
||||
.context("Failed to fetch warren")?;
|
||||
|
||||
let result = self
|
||||
.fs_service
|
||||
.list_files(request.to_fs_request(&warren))
|
||||
.await;
|
||||
|
||||
if let Ok(files) = result.as_ref() {
|
||||
self.metrics.record_list_warren_files_success().await;
|
||||
self.notifier.warren_files_listed(&warren, files).await;
|
||||
} else {
|
||||
self.metrics.record_list_warren_files_failure().await;
|
||||
}
|
||||
|
||||
result.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn create_warren_directory(
|
||||
&self,
|
||||
request: CreateWarrenDirectoryRequest,
|
||||
) -> Result<FilePath, CreateWarrenDirectoryError> {
|
||||
let warren = self
|
||||
.repository
|
||||
.fetch_warren(request.clone().into())
|
||||
.await
|
||||
.context("Failed to fetch warren")?;
|
||||
|
||||
let result = self
|
||||
.fs_service
|
||||
.create_directory(request.to_fs_request(&warren))
|
||||
.await;
|
||||
|
||||
if let Ok(path) = result.as_ref() {
|
||||
self.metrics
|
||||
.record_warren_directory_creation_success()
|
||||
.await;
|
||||
self.notifier.warren_directory_created(&warren, path).await;
|
||||
} else {
|
||||
self.metrics
|
||||
.record_warren_directory_creation_failure()
|
||||
.await;
|
||||
}
|
||||
|
||||
result.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn delete_warren_directory(
|
||||
&self,
|
||||
request: DeleteWarrenDirectoryRequest,
|
||||
) -> Result<FilePath, DeleteWarrenDirectoryError> {
|
||||
let warren = self
|
||||
.repository
|
||||
.fetch_warren((&request).into())
|
||||
.await
|
||||
.context("Failed to fetch warren")?;
|
||||
|
||||
let result = self
|
||||
.fs_service
|
||||
.delete_directory(request.to_fs_request(&warren))
|
||||
.await;
|
||||
|
||||
if let Ok(path) = result.as_ref() {
|
||||
self.metrics
|
||||
.record_warren_directory_deletion_success()
|
||||
.await;
|
||||
self.notifier.warren_directory_deleted(&warren, path).await;
|
||||
} else {
|
||||
self.metrics
|
||||
.record_warren_directory_deletion_failure()
|
||||
.await;
|
||||
}
|
||||
|
||||
result.map_err(Into::into)
|
||||
}
|
||||
|
||||
// TODO: Improve this
|
||||
async fn upload_warren_files(
|
||||
&self,
|
||||
request: UploadWarrenFilesRequest,
|
||||
) -> Result<Vec<FilePath>, UploadWarrenFilesError> {
|
||||
let warren = self
|
||||
.repository
|
||||
.fetch_warren((&request).into())
|
||||
.await
|
||||
.context("Failed to fetch warren")?;
|
||||
|
||||
let fs_requests = request.to_fs_requests(&warren);
|
||||
|
||||
let mut paths = Vec::with_capacity(fs_requests.len());
|
||||
|
||||
for (i, req) in fs_requests.into_iter().enumerate() {
|
||||
let result = self.fs_service.create_file(req).await;
|
||||
|
||||
let Ok(file_path) = result else {
|
||||
self.metrics.record_warren_file_upload_failure().await;
|
||||
self.metrics.record_warren_files_upload_failure().await;
|
||||
return Err(UploadWarrenFilesError::Partial { fail_index: i });
|
||||
};
|
||||
|
||||
self.metrics.record_warren_file_upload_success().await;
|
||||
self.notifier
|
||||
.warren_file_uploaded(&warren, &file_path)
|
||||
.await;
|
||||
|
||||
paths.push(file_path);
|
||||
}
|
||||
|
||||
self.metrics.record_warren_files_upload_success().await;
|
||||
self.notifier.warren_files_uploaded(&warren, &paths).await;
|
||||
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
async fn delete_warren_file(
|
||||
&self,
|
||||
request: DeleteWarrenFileRequest,
|
||||
) -> Result<FilePath, DeleteWarrenFileError> {
|
||||
let warren = self
|
||||
.repository
|
||||
.fetch_warren((&request).into())
|
||||
.await
|
||||
.context("Failed to fetch warren")?;
|
||||
|
||||
let result = self
|
||||
.fs_service
|
||||
.delete_file(request.to_fs_request(&warren))
|
||||
.await;
|
||||
|
||||
result.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::multipart::MultipartError,
|
||||
http::{StatusCode, header::InvalidHeaderValue},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AppError {
|
||||
#[error("IO: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Could not get file name")]
|
||||
FileName,
|
||||
#[error("SQLx: {0}")]
|
||||
Sqlx(#[from] sqlx::Error),
|
||||
#[error("Failed to migrate the database")]
|
||||
DatabaseMigration(#[from] sqlx::migrate::MigrateError),
|
||||
#[error("Var: {0}")]
|
||||
Var(#[from] std::env::VarError),
|
||||
#[error("InvalidHeaderValue: {0}")]
|
||||
InvalidHeaderValue(#[from] InvalidHeaderValue),
|
||||
#[error("Multipart: {0}")]
|
||||
Multipart(#[from] MultipartError),
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let status = match self {
|
||||
Self::Io(error) => match error.kind() {
|
||||
std::io::ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
Self::FileName => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
// TODO: Improve
|
||||
Self::Sqlx(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Self::DatabaseMigration(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Self::Var(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Self::InvalidHeaderValue(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Self::Multipart(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.status(status)
|
||||
.body(Body::empty())
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
use std::path::Path;
|
||||
|
||||
use tokio::fs;
|
||||
|
||||
use crate::{Result, error::AppError};
|
||||
|
||||
use super::{DirectoryEntry, FileType};
|
||||
|
||||
pub async fn get_dir_entries<P>(path: P) -> Result<Vec<DirectoryEntry>>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let mut dir = fs::read_dir(path).await?;
|
||||
|
||||
let mut files = Vec::new();
|
||||
|
||||
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||
let name = entry
|
||||
.file_name()
|
||||
.into_string()
|
||||
.map_err(|_| AppError::FileName)?;
|
||||
let file_type = {
|
||||
let file_type = entry.file_type().await?;
|
||||
|
||||
if file_type.is_dir() {
|
||||
FileType::Directory
|
||||
} else if file_type.is_file() {
|
||||
FileType::File
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
files.push(DirectoryEntry::new(name, file_type));
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
pub async fn create_dir<P>(path: P) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
fs::create_dir(path).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_dir<P>(path: P) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
fs::remove_dir_all(path).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
use std::path::Path;
|
||||
|
||||
use tokio::{fs, io::AsyncWriteExt};
|
||||
|
||||
use crate::Result;
|
||||
|
||||
pub async fn write_file<P>(path: P, data: &[u8]) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(path)
|
||||
.await?;
|
||||
|
||||
file.write_all(data).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_file<P>(path: P) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
fs::remove_file(path).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
mod dir;
|
||||
mod file;
|
||||
pub use dir::*;
|
||||
pub use file::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum FileType {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DirectoryEntry {
|
||||
name: String,
|
||||
file_type: FileType,
|
||||
mime_type: Option<String>,
|
||||
}
|
||||
|
||||
impl DirectoryEntry {
|
||||
pub fn new(name: String, file_type: FileType) -> Self {
|
||||
let mime_type = match name.split("/").last() {
|
||||
Some(last) if last.contains(".") => {
|
||||
mime_guess::from_path(&name).first_raw().map(str::to_owned)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Self {
|
||||
name,
|
||||
file_type,
|
||||
mime_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
1
backend/src/lib/inbound/http/handlers/mod.rs
Normal file
1
backend/src/lib/inbound/http/handlers/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod warrens;
|
||||
@@ -0,0 +1,86 @@
|
||||
use axum::{Json, extract::State, http::StatusCode};
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
domain::{
|
||||
file_system::models::file::{AbsoluteFilePathError, FilePath, FilePathError},
|
||||
warren::{
|
||||
models::warren::{CreateWarrenDirectoryError, CreateWarrenDirectoryRequest},
|
||||
ports::WarrenService,
|
||||
},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
responses::{ApiError, ApiSuccess},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateWarrenDirectoryHttpRequestBody {
|
||||
warren_id: Uuid,
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum ParseCreateWarrenDirectoryHttpRequestError {
|
||||
#[error(transparent)]
|
||||
FilePath(#[from] FilePathError),
|
||||
#[error(transparent)]
|
||||
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||
}
|
||||
|
||||
impl From<ParseCreateWarrenDirectoryHttpRequestError> for ApiError {
|
||||
fn from(value: ParseCreateWarrenDirectoryHttpRequestError) -> Self {
|
||||
match value {
|
||||
ParseCreateWarrenDirectoryHttpRequestError::FilePath(err) => match err {
|
||||
FilePathError::InvalidPath => {
|
||||
ApiError::BadRequest("The file path must be valid".to_string())
|
||||
}
|
||||
},
|
||||
ParseCreateWarrenDirectoryHttpRequestError::AbsoluteFilePath(err) => match err {
|
||||
AbsoluteFilePathError::NotAbsolute => {
|
||||
ApiError::BadRequest("The file path must be absolute".to_string())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CreateWarrenDirectoryHttpRequestBody {
|
||||
fn try_into_domain(
|
||||
self,
|
||||
) -> Result<CreateWarrenDirectoryRequest, ParseCreateWarrenDirectoryHttpRequestError> {
|
||||
let path = FilePath::new(&self.path)?;
|
||||
|
||||
Ok(CreateWarrenDirectoryRequest::new(
|
||||
self.warren_id,
|
||||
path.try_into()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CreateWarrenDirectoryError> for ApiError {
|
||||
fn from(_value: CreateWarrenDirectoryError) -> Self {
|
||||
ApiError::InternalServerError("Internal server error".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_warren_directory<WS>(
|
||||
State(state): State<AppState<WS>>,
|
||||
Json(request): Json<CreateWarrenDirectoryHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<()>, ApiError>
|
||||
where
|
||||
WS: WarrenService,
|
||||
{
|
||||
let domain_request = request.try_into_domain()?;
|
||||
|
||||
state
|
||||
.warren_service
|
||||
.create_warren_directory(domain_request)
|
||||
.await
|
||||
.map(|_| ApiSuccess::new(StatusCode::CREATED, ()))
|
||||
.map_err(ApiError::from)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use axum::{Json, extract::State, http::StatusCode};
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
domain::{
|
||||
file_system::models::file::{AbsoluteFilePathError, FilePath, FilePathError},
|
||||
warren::{
|
||||
models::warren::{DeleteWarrenDirectoryError, DeleteWarrenDirectoryRequest},
|
||||
ports::WarrenService,
|
||||
},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
responses::{ApiError, ApiSuccess},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteWarrenDirectoryHttpRequestBody {
|
||||
warren_id: Uuid,
|
||||
path: String,
|
||||
force: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum ParseDeleteWarrenDirectoryHttpRequestError {
|
||||
#[error(transparent)]
|
||||
FilePath(#[from] FilePathError),
|
||||
#[error(transparent)]
|
||||
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||
}
|
||||
|
||||
impl From<ParseDeleteWarrenDirectoryHttpRequestError> for ApiError {
|
||||
fn from(value: ParseDeleteWarrenDirectoryHttpRequestError) -> Self {
|
||||
match value {
|
||||
ParseDeleteWarrenDirectoryHttpRequestError::FilePath(err) => match err {
|
||||
FilePathError::InvalidPath => {
|
||||
ApiError::BadRequest("The file path must be valid".to_string())
|
||||
}
|
||||
},
|
||||
ParseDeleteWarrenDirectoryHttpRequestError::AbsoluteFilePath(err) => match err {
|
||||
AbsoluteFilePathError::NotAbsolute => {
|
||||
ApiError::BadRequest("The file path must be absolute".to_string())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DeleteWarrenDirectoryHttpRequestBody {
|
||||
fn try_into_domain(
|
||||
self,
|
||||
) -> Result<DeleteWarrenDirectoryRequest, ParseDeleteWarrenDirectoryHttpRequestError> {
|
||||
let path = FilePath::new(&self.path)?;
|
||||
|
||||
Ok(DeleteWarrenDirectoryRequest::new(
|
||||
self.warren_id,
|
||||
path.try_into()?,
|
||||
self.force,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DeleteWarrenDirectoryError> for ApiError {
|
||||
fn from(_value: DeleteWarrenDirectoryError) -> Self {
|
||||
ApiError::InternalServerError("Internal server error".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_warren_directory<WS>(
|
||||
State(state): State<AppState<WS>>,
|
||||
Json(request): Json<DeleteWarrenDirectoryHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<()>, ApiError>
|
||||
where
|
||||
WS: WarrenService,
|
||||
{
|
||||
let domain_request = request.try_into_domain()?;
|
||||
|
||||
state
|
||||
.warren_service
|
||||
.delete_warren_directory(domain_request)
|
||||
.await
|
||||
.map(|_| ApiSuccess::new(StatusCode::CREATED, ()))
|
||||
.map_err(ApiError::from)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
use axum::{Json, extract::State, http::StatusCode};
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
domain::{
|
||||
file_system::models::file::{AbsoluteFilePathError, FilePath, FilePathError},
|
||||
warren::{
|
||||
models::warren::{DeleteWarrenFileError, DeleteWarrenFileRequest},
|
||||
ports::WarrenService,
|
||||
},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
responses::{ApiError, ApiSuccess},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteWarrenFileHttpRequestBody {
|
||||
warren_id: Uuid,
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum ParseDeleteWarrenFileHttpRequestError {
|
||||
#[error(transparent)]
|
||||
FilePath(#[from] FilePathError),
|
||||
#[error(transparent)]
|
||||
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||
}
|
||||
|
||||
impl From<ParseDeleteWarrenFileHttpRequestError> for ApiError {
|
||||
fn from(value: ParseDeleteWarrenFileHttpRequestError) -> Self {
|
||||
match value {
|
||||
ParseDeleteWarrenFileHttpRequestError::FilePath(err) => match err {
|
||||
FilePathError::InvalidPath => {
|
||||
ApiError::BadRequest("The file path must be valid".to_string())
|
||||
}
|
||||
},
|
||||
ParseDeleteWarrenFileHttpRequestError::AbsoluteFilePath(err) => match err {
|
||||
AbsoluteFilePathError::NotAbsolute => {
|
||||
ApiError::BadRequest("The file path must be absolute".to_string())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DeleteWarrenFileHttpRequestBody {
|
||||
fn try_into_domain(
|
||||
self,
|
||||
) -> Result<DeleteWarrenFileRequest, ParseDeleteWarrenFileHttpRequestError> {
|
||||
let path = FilePath::new(&self.path)?;
|
||||
|
||||
Ok(DeleteWarrenFileRequest::new(
|
||||
self.warren_id,
|
||||
path.try_into()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DeleteWarrenFileError> for ApiError {
|
||||
fn from(_value: DeleteWarrenFileError) -> Self {
|
||||
ApiError::InternalServerError("Internal server error".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_warren_file<WS>(
|
||||
State(state): State<AppState<WS>>,
|
||||
Json(request): Json<DeleteWarrenFileHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<()>, ApiError>
|
||||
where
|
||||
WS: WarrenService,
|
||||
{
|
||||
let domain_request = request.try_into_domain()?;
|
||||
|
||||
state
|
||||
.warren_service
|
||||
.delete_warren_file(domain_request)
|
||||
.await
|
||||
.map(|_| ApiSuccess::new(StatusCode::CREATED, ()))
|
||||
.map_err(ApiError::from)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
use axum::{Json, extract::State, http::StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
domain::warren::{
|
||||
models::warren::{FetchWarrenError, FetchWarrenRequest, Warren},
|
||||
ports::WarrenService,
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
responses::{ApiError, ApiSuccess},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct FetchWarrenResponseData {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Into<FetchWarrenResponseData> for &Warren {
|
||||
fn into(self) -> FetchWarrenResponseData {
|
||||
FetchWarrenResponseData {
|
||||
id: *self.id(),
|
||||
name: self.name().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FetchWarrenError> for ApiError {
|
||||
fn from(error: FetchWarrenError) -> Self {
|
||||
match error {
|
||||
FetchWarrenError::NotFound(uuid) => {
|
||||
Self::NotFound(format!("Warren with id {uuid} does not exist"))
|
||||
}
|
||||
FetchWarrenError::Unknown(_cause) => {
|
||||
Self::InternalServerError("Internal server error".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
enum ParseFetchWarrenHttpRequestError {}
|
||||
|
||||
impl From<ParseFetchWarrenHttpRequestError> for ApiError {
|
||||
fn from(_e: ParseFetchWarrenHttpRequestError) -> Self {
|
||||
ApiError::InternalServerError("Internal server error".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub struct FetchWarrenHttpRequestBody {
|
||||
id: Uuid,
|
||||
}
|
||||
|
||||
impl FetchWarrenHttpRequestBody {
|
||||
fn try_into_domain(self) -> Result<FetchWarrenRequest, ParseFetchWarrenHttpRequestError> {
|
||||
Ok(FetchWarrenRequest::new(self.id))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_warren<WS: WarrenService>(
|
||||
State(state): State<AppState<WS>>,
|
||||
Json(request): Json<FetchWarrenHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<FetchWarrenResponseData>, ApiError> {
|
||||
let domain_request = request.try_into_domain()?;
|
||||
|
||||
state
|
||||
.warren_service
|
||||
.fetch_warren(domain_request)
|
||||
.await
|
||||
.map(|ref warren| ApiSuccess::new(StatusCode::OK, warren.into()))
|
||||
.map_err(ApiError::from)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
use axum::{Json, extract::State, http::StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
domain::{
|
||||
file_system::models::file::{
|
||||
AbsoluteFilePathError, File, FileMimeType, FilePath, FilePathError, FileType,
|
||||
},
|
||||
warren::{
|
||||
models::warren::{ListWarrenFilesError, ListWarrenFilesRequest},
|
||||
ports::WarrenService,
|
||||
},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
responses::{ApiError, ApiSuccess},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum ParseListWarrenHttpRequestError {
|
||||
#[error(transparent)]
|
||||
FilePath(#[from] FilePathError),
|
||||
#[error(transparent)]
|
||||
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListWarrenFilesHttpRequestBody {
|
||||
warren_id: Uuid,
|
||||
path: String,
|
||||
}
|
||||
|
||||
impl ListWarrenFilesHttpRequestBody {
|
||||
fn try_into_domain(self) -> Result<ListWarrenFilesRequest, ParseListWarrenHttpRequestError> {
|
||||
let path = FilePath::new(&self.path)?;
|
||||
|
||||
Ok(ListWarrenFilesRequest::new(
|
||||
self.warren_id,
|
||||
path.try_into()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseListWarrenHttpRequestError> for ApiError {
|
||||
fn from(value: ParseListWarrenHttpRequestError) -> Self {
|
||||
match value {
|
||||
ParseListWarrenHttpRequestError::FilePath(err) => match err {
|
||||
FilePathError::InvalidPath => {
|
||||
ApiError::BadRequest("The file path must be valid".to_string())
|
||||
}
|
||||
},
|
||||
ParseListWarrenHttpRequestError::AbsoluteFilePath(err) => match err {
|
||||
AbsoluteFilePathError::NotAbsolute => {
|
||||
ApiError::BadRequest("The file path must be absolute".to_string())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WarrenFileElement {
|
||||
name: String,
|
||||
file_type: FileType,
|
||||
mime_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListWarrenFilesResponseData {
|
||||
files: Vec<WarrenFileElement>,
|
||||
}
|
||||
|
||||
impl From<File> for WarrenFileElement {
|
||||
fn from(value: File) -> Self {
|
||||
Self {
|
||||
name: value.name().to_string(),
|
||||
file_type: value.file_type().to_owned(),
|
||||
mime_type: value.mime_type().map(FileMimeType::to_string),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<File>> for ListWarrenFilesResponseData {
|
||||
fn from(value: Vec<File>) -> Self {
|
||||
Self {
|
||||
files: value.into_iter().map(WarrenFileElement::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListWarrenFilesError> for ApiError {
|
||||
fn from(_value: ListWarrenFilesError) -> Self {
|
||||
ApiError::InternalServerError("Internal server error".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_warren_files<WS: WarrenService>(
|
||||
State(state): State<AppState<WS>>,
|
||||
Json(request): Json<ListWarrenFilesHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<ListWarrenFilesResponseData>, ApiError> {
|
||||
let domain_request = request.try_into_domain()?;
|
||||
|
||||
state
|
||||
.warren_service
|
||||
.list_files(domain_request)
|
||||
.await
|
||||
.map(|files| ApiSuccess::new(StatusCode::OK, files.into()))
|
||||
.map_err(ApiError::from)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use axum::{extract::State, http::StatusCode};
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
domain::warren::{
|
||||
models::warren::{ListWarrensError, ListWarrensRequest, Warren},
|
||||
ports::WarrenService,
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
responses::{ApiError, ApiSuccess},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct WarrenListElement {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl From<&Warren> for WarrenListElement {
|
||||
fn from(value: &Warren) -> Self {
|
||||
Self {
|
||||
id: *value.id(),
|
||||
name: value.name().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct ListWarrensResponseData {
|
||||
warrens: Vec<WarrenListElement>,
|
||||
}
|
||||
|
||||
impl From<&Vec<Warren>> for ListWarrensResponseData {
|
||||
fn from(value: &Vec<Warren>) -> Self {
|
||||
ListWarrensResponseData {
|
||||
warrens: value.into_iter().map(WarrenListElement::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListWarrensError> for ApiError {
|
||||
fn from(error: ListWarrensError) -> Self {
|
||||
match error {
|
||||
ListWarrensError::Unknown(_cause) => {
|
||||
Self::InternalServerError("Internal server error".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_warrens<WS: WarrenService>(
|
||||
State(state): State<AppState<WS>>,
|
||||
) -> Result<ApiSuccess<ListWarrensResponseData>, ApiError> {
|
||||
let domain_request = ListWarrensRequest::new();
|
||||
|
||||
state
|
||||
.warren_service
|
||||
.list_warrens(domain_request)
|
||||
.await
|
||||
.map_err(ApiError::from)
|
||||
.map(|ref warrens| ApiSuccess::new(StatusCode::OK, warrens.into()))
|
||||
}
|
||||
40
backend/src/lib/inbound/http/handlers/warrens/mod.rs
Normal file
40
backend/src/lib/inbound/http/handlers/warrens/mod.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
mod create_warren_directory;
|
||||
mod delete_warren_directory;
|
||||
mod delete_warren_file;
|
||||
mod fetch_warren;
|
||||
mod list_warren_files;
|
||||
mod list_warrens;
|
||||
mod upload_warren_files;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
extract::DefaultBodyLimit,
|
||||
routing::{delete, get, post},
|
||||
};
|
||||
|
||||
use crate::{domain::warren::ports::WarrenService, inbound::http::AppState};
|
||||
|
||||
use fetch_warren::fetch_warren;
|
||||
use list_warren_files::list_warren_files;
|
||||
use list_warrens::list_warrens;
|
||||
|
||||
use create_warren_directory::create_warren_directory;
|
||||
use delete_warren_directory::delete_warren_directory;
|
||||
|
||||
use delete_warren_file::delete_warren_file;
|
||||
use upload_warren_files::upload_warren_files;
|
||||
|
||||
pub fn routes<WS: WarrenService>() -> Router<AppState<WS>> {
|
||||
Router::new()
|
||||
.route("/", get(list_warrens))
|
||||
.route("/", post(fetch_warren))
|
||||
.route("/files", post(list_warren_files))
|
||||
.route("/files/directory", post(create_warren_directory))
|
||||
.route("/files/directory", delete(delete_warren_directory))
|
||||
.route(
|
||||
"/files/upload",
|
||||
// 1073741824 bytes = 1GB
|
||||
post(upload_warren_files).route_layer(DefaultBodyLimit::max(1073741824)),
|
||||
)
|
||||
.route("/files/file", delete(delete_warren_file))
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use axum::{body::Bytes, extract::State, http::StatusCode};
|
||||
use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
domain::{
|
||||
file_system::models::file::{
|
||||
AbsoluteFilePathError, FileName, FileNameError, FilePath, FilePathError,
|
||||
},
|
||||
warren::{
|
||||
models::warren::{
|
||||
UploadFile, UploadFileList, UploadFileListError, UploadWarrenFilesError,
|
||||
UploadWarrenFilesRequest,
|
||||
},
|
||||
ports::WarrenService,
|
||||
},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
responses::{ApiError, ApiSuccess},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, TryFromMultipart)]
|
||||
#[try_from_multipart(rename_all = "camelCase")]
|
||||
pub struct UploadWarrenFilesHttpRequestBody {
|
||||
warren_id: Uuid,
|
||||
path: String,
|
||||
files: Vec<FieldData<Bytes>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
enum ParseUploadWarrenFilesHttpRequestError {
|
||||
#[error(transparent)]
|
||||
FileName(#[from] FileNameError),
|
||||
#[error(transparent)]
|
||||
FilePath(#[from] FilePathError),
|
||||
#[error(transparent)]
|
||||
FileList(#[from] UploadFileListError),
|
||||
#[error(transparent)]
|
||||
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||
}
|
||||
|
||||
impl UploadWarrenFilesHttpRequestBody {
|
||||
fn try_into_domain(
|
||||
self,
|
||||
) -> Result<UploadWarrenFilesRequest, ParseUploadWarrenFilesHttpRequestError> {
|
||||
let path = FilePath::new(&self.path)?;
|
||||
|
||||
let mut files = Vec::with_capacity(self.files.len());
|
||||
|
||||
for file in self.files {
|
||||
let raw_file_name = file.metadata.file_name.ok_or(FileNameError::Empty)?;
|
||||
let file_name = FileName::new(&raw_file_name)?;
|
||||
let data = file.contents.to_vec().into_boxed_slice();
|
||||
files.push(UploadFile::new(file_name, data));
|
||||
}
|
||||
|
||||
let files = UploadFileList::new(files)?;
|
||||
|
||||
Ok(UploadWarrenFilesRequest::new(
|
||||
self.warren_id,
|
||||
path.try_into()?,
|
||||
files,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseUploadWarrenFilesHttpRequestError> for ApiError {
|
||||
fn from(value: ParseUploadWarrenFilesHttpRequestError) -> Self {
|
||||
match value {
|
||||
ParseUploadWarrenFilesHttpRequestError::FilePath(err) => match err {
|
||||
FilePathError::InvalidPath => {
|
||||
ApiError::BadRequest("The file path must be valid".to_string())
|
||||
}
|
||||
},
|
||||
ParseUploadWarrenFilesHttpRequestError::AbsoluteFilePath(err) => match err {
|
||||
AbsoluteFilePathError::NotAbsolute => {
|
||||
ApiError::BadRequest("The file path must be absolute".to_string())
|
||||
}
|
||||
},
|
||||
ParseUploadWarrenFilesHttpRequestError::FileList(err) => match err {
|
||||
UploadFileListError::Empty => {
|
||||
ApiError::BadRequest("There has to be at least 1 file present".to_string())
|
||||
}
|
||||
},
|
||||
ParseUploadWarrenFilesHttpRequestError::FileName(err) => match err {
|
||||
FileNameError::Slash => {
|
||||
ApiError::BadRequest("A file's name contained an invalid character".to_string())
|
||||
}
|
||||
FileNameError::Empty => {
|
||||
ApiError::BadRequest("File names must not be empty".to_string())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UploadWarrenFilesError> for ApiError {
|
||||
fn from(_: UploadWarrenFilesError) -> Self {
|
||||
ApiError::InternalServerError("Internal server error".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upload_warren_files<WS>(
|
||||
State(state): State<AppState<WS>>,
|
||||
TypedMultipart(multipart): TypedMultipart<UploadWarrenFilesHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<()>, ApiError>
|
||||
where
|
||||
WS: WarrenService,
|
||||
{
|
||||
let domain_request = multipart.try_into_domain()?;
|
||||
|
||||
state
|
||||
.warren_service
|
||||
.upload_warren_files(domain_request)
|
||||
.await
|
||||
.map(|_| ApiSuccess::new(StatusCode::CREATED, ()))
|
||||
.map_err(ApiError::from)
|
||||
}
|
||||
107
backend/src/lib/inbound/http/mod.rs
Normal file
107
backend/src/lib/inbound/http/mod.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
pub mod handlers;
|
||||
pub mod responses;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{Router, http::HeaderValue};
|
||||
use handlers::warrens;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::{cors::CorsLayer, services::ServeDir};
|
||||
|
||||
use crate::domain::warren::ports::WarrenService;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HttpServerConfig<'a> {
|
||||
pub address: &'a str,
|
||||
pub port: &'a u16,
|
||||
|
||||
pub cors_allow_origin: &'a str,
|
||||
|
||||
pub static_frontend_dir: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> HttpServerConfig<'a> {
|
||||
pub fn new(
|
||||
address: &'a str,
|
||||
port: &'a u16,
|
||||
cors_allow_origin: &'a str,
|
||||
static_frontend_dir: Option<&'a str>,
|
||||
) -> Self {
|
||||
Self {
|
||||
address,
|
||||
port,
|
||||
cors_allow_origin,
|
||||
|
||||
static_frontend_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppState<WS: WarrenService> {
|
||||
warren_service: Arc<WS>,
|
||||
}
|
||||
|
||||
pub struct HttpServer {
|
||||
router: Router,
|
||||
listener: TcpListener,
|
||||
}
|
||||
|
||||
impl HttpServer {
|
||||
pub async fn new<WS: WarrenService>(
|
||||
warren_service: WS,
|
||||
config: HttpServerConfig<'_>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let cors_layer = cors_layer(&config)?;
|
||||
|
||||
let state = AppState {
|
||||
warren_service: Arc::new(warren_service),
|
||||
};
|
||||
|
||||
let mut router = Router::new()
|
||||
.nest("/api", api_routes())
|
||||
.layer(cors_layer)
|
||||
.with_state(state);
|
||||
|
||||
if let Some(frontend_dir) = config.static_frontend_dir {
|
||||
let frontend_service = ServeDir::new(frontend_dir);
|
||||
|
||||
log::debug!("Registering static frontend");
|
||||
router = router.fallback_service(frontend_service);
|
||||
}
|
||||
|
||||
let bind_addr = format!("{}:{}", config.address, config.port);
|
||||
let listener = TcpListener::bind(&bind_addr)
|
||||
.await
|
||||
.context(format!("Failed to listen on {bind_addr}"))?;
|
||||
|
||||
Ok(Self { router, listener })
|
||||
}
|
||||
|
||||
pub async fn run(self) -> anyhow::Result<()> {
|
||||
log::info!("Listening on {}", self.listener.local_addr()?);
|
||||
|
||||
axum::serve(self.listener, self.router)
|
||||
.await
|
||||
.context("HTTP server error")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn cors_layer(config: &HttpServerConfig<'_>) -> anyhow::Result<CorsLayer> {
|
||||
let origin = HeaderValue::from_str(config.cors_allow_origin)
|
||||
.context("Failed to convert cors_allow_origin to a header")?;
|
||||
|
||||
let layer = CorsLayer::default()
|
||||
.allow_origin(origin)
|
||||
.allow_methods(tower_http::cors::Any)
|
||||
.allow_headers(tower_http::cors::Any);
|
||||
|
||||
Ok(layer)
|
||||
}
|
||||
|
||||
fn api_routes<WS: WarrenService>() -> Router<AppState<WS>> {
|
||||
Router::new().nest("/warrens", warrens::routes())
|
||||
}
|
||||
76
backend/src/lib/inbound/http/responses.rs
Normal file
76
backend/src/lib/inbound/http/responses.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use axum::{
|
||||
Json,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
/// Generic response structure shared by all API responses.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct ResponseBody<T: Serialize + PartialEq> {
|
||||
#[serde(rename = "status")]
|
||||
status_code: u16,
|
||||
data: T,
|
||||
}
|
||||
|
||||
impl<T: Serialize + PartialEq> ResponseBody<T> {
|
||||
pub fn new(status: StatusCode, data: T) -> Self {
|
||||
Self {
|
||||
status_code: status.as_u16(),
|
||||
data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApiSuccess<T: Serialize + PartialEq>(StatusCode, Json<ResponseBody<T>>);
|
||||
|
||||
impl<T> PartialEq for ApiSuccess<T>
|
||||
where
|
||||
T: Serialize + PartialEq,
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0 == other.0 && self.1.0 == other.1.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize + PartialEq> ApiSuccess<T> {
|
||||
pub fn new(status: StatusCode, data: T) -> Self {
|
||||
Self(status, Json(ResponseBody::new(status, data)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize + PartialEq> IntoResponse for ApiSuccess<T> {
|
||||
fn into_response(self) -> Response {
|
||||
(self.0, self.1).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ApiError {
|
||||
BadRequest(String),
|
||||
NotFound(String),
|
||||
InternalServerError(String),
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for ApiError {
|
||||
fn from(e: anyhow::Error) -> Self {
|
||||
Self::InternalServerError(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> Response {
|
||||
use ApiError::*;
|
||||
|
||||
match self {
|
||||
BadRequest(_) => (StatusCode::BAD_REQUEST, "Bad request".to_string()).into_response(),
|
||||
NotFound(_) => (StatusCode::NOT_FOUND, "Not found".to_string()).into_response(),
|
||||
InternalServerError(_) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Internal server error".to_string(),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
1
backend/src/lib/inbound/mod.rs
Normal file
1
backend/src/lib/inbound/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod http;
|
||||
@@ -1,10 +1,5 @@
|
||||
use error::AppError;
|
||||
|
||||
pub mod api;
|
||||
pub mod db;
|
||||
pub mod error;
|
||||
pub mod fs;
|
||||
pub mod server;
|
||||
pub mod config;
|
||||
pub mod domain;
|
||||
pub mod inbound;
|
||||
pub mod outbound;
|
||||
pub mod warrens;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AppError>;
|
||||
|
||||
192
backend/src/lib/outbound/file_system.rs
Normal file
192
backend/src/lib/outbound/file_system.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use tokio::{fs, io::AsyncWriteExt as _};
|
||||
|
||||
use crate::domain::file_system::{
|
||||
models::file::{
|
||||
AbsoluteFilePath, CreateDirectoryError, CreateDirectoryRequest, CreateFileError,
|
||||
CreateFileRequest, DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError,
|
||||
DeleteFileRequest, File, FileMimeType, FileName, FilePath, FileType, ListFilesError,
|
||||
ListFilesRequest,
|
||||
},
|
||||
ports::FileSystemRepository,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileSystemConfig {
|
||||
base_directory: String,
|
||||
}
|
||||
|
||||
impl FileSystemConfig {
|
||||
pub fn new(base_directory: String) -> Self {
|
||||
Self { base_directory }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileSystem {
|
||||
base_directory: FilePath,
|
||||
}
|
||||
|
||||
impl FileSystem {
|
||||
pub fn new(config: FileSystemConfig) -> anyhow::Result<Self> {
|
||||
let file_system = Self {
|
||||
base_directory: FilePath::new(&config.base_directory)?,
|
||||
};
|
||||
|
||||
Ok(file_system)
|
||||
}
|
||||
|
||||
/// Combines `self.base_directory` with the specified path
|
||||
///
|
||||
/// * `path`: The absolute path (absolute in relation to the base directory)
|
||||
fn get_target_path(&self, path: &AbsoluteFilePath) -> FilePath {
|
||||
self.base_directory.join(&path.as_relative())
|
||||
}
|
||||
|
||||
async fn get_all_files(&self, absolute_path: &AbsoluteFilePath) -> anyhow::Result<Vec<File>> {
|
||||
let directory_path = self.get_target_path(absolute_path);
|
||||
|
||||
let mut dir = fs::read_dir(&directory_path).await?;
|
||||
|
||||
let mut files = Vec::new();
|
||||
|
||||
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||
let name = entry
|
||||
.file_name()
|
||||
.into_string()
|
||||
.ok()
|
||||
.context("Failed to get file name")?;
|
||||
let file_type = {
|
||||
let file_type = entry.file_type().await?;
|
||||
|
||||
if file_type.is_dir() {
|
||||
FileType::Directory
|
||||
} else if file_type.is_file() {
|
||||
FileType::File
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mime_type = match file_type {
|
||||
FileType::File => FileMimeType::from_name(&name),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
files.push(File::new(FileName::new(&name)?, file_type, mime_type));
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// Actually created a directory in the underlying file system
|
||||
///
|
||||
/// * `path`: The directory's absolute path (absolute not in relation to the root file system but `self.base_directory`)
|
||||
async fn create_dir(&self, path: &AbsoluteFilePath) -> anyhow::Result<FilePath> {
|
||||
let file_path = self.get_target_path(path);
|
||||
|
||||
fs::create_dir(&file_path).await?;
|
||||
|
||||
Ok(file_path)
|
||||
}
|
||||
|
||||
/// Actually removes a directory from the underlying file system
|
||||
///
|
||||
/// * `path`: The directory's absolute path (absolute not in relation to the root file system but `self.base_directory`)
|
||||
/// * `force`: Whether to delete directories that are not empty
|
||||
async fn remove_dir(&self, path: &AbsoluteFilePath, force: bool) -> anyhow::Result<FilePath> {
|
||||
let file_path = self.get_target_path(path);
|
||||
|
||||
if force {
|
||||
fs::remove_dir_all(&file_path).await?;
|
||||
} else {
|
||||
fs::remove_dir(&file_path).await?;
|
||||
}
|
||||
|
||||
Ok(file_path)
|
||||
}
|
||||
|
||||
async fn write_file(&self, path: &AbsoluteFilePath, data: &[u8]) -> anyhow::Result<FilePath> {
|
||||
let path = self.get_target_path(path);
|
||||
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(&path)
|
||||
.await?;
|
||||
|
||||
file.write_all(data).await?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// 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`)
|
||||
async fn remove_file(&self, path: &AbsoluteFilePath) -> anyhow::Result<FilePath> {
|
||||
let path = self.get_target_path(path);
|
||||
|
||||
fs::remove_file(&path).await?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSystemRepository for FileSystem {
|
||||
async fn list_files(&self, request: ListFilesRequest) -> Result<Vec<File>, ListFilesError> {
|
||||
let files = self.get_all_files(request.path()).await.map_err(|err| {
|
||||
anyhow!(err).context(format!(
|
||||
"Failed to get the files at path: {}",
|
||||
request.path()
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
async fn create_directory(
|
||||
&self,
|
||||
request: CreateDirectoryRequest,
|
||||
) -> Result<FilePath, CreateDirectoryError> {
|
||||
let created_path = self.create_dir(request.path()).await.context(format!(
|
||||
"Failed to create directory at path {}",
|
||||
request.path()
|
||||
))?;
|
||||
|
||||
Ok(created_path)
|
||||
}
|
||||
|
||||
async fn delete_directory(
|
||||
&self,
|
||||
request: DeleteDirectoryRequest,
|
||||
) -> Result<FilePath, DeleteDirectoryError> {
|
||||
let deleted_path = self
|
||||
.remove_dir(request.path(), false)
|
||||
.await
|
||||
.context(format!("Failed to delete directory at {}", request.path()))?;
|
||||
|
||||
Ok(deleted_path)
|
||||
}
|
||||
|
||||
async fn create_file(&self, request: CreateFileRequest) -> Result<FilePath, CreateFileError> {
|
||||
let file_path = self
|
||||
.write_file(request.path(), request.data())
|
||||
.await
|
||||
.context(format!(
|
||||
"Failed to write {} byte(s) to path {}",
|
||||
request.data().len(),
|
||||
request.path()
|
||||
))?;
|
||||
|
||||
Ok(file_path)
|
||||
}
|
||||
|
||||
async fn delete_file(&self, request: DeleteFileRequest) -> Result<FilePath, DeleteFileError> {
|
||||
let deleted_path = self
|
||||
.remove_file(request.path())
|
||||
.await
|
||||
.context(format!("Failed to delete file at {}", request.path()))?;
|
||||
|
||||
Ok(deleted_path)
|
||||
}
|
||||
}
|
||||
110
backend/src/lib/outbound/metrics_debug_logger.rs
Normal file
110
backend/src/lib/outbound/metrics_debug_logger.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use crate::domain::{file_system::ports::FileSystemMetrics, warren::ports::WarrenMetrics};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct MetricsDebugLogger;
|
||||
|
||||
impl MetricsDebugLogger {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl WarrenMetrics for MetricsDebugLogger {
|
||||
async fn record_warren_list_success(&self) {
|
||||
log::debug!("[Metrics] Warren list succeeded");
|
||||
}
|
||||
|
||||
async fn record_warren_list_failure(&self) {
|
||||
log::debug!("[Metrics] Warren list failed");
|
||||
}
|
||||
|
||||
async fn record_warren_fetch_success(&self) {
|
||||
log::debug!("[Metrics] Warren fetch succeeded");
|
||||
}
|
||||
|
||||
async fn record_warren_fetch_failure(&self) {
|
||||
log::debug!("[Metrics] Warren fetch failed");
|
||||
}
|
||||
|
||||
async fn record_list_warren_files_success(&self) {
|
||||
log::debug!("[Metrics] Warren list files succeeded");
|
||||
}
|
||||
|
||||
async fn record_list_warren_files_failure(&self) {
|
||||
log::debug!("[Metrics] Warren list files failed");
|
||||
}
|
||||
|
||||
async fn record_warren_directory_creation_success(&self) {
|
||||
log::debug!("[Metrics] Warren directory creation succeeded");
|
||||
}
|
||||
|
||||
async fn record_warren_directory_creation_failure(&self) {
|
||||
log::debug!("[Metrics] Warren directory creation failed");
|
||||
}
|
||||
|
||||
async fn record_warren_directory_deletion_success(&self) {
|
||||
log::debug!("[Metrics] Warren directory deletion succeeded");
|
||||
}
|
||||
|
||||
async fn record_warren_directory_deletion_failure(&self) {
|
||||
log::debug!("[Metrics] Warren directory deletion failed");
|
||||
}
|
||||
|
||||
async fn record_warren_file_upload_success(&self) {
|
||||
log::debug!("[Metrics] Warren file upload succeeded");
|
||||
}
|
||||
async fn record_warren_file_upload_failure(&self) {
|
||||
log::debug!("[Metrics] Warren file upload failed");
|
||||
}
|
||||
|
||||
async fn record_warren_files_upload_success(&self) {
|
||||
log::debug!("[Metrics] Warren files upload succeded");
|
||||
}
|
||||
async fn record_warren_files_upload_failure(&self) {
|
||||
log::debug!("[Metrics] Warren files upload failed at least partially");
|
||||
}
|
||||
|
||||
async fn record_warren_file_deletion_success(&self) {
|
||||
log::debug!("[Metrics] Warren file deletion succeeded");
|
||||
}
|
||||
async fn record_warren_file_deletion_failure(&self) {
|
||||
log::debug!("[Metrics] Warren file deletion failed");
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSystemMetrics for MetricsDebugLogger {
|
||||
async fn record_list_files_success(&self) {
|
||||
log::debug!("[Metrics] File list succeeded");
|
||||
}
|
||||
async fn record_list_files_failure(&self) {
|
||||
log::debug!("[Metrics] File list failed");
|
||||
}
|
||||
|
||||
async fn record_directory_creation_success(&self) {
|
||||
log::debug!("[Metrics] Directory creation succeeded");
|
||||
}
|
||||
async fn record_directory_creation_failure(&self) {
|
||||
log::debug!("[Metrics] Directory creation failed");
|
||||
}
|
||||
|
||||
async fn record_directory_deletion_success(&self) {
|
||||
log::debug!("[Metrics] Directory deletion succeeded");
|
||||
}
|
||||
async fn record_directory_deletion_failure(&self) {
|
||||
log::debug!("[Metrics] Directory deletion failed");
|
||||
}
|
||||
|
||||
async fn record_file_creation_success(&self) {
|
||||
log::debug!("[Metrics] File creation succeeded");
|
||||
}
|
||||
async fn record_file_creation_failure(&self) {
|
||||
log::debug!("[Metrics] File creation failed");
|
||||
}
|
||||
|
||||
async fn record_file_deletion_success(&self) {
|
||||
log::debug!("[Metrics] File deletion succeeded");
|
||||
}
|
||||
async fn record_file_deletion_failure(&self) {
|
||||
log::debug!("[Metrics] File deletion failed");
|
||||
}
|
||||
}
|
||||
4
backend/src/lib/outbound/mod.rs
Normal file
4
backend/src/lib/outbound/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod file_system;
|
||||
pub mod metrics_debug_logger;
|
||||
pub mod notifier_debug_logger;
|
||||
pub mod postgres;
|
||||
96
backend/src/lib/outbound/notifier_debug_logger.rs
Normal file
96
backend/src/lib/outbound/notifier_debug_logger.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use crate::domain::{
|
||||
file_system::{
|
||||
models::file::{File, FilePath},
|
||||
ports::FileSystemNotifier,
|
||||
},
|
||||
warren::{models::warren::Warren, ports::WarrenNotifier},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct NotifierDebugLogger;
|
||||
|
||||
impl NotifierDebugLogger {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl WarrenNotifier for NotifierDebugLogger {
|
||||
async fn warrens_listed(&self, warrens: &Vec<Warren>) {
|
||||
log::debug!("[Notifier] Listed {} warren(s)", warrens.len());
|
||||
}
|
||||
|
||||
async fn warren_fetched(&self, warren: &Warren) {
|
||||
log::debug!("[Notifier] Fetched warren {}", warren.name());
|
||||
}
|
||||
|
||||
async fn warren_files_listed(&self, warren: &Warren, files: &Vec<File>) {
|
||||
log::debug!(
|
||||
"[Notifier] Listed {} file(s) in warren {}",
|
||||
files.len(),
|
||||
warren.name()
|
||||
);
|
||||
}
|
||||
|
||||
async fn warren_directory_created(&self, warren: &Warren, path: &FilePath) {
|
||||
log::debug!(
|
||||
"[Notifier] Created directory {} in warren {}",
|
||||
path,
|
||||
warren.name()
|
||||
);
|
||||
}
|
||||
|
||||
async fn warren_directory_deleted(&self, warren: &Warren, path: &FilePath) {
|
||||
log::debug!(
|
||||
"[Notifier] Deleted directory {} in warren {}",
|
||||
path,
|
||||
warren.name()
|
||||
);
|
||||
}
|
||||
|
||||
async fn warren_file_uploaded(&self, warren: &Warren, path: &FilePath) {
|
||||
log::debug!(
|
||||
"[Notifier] Uploaded file {} to warren {}",
|
||||
path,
|
||||
warren.name()
|
||||
);
|
||||
}
|
||||
|
||||
async fn warren_files_uploaded(&self, warren: &Warren, paths: &[FilePath]) {
|
||||
log::debug!(
|
||||
"[Notifier] Uploaded {} file(s) to warren {}",
|
||||
paths.len(),
|
||||
warren.name()
|
||||
);
|
||||
}
|
||||
|
||||
async fn warren_file_deleted(&self, warren: &Warren, path: &FilePath) {
|
||||
log::debug!(
|
||||
"[Notifier] Deleted file {} from warren {}",
|
||||
path,
|
||||
warren.name(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSystemNotifier for NotifierDebugLogger {
|
||||
async fn files_listed(&self, files: &Vec<File>) {
|
||||
log::debug!("[Notifier] Listed {} file(s)", files.len());
|
||||
}
|
||||
|
||||
async fn directory_created(&self, path: &FilePath) {
|
||||
log::debug!("[Notifier] Created directory {}", path);
|
||||
}
|
||||
|
||||
async fn directory_deleted(&self, path: &FilePath) {
|
||||
log::debug!("[Notifier] Deleted directory {}", path);
|
||||
}
|
||||
|
||||
async fn file_created(&self, path: &FilePath) {
|
||||
log::debug!("[Notifier] Created file {}", path);
|
||||
}
|
||||
|
||||
async fn file_deleted(&self, path: &FilePath) {
|
||||
log::debug!("[Notifier] Deleted file {}", path);
|
||||
}
|
||||
}
|
||||
148
backend/src/lib/outbound/postgres.rs
Normal file
148
backend/src/lib/outbound/postgres.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use sqlx::{
|
||||
ConnectOptions as _, Connection as _, PgConnection, PgPool,
|
||||
postgres::{PgConnectOptions, PgPoolOptions},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::domain::warren::{
|
||||
models::warren::{
|
||||
FetchWarrenError, FetchWarrenRequest, ListWarrensError, ListWarrensRequest, Warren,
|
||||
},
|
||||
ports::WarrenRepository,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PostgresConfig {
|
||||
database_url: String,
|
||||
database_name: String,
|
||||
}
|
||||
|
||||
impl PostgresConfig {
|
||||
pub fn new(database_url: String, database_name: String) -> Self {
|
||||
Self {
|
||||
database_url,
|
||||
database_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Postgres {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl Postgres {
|
||||
pub async fn new(config: PostgresConfig) -> anyhow::Result<Self> {
|
||||
let opts = PgConnectOptions::from_str(&config.database_url)?.disable_statement_logging();
|
||||
|
||||
let mut connection = PgConnection::connect_with(&opts)
|
||||
.await
|
||||
.context("Failed to connect to the PostgreSQL database")?;
|
||||
|
||||
// If this fails it's probably because the database already exists, which is exactly what
|
||||
// we want
|
||||
let _ = sqlx::query(&format!("CREATE DATABASE {}", config.database_name))
|
||||
.execute(&mut connection)
|
||||
.await;
|
||||
connection.close().await?;
|
||||
|
||||
let pool = PgPoolOptions::new()
|
||||
.connect_with(opts.database(&config.database_name))
|
||||
.await?;
|
||||
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||
|
||||
Ok(Self { pool })
|
||||
}
|
||||
|
||||
async fn get_warren(
|
||||
&self,
|
||||
connection: &mut PgConnection,
|
||||
id: &Uuid,
|
||||
) -> Result<Warren, sqlx::Error> {
|
||||
let warren: Warren = sqlx::query_as(
|
||||
"
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
warrens
|
||||
WHERE
|
||||
id = $1
|
||||
",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(connection)
|
||||
.await?;
|
||||
|
||||
Ok(warren)
|
||||
}
|
||||
|
||||
async fn list_warrens(
|
||||
&self,
|
||||
connection: &mut PgConnection,
|
||||
) -> Result<Vec<Warren>, sqlx::Error> {
|
||||
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Postgres, Warren>(
|
||||
"
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
warrens
|
||||
LIMIT
|
||||
50
|
||||
",
|
||||
)
|
||||
.fetch_all(&mut *connection)
|
||||
.await?;
|
||||
|
||||
Ok(warrens)
|
||||
}
|
||||
}
|
||||
|
||||
impl WarrenRepository for Postgres {
|
||||
async fn list_warrens(
|
||||
&self,
|
||||
_request: ListWarrensRequest,
|
||||
) -> Result<Vec<Warren>, ListWarrensError> {
|
||||
let mut connection = self
|
||||
.pool
|
||||
.acquire()
|
||||
.await
|
||||
.context("Failed to get a PostgreSQL connection")?;
|
||||
|
||||
let warrens = self
|
||||
.list_warrens(&mut connection)
|
||||
.await
|
||||
.map_err(|err| anyhow!(err).context("Failed to fetch warren with id"))?;
|
||||
|
||||
Ok(warrens)
|
||||
}
|
||||
|
||||
async fn fetch_warren(&self, request: FetchWarrenRequest) -> Result<Warren, FetchWarrenError> {
|
||||
let mut connection = self
|
||||
.pool
|
||||
.acquire()
|
||||
.await
|
||||
.context("Failed to get a PostgreSQL connection")?;
|
||||
|
||||
let warren = self
|
||||
.get_warren(&mut connection, request.id())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
if is_not_found_error(&err) {
|
||||
return FetchWarrenError::NotFound(request.id().clone());
|
||||
}
|
||||
|
||||
anyhow!(err)
|
||||
.context(format!("Failed to fetch warren with id {:?}", request.id()))
|
||||
.into()
|
||||
})?;
|
||||
|
||||
Ok(warren)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_not_found_error(err: &sqlx::Error) -> bool {
|
||||
matches!(err, sqlx::Error::RowNotFound)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
use std::env;
|
||||
|
||||
use axum::http::HeaderValue;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::{
|
||||
cors::{self, CorsLayer},
|
||||
services::ServeDir,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Result,
|
||||
api::{self, AppState},
|
||||
};
|
||||
|
||||
pub type Router = axum::Router<AppState>;
|
||||
|
||||
pub async fn start(pool: Pool<Postgres>) -> Result<()> {
|
||||
let cors_origin = env::var("CORS_ALLOW_ORIGIN").unwrap_or("*".to_string());
|
||||
let serve_dir = std::env::var("SERVE_DIRECTORY")?;
|
||||
|
||||
let mut app = Router::new()
|
||||
.nest("/api", api::router())
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_origin(cors::AllowOrigin::exact(HeaderValue::from_str(
|
||||
&cors_origin,
|
||||
)?))
|
||||
.allow_methods(cors::Any),
|
||||
)
|
||||
.with_state(AppState::new(pool, serve_dir));
|
||||
|
||||
if !cfg!(debug_assertions) {
|
||||
let frontend_path = env::var("STATIC_FRONTEND_DIR").unwrap_or("./frontend".to_string());
|
||||
let frontend_service = ServeDir::new(frontend_path);
|
||||
|
||||
log::debug!("Registering static frontend");
|
||||
app = app.fallback_service(frontend_service);
|
||||
}
|
||||
|
||||
let addr = env::var("BIND_ADDRESS").unwrap_or("127.0.0.1:8080".to_string());
|
||||
|
||||
let listener = TcpListener::bind(&addr).await?;
|
||||
|
||||
log::info!("Listening on {}", addr);
|
||||
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
pub mod db;
|
||||
/* pub mod db;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -92,4 +92,4 @@ fn build_path(serve_path: &str, warren_path: &str, rest_path: Option<&str>) -> P
|
||||
}
|
||||
|
||||
final_path
|
||||
}
|
||||
} */
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
} from '@/components/ui/context-menu';
|
||||
import { deleteWarrenEntry } from '~/lib/api/warrens';
|
||||
import { deleteWarrenDirectory, deleteWarrenFile } from '~/lib/api/warrens';
|
||||
import type { DirectoryEntry } from '~/types';
|
||||
|
||||
const route = useRoute();
|
||||
@@ -21,7 +21,11 @@ const deleting = ref(false);
|
||||
async function submitDelete() {
|
||||
deleting.value = true;
|
||||
|
||||
await deleteWarrenEntry(warrenRoute.value, entry.name, entry.fileType);
|
||||
if (entry.fileType === 'directory') {
|
||||
await deleteWarrenDirectory(warrenRoute.value, entry.name);
|
||||
} else {
|
||||
await deleteWarrenFile(warrenRoute.value, entry.name);
|
||||
}
|
||||
|
||||
deleting.value = false;
|
||||
}
|
||||
@@ -32,7 +36,13 @@ async function submitDelete() {
|
||||
<ContextMenuTrigger>
|
||||
<NuxtLink
|
||||
:to="joinPaths(route.path, entry.name)"
|
||||
:class="['select-none', { 'pointer-events-none': disabled }]"
|
||||
:class="[
|
||||
'select-none',
|
||||
{
|
||||
'pointer-events-none':
|
||||
disabled || entry.fileType === 'file',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Button
|
||||
class="w-44 h-12"
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { toast } from 'vue-sonner';
|
||||
import type { DirectoryEntry, FileType } from '~/types';
|
||||
import type { ApiResponse } from '~/types/api';
|
||||
import type { Warren } from '~/types/warrens';
|
||||
|
||||
export async function getWarrens(): Promise<Record<string, Warren>> {
|
||||
const { data: arr, error } = await useFetch<Warren[]>(
|
||||
const { data, error } = await useFetch<ApiResponse<{ warrens: Warren[] }>>(
|
||||
getApiUrl('warrens'),
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
);
|
||||
|
||||
if (arr.value == null) {
|
||||
if (data.value == null) {
|
||||
throw error.value?.name;
|
||||
}
|
||||
|
||||
const warrens: Record<string, Warren> = {};
|
||||
|
||||
for (const warren of arr.value) {
|
||||
for (const warren of data.value.data.warrens) {
|
||||
warrens[warren.id] = warren;
|
||||
}
|
||||
|
||||
@@ -30,23 +31,31 @@ export async function getWarrenDirectory(
|
||||
let [warrenId, rest] = splitOnce(path, '/');
|
||||
|
||||
if (rest == null) {
|
||||
rest = '';
|
||||
rest = '/';
|
||||
} else {
|
||||
rest = '/' + rest;
|
||||
rest = '/' + decodeURI(rest);
|
||||
}
|
||||
|
||||
const { data: entries, error } = await useFetch<DirectoryEntry[]>(
|
||||
getApiUrl(`warrens/${warrenId}/get${rest}`),
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
);
|
||||
const { data, error } = await useFetch<
|
||||
ApiResponse<{ files: DirectoryEntry[] }>
|
||||
>(getApiUrl(`warrens/files`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
warrenId,
|
||||
path: rest,
|
||||
}),
|
||||
});
|
||||
|
||||
if (entries.value == null) {
|
||||
if (data.value == null) {
|
||||
throw error.value?.name;
|
||||
}
|
||||
|
||||
return entries.value;
|
||||
const { files } = data.value.data;
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
export async function createDirectory(
|
||||
@@ -57,17 +66,23 @@ export async function createDirectory(
|
||||
let [warrenId, rest] = splitOnce(path, '/');
|
||||
|
||||
if (rest == null) {
|
||||
rest = '';
|
||||
rest = '/';
|
||||
} else {
|
||||
rest += '/';
|
||||
rest = '/' + rest + '/';
|
||||
}
|
||||
|
||||
const { status } = await useFetch(
|
||||
getApiUrl(`warrens/${warrenId}/create/${rest}${directoryName}`),
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
rest += directoryName;
|
||||
|
||||
const { status } = await useFetch(getApiUrl(`warrens/files/directory`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
warrenId,
|
||||
path: rest,
|
||||
}),
|
||||
});
|
||||
|
||||
if (status.value !== 'success') {
|
||||
toast.error('Directory', {
|
||||
@@ -87,44 +102,93 @@ export async function createDirectory(
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export async function deleteWarrenEntry(
|
||||
export async function deleteWarrenDirectory(
|
||||
path: string,
|
||||
directoryName: string,
|
||||
fileType: FileType
|
||||
directoryName: string
|
||||
): Promise<{ success: boolean }> {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [warrenId, rest] = splitOnce(path, '/');
|
||||
|
||||
if (rest == null) {
|
||||
rest = '';
|
||||
rest = '/';
|
||||
} else {
|
||||
rest = '/' + rest;
|
||||
rest = '/' + rest + '/';
|
||||
}
|
||||
|
||||
const { status } = await useFetch(
|
||||
getApiUrl(
|
||||
`warrens/${warrenId}/delete${rest}/${directoryName}?fileType=${fileType}`
|
||||
),
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
rest += directoryName;
|
||||
|
||||
const toastTitle = fileType.slice(0, 1).toUpperCase() + fileType.slice(1);
|
||||
const { status } = await useFetch(getApiUrl(`warrens/files/directory`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
warrenId,
|
||||
path: rest,
|
||||
force: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const TOAST_TITLE = 'Directory';
|
||||
|
||||
if (status.value !== 'success') {
|
||||
toast.error(toastTitle, {
|
||||
toast.error(TOAST_TITLE, {
|
||||
id: 'DELETE_DIRECTORY_TOAST',
|
||||
description: `Failed to delete ${fileType}`,
|
||||
description: `Failed to delete directory`,
|
||||
});
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
await refreshNuxtData('current-directory');
|
||||
|
||||
toast.success(toastTitle, {
|
||||
toast.success(TOAST_TITLE, {
|
||||
id: 'DELETE_DIRECTORY_TOAST',
|
||||
description: `Successfully deleted ${fileType}: ${directoryName}`,
|
||||
description: `Successfully deleted ${directoryName}`,
|
||||
});
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export async function deleteWarrenFile(
|
||||
path: string,
|
||||
fileName: string
|
||||
): Promise<{ success: boolean }> {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [warrenId, rest] = splitOnce(path, '/');
|
||||
|
||||
if (rest == null) {
|
||||
rest = '/';
|
||||
} else {
|
||||
rest = '/' + rest + '/';
|
||||
}
|
||||
|
||||
rest += fileName;
|
||||
|
||||
const { status } = await useFetch(getApiUrl(`warrens/files/file`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
warrenId,
|
||||
path: rest,
|
||||
}),
|
||||
});
|
||||
|
||||
const TOAST_TITLE = 'File';
|
||||
|
||||
if (status.value !== 'success') {
|
||||
toast.error(TOAST_TITLE, {
|
||||
id: 'DELETE_FILE_TOAST',
|
||||
description: `Failed to delete file`,
|
||||
});
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
await refreshNuxtData('current-directory');
|
||||
|
||||
toast.success(TOAST_TITLE, {
|
||||
id: 'DELETE_FILE_TOAST',
|
||||
description: `Successfully deleted ${fileName}`,
|
||||
});
|
||||
return { success: true };
|
||||
}
|
||||
@@ -134,22 +198,17 @@ export async function uploadToWarren(
|
||||
files: FileList,
|
||||
onProgress: ((loaded: number, total: number) => void) | undefined
|
||||
): Promise<{ success: boolean }> {
|
||||
const body = new FormData();
|
||||
for (const file of files) {
|
||||
body.append('files', file);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [warrenId, rest] = splitOnce(path, '/');
|
||||
|
||||
if (rest == null) {
|
||||
rest = '';
|
||||
rest = '/';
|
||||
} else {
|
||||
rest = '/' + rest;
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', getApiUrl(`warrens/${warrenId}/upload${rest}`));
|
||||
xhr.open('POST', getApiUrl(`warrens/files/upload`));
|
||||
xhr.upload.onprogress = (e) => {
|
||||
onProgress?.(e.loaded, e.total);
|
||||
};
|
||||
@@ -160,7 +219,7 @@ export async function uploadToWarren(
|
||||
return;
|
||||
}
|
||||
|
||||
if (xhr.status === 200) {
|
||||
if (xhr.status >= 200 && xhr.status <= 300) {
|
||||
res();
|
||||
} else {
|
||||
rej();
|
||||
@@ -168,6 +227,13 @@ export async function uploadToWarren(
|
||||
};
|
||||
});
|
||||
|
||||
const body = new FormData();
|
||||
body.set('warrenId', warrenId);
|
||||
body.set('path', rest);
|
||||
for (const file of files) {
|
||||
body.append('files', file);
|
||||
}
|
||||
|
||||
xhr.send(body);
|
||||
|
||||
try {
|
||||
|
||||
4
frontend/types/api.ts
Normal file
4
frontend/types/api.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type ApiResponse<T> = {
|
||||
status: number;
|
||||
data: T;
|
||||
};
|
||||
Reference in New Issue
Block a user