completely refactor the backend
This commit is contained in:
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)
|
||||
}
|
||||
Reference in New Issue
Block a user