split backend into bin and lib
This commit is contained in:
10
backend/src/lib/api/mod.rs
Normal file
10
backend/src/lib/api/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use crate::server::Router;
|
||||
|
||||
mod state;
|
||||
mod warrens;
|
||||
|
||||
pub use state::AppState;
|
||||
|
||||
pub(super) fn router() -> Router {
|
||||
Router::new().nest("/warrens", warrens::router())
|
||||
}
|
||||
21
backend/src/lib/api/state.rs
Normal file
21
backend/src/lib/api/state.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
24
backend/src/lib/api/warrens/create_directory.rs
Normal file
24
backend/src/lib/api/warrens/create_directory.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
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(())
|
||||
}
|
||||
31
backend/src/lib/api/warrens/delete_directory.rs
Normal file
31
backend/src/lib/api/warrens/delete_directory.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
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(())
|
||||
}
|
||||
27
backend/src/lib/api/warrens/get_warren_path.rs
Normal file
27
backend/src/lib/api/warrens/get_warren_path.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
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))
|
||||
}
|
||||
9
backend/src/lib/api/warrens/list_warrens.rs
Normal file
9
backend/src/lib/api/warrens/list_warrens.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
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))
|
||||
}
|
||||
34
backend/src/lib/api/warrens/mod.rs
Normal file
34
backend/src/lib/api/warrens/mod.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
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)),
|
||||
)
|
||||
}
|
||||
34
backend/src/lib/api/warrens/upload_files.rs
Normal file
34
backend/src/lib/api/warrens/upload_files.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
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(())
|
||||
}
|
||||
28
backend/src/lib/db/mod.rs
Normal file
28
backend/src/lib/db/mod.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
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)
|
||||
}
|
||||
48
backend/src/lib/error.rs
Normal file
48
backend/src/lib/error.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
56
backend/src/lib/fs/dir.rs
Normal file
56
backend/src/lib/fs/dir.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
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(())
|
||||
}
|
||||
29
backend/src/lib/fs/file.rs
Normal file
29
backend/src/lib/fs/file.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
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(())
|
||||
}
|
||||
38
backend/src/lib/fs/mod.rs
Normal file
38
backend/src/lib/fs/mod.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
10
backend/src/lib/lib.rs
Normal file
10
backend/src/lib/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use error::AppError;
|
||||
|
||||
pub mod api;
|
||||
pub mod db;
|
||||
pub mod error;
|
||||
pub mod fs;
|
||||
pub mod server;
|
||||
pub mod warrens;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AppError>;
|
||||
50
backend/src/lib/server.rs
Normal file
50
backend/src/lib/server.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
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(())
|
||||
}
|
||||
49
backend/src/lib/warrens/db.rs
Normal file
49
backend/src/lib/warrens/db.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use sqlx::{Acquire, PgExecutor, Postgres};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::Result;
|
||||
|
||||
use super::Warren;
|
||||
|
||||
impl Warren {
|
||||
pub async fn get<'c, E>(e: E, id: &Uuid) -> Result<Self>
|
||||
where
|
||||
E: PgExecutor<'c> + Acquire<'c, Database = Postgres>,
|
||||
{
|
||||
let warren: Self = sqlx::query_as(
|
||||
"
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
warrens
|
||||
WHERE
|
||||
id = $1
|
||||
",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(e)
|
||||
.await?;
|
||||
|
||||
Ok(warren)
|
||||
}
|
||||
|
||||
pub async fn list<'c, E>(e: E) -> Result<Vec<Self>>
|
||||
where
|
||||
E: PgExecutor<'c> + Acquire<'c, Database = Postgres>,
|
||||
{
|
||||
let warren: Vec<Self> = sqlx::query_as(
|
||||
"
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
warrens
|
||||
LIMIT
|
||||
50
|
||||
",
|
||||
)
|
||||
.fetch_all(e)
|
||||
.await?;
|
||||
|
||||
Ok(warren)
|
||||
}
|
||||
}
|
||||
95
backend/src/lib/warrens/mod.rs
Normal file
95
backend/src/lib/warrens/mod.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
pub mod db;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Serialize;
|
||||
use sqlx::prelude::FromRow;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
Result,
|
||||
fs::{
|
||||
DirectoryEntry, FileType, create_dir, delete_dir, delete_file, get_dir_entries, write_file,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, FromRow)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Warren {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
#[serde(skip)]
|
||||
path: String,
|
||||
}
|
||||
|
||||
impl Warren {
|
||||
pub fn id(&self) -> &Uuid {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Warren {
|
||||
pub async fn read_path(
|
||||
&self,
|
||||
serve_path: &str,
|
||||
path: Option<String>,
|
||||
) -> Result<Vec<DirectoryEntry>> {
|
||||
let path = build_path(serve_path, &self.path, path.as_deref());
|
||||
|
||||
get_dir_entries(path).await
|
||||
}
|
||||
|
||||
pub async fn create_directory(&self, serve_path: &str, path: String) -> Result<()> {
|
||||
let path = build_path(serve_path, &self.path, Some(&path));
|
||||
|
||||
create_dir(path).await
|
||||
}
|
||||
|
||||
pub async fn delete_entry(
|
||||
&self,
|
||||
serve_path: &str,
|
||||
path: String,
|
||||
file_type: FileType,
|
||||
) -> Result<()> {
|
||||
let path = build_path(serve_path, &self.path, Some(&path));
|
||||
|
||||
match file_type {
|
||||
FileType::File => delete_file(path).await,
|
||||
FileType::Directory => delete_dir(path).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upload(
|
||||
&self,
|
||||
serve_path: &str,
|
||||
rest_path: Option<&str>,
|
||||
file_name: &str,
|
||||
data: &[u8],
|
||||
) -> Result<()> {
|
||||
let rest = format!("{}/{file_name}", rest_path.unwrap_or(""));
|
||||
|
||||
let path = build_path(serve_path, &self.path, Some(&rest));
|
||||
|
||||
write_file(path, data).await
|
||||
}
|
||||
}
|
||||
|
||||
fn build_path(serve_path: &str, warren_path: &str, rest_path: Option<&str>) -> PathBuf {
|
||||
let mut final_path = PathBuf::from(serve_path);
|
||||
|
||||
final_path.push(warren_path.strip_prefix("/").unwrap_or(warren_path));
|
||||
|
||||
if let Some(ref rest) = rest_path {
|
||||
final_path.push(rest.strip_prefix("/").unwrap_or(rest));
|
||||
}
|
||||
|
||||
final_path
|
||||
}
|
||||
Reference in New Issue
Block a user