refactor: rewrite
This commit is contained in:
parent
eef5cd8e02
commit
4dd9013521
File diff suppressed because it is too large
Load Diff
48
Cargo.toml
48
Cargo.toml
|
@ -1,8 +1,40 @@
|
||||||
[workspace]
|
[package]
|
||||||
members = [
|
name = "homedisk"
|
||||||
"core",
|
authors = ["MedzikUser <medzik@duck.com>"]
|
||||||
"database",
|
homepage = "https://github.com/HomeDisk/cloud"
|
||||||
"server",
|
repository = "https://github.com/HomeDisk/cloud"
|
||||||
"types",
|
license = "GPL-3.0-or-later"
|
||||||
]
|
version = "0.0.0-dev"
|
||||||
resolver = "2"
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Types
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
# Errors
|
||||||
|
anyhow = "1.0"
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
# Logger
|
||||||
|
tracing = { version = "0.1", features = ["max_level_debug", "release_max_level_info", "log"] }
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
# Backtrace in panic hook
|
||||||
|
backtrace = "0.3"
|
||||||
|
|
||||||
|
# Tokio runtime
|
||||||
|
tokio = { version = "1.20", features = ["rt-multi-thread", "macros"] }
|
||||||
|
|
||||||
|
# Database
|
||||||
|
futures-util = "0.3"
|
||||||
|
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "sqlite"] }
|
||||||
|
uuid = { version = "1.1", features = ["v4"] }
|
||||||
|
|
||||||
|
# Cryptographic
|
||||||
|
crypto-utils = "0.4"
|
||||||
|
hex = "0.4"
|
||||||
|
|
||||||
|
# Config
|
||||||
|
toml = "0.5"
|
||||||
|
|
||||||
|
# HTTP server
|
||||||
|
axum = { version = "0.6.0-rc.2", features = ["http2", "multipart"] }
|
||||||
|
byte-unit = "4.0.14"
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "homedisk"
|
|
||||||
authors = ["MedzikUser <medzik@duck.com>"]
|
|
||||||
homepage = "https://github.com/HomeDisk/cloud"
|
|
||||||
repository = "https://github.com/HomeDisk/cloud"
|
|
||||||
license = "GPL-3.0-or-later"
|
|
||||||
version = "0.0.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "homedisk"
|
|
||||||
path = "./src/main.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = "1.0.62"
|
|
||||||
better-panic = "0.3.0"
|
|
||||||
tracing-subscriber = "0.3.15"
|
|
||||||
tracing = { version = "0.1.36", features = ["max_level_debug", "release_max_level_info"] }
|
|
||||||
tokio = { version = "1.20.1", features = ["rt-multi-thread", "macros"] }
|
|
||||||
homedisk-database = { path = "../database" }
|
|
||||||
homedisk-server = { path = "../server" }
|
|
||||||
homedisk-types = { path = "../types", features = ["config"] }
|
|
|
@ -1,36 +0,0 @@
|
||||||
//! # HomeDisk cloud server
|
|
||||||
//!
|
|
||||||
//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
|
|
||||||
//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
|
|
||||||
//! [total-lines]: https://img.shields.io/tokei/lines/github/MedzikUser/HomeDisk?style=for-the-badge&logo=github&color=fede00
|
|
||||||
//! [code-size]: https://img.shields.io/github/languages/code-size/MedzikUser/HomeDisk?style=for-the-badge&color=c8df52&logo=github
|
|
||||||
//! [ci]: https://img.shields.io/github/workflow/status/MedzikUser/HomeDisk/Rust/main?style=for-the-badge
|
|
||||||
//!
|
|
||||||
//! [home-screenshot]: https://i.imgur.com/x4Glw7w.png
|
|
||||||
//! [login-screenshot]: https://i.imgur.com/KpwY4nb.png
|
|
||||||
//!
|
|
||||||
//! [![github]](https://github.com/MedzikUser/HomeDisk)
|
|
||||||
//! [![docs-rs]](https://homedisk-doc.vercel.app)
|
|
||||||
//! [![total-lines]](https://github.com/MedzikUser/HomeDisk)
|
|
||||||
//! [![code-size]](https://github.com/MedzikUser/HomeDisk)
|
|
||||||
//! [![ci]](https://github.com/MedzikUser/HomeDisk/actions/workflows/rust.yml)
|
|
||||||
//!
|
|
||||||
//! ![home-screenshot]
|
|
||||||
//! ![login-screenshot]
|
|
||||||
//!
|
|
||||||
//! ## 👨💻 Building
|
|
||||||
//!
|
|
||||||
//! First clone the repository: `git clone https://github.com/MedzikUser/HomeDisk.git`
|
|
||||||
//!
|
|
||||||
//! ### Requirements
|
|
||||||
//! - [Rust](https://rust-lang.org)
|
|
||||||
//!
|
|
||||||
//! To build run the command: `cargo build --release`
|
|
||||||
//!
|
|
||||||
//! The compiled binary can be found in `./target/release/homedisk`
|
|
||||||
//!
|
|
||||||
//! ## Configure
|
|
||||||
//!
|
|
||||||
//! Go to [config](homedisk_types::config) module
|
|
||||||
|
|
||||||
#![doc(html_root_url = "https://homedisk-doc.medzik.xyz")]
|
|
|
@ -1,17 +0,0 @@
|
||||||
use tracing::level_filters::LevelFilter;
|
|
||||||
|
|
||||||
// Max Logger Level on debug build
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
const MAX_LEVEL: LevelFilter = LevelFilter::DEBUG;
|
|
||||||
|
|
||||||
// Max Logger Level on release build
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
const MAX_LEVEL: LevelFilter = LevelFilter::INFO;
|
|
||||||
|
|
||||||
pub fn init() {
|
|
||||||
// initialize better_panic
|
|
||||||
better_panic::install();
|
|
||||||
|
|
||||||
// initialize tracing
|
|
||||||
tracing_subscriber::fmt().with_max_level(MAX_LEVEL).init();
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
use std::{fs::File, path::Path};
|
|
||||||
|
|
||||||
use homedisk_database::Database;
|
|
||||||
use homedisk_server::serve_http;
|
|
||||||
use homedisk_types::config::Config;
|
|
||||||
use tracing::{info, warn};
|
|
||||||
|
|
||||||
mod logger;
|
|
||||||
|
|
||||||
pub const DATABASE_FILE: &str = "homedisk.db";
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
logger::init();
|
|
||||||
|
|
||||||
let config = Config::parse().expect("Failed to parse configuration file");
|
|
||||||
|
|
||||||
// open database connection
|
|
||||||
let db =
|
|
||||||
// if database file doesn't exists create it
|
|
||||||
if !Path::new(DATABASE_FILE).exists() {
|
|
||||||
warn!("Database file doesn't exists.");
|
|
||||||
info!("Creating database file...");
|
|
||||||
|
|
||||||
// create an empty database file
|
|
||||||
File::create(DATABASE_FILE).expect("Failed to create a database file");
|
|
||||||
|
|
||||||
// open database file
|
|
||||||
let db = Database::open(DATABASE_FILE)
|
|
||||||
.await
|
|
||||||
.expect("Failed to open database file");
|
|
||||||
|
|
||||||
// create tables in the database
|
|
||||||
db.create_tables()
|
|
||||||
.await
|
|
||||||
.expect("Failed to create tables in the database");
|
|
||||||
|
|
||||||
db
|
|
||||||
}
|
|
||||||
// if database file exists
|
|
||||||
else {
|
|
||||||
Database::open(DATABASE_FILE)
|
|
||||||
.await
|
|
||||||
.expect("Failed to open database file")
|
|
||||||
};
|
|
||||||
|
|
||||||
// change the type from Vec<String> to Vec<HeaderValue> so that the http server can correctly detect CORS hosts
|
|
||||||
let origins = config
|
|
||||||
.http
|
|
||||||
.cors
|
|
||||||
.iter()
|
|
||||||
.map(|e| e.parse().expect("parse CORS hosts"))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// format host ip and port
|
|
||||||
let host = format!(
|
|
||||||
"{host}:{port}",
|
|
||||||
host = config.http.host,
|
|
||||||
port = config.http.port
|
|
||||||
);
|
|
||||||
|
|
||||||
serve_http(host, origins, db, config)
|
|
||||||
.await
|
|
||||||
.expect("start http server");
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "homedisk-database"
|
|
||||||
version = "0.0.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
tracing = "0.1.36"
|
|
||||||
futures-util = "0.3.23"
|
|
||||||
sqlx = { version = "0.6.1", features = ["runtime-tokio-rustls", "sqlite"] }
|
|
||||||
uuid = { version = "1.1.2", features = ["v5"] }
|
|
||||||
homedisk-types = { path = "../types", features = ["database"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tokio = { version = "1.20.1", features = ["rt-multi-thread", "macros"] }
|
|
|
@ -1,9 +0,0 @@
|
||||||
#![doc(html_root_url = "https://homedisk-doc.medzik.xyz")]
|
|
||||||
|
|
||||||
mod sqlite;
|
|
||||||
|
|
||||||
pub use homedisk_types::{
|
|
||||||
database::User,
|
|
||||||
errors::{DatabaseError as Error, DatabaseResult as Result},
|
|
||||||
};
|
|
||||||
pub use sqlite::*;
|
|
|
@ -1,286 +0,0 @@
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use futures_util::TryStreamExt;
|
|
||||||
use sqlx::{
|
|
||||||
sqlite::{SqliteConnectOptions, SqliteQueryResult},
|
|
||||||
ConnectOptions, Executor, Row, SqlitePool,
|
|
||||||
};
|
|
||||||
use tracing::{debug, log::LevelFilter};
|
|
||||||
|
|
||||||
use super::{Error, Result, User};
|
|
||||||
|
|
||||||
/// SQL Database
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Database {
|
|
||||||
/// SQLite Connection Pool
|
|
||||||
pub conn: SqlitePool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Database {
|
|
||||||
/// Open a SQLite database
|
|
||||||
/// ```no_run
|
|
||||||
/// # async fn foo() -> homedisk_database::Result<()> {
|
|
||||||
/// use homedisk_database::Database;
|
|
||||||
///
|
|
||||||
/// // open database in memory
|
|
||||||
/// Database::open("sqlite::memory:").await?;
|
|
||||||
///
|
|
||||||
/// // open database from file
|
|
||||||
/// Database::open("path/to/database.db").await?;
|
|
||||||
///
|
|
||||||
/// # Ok(()) }
|
|
||||||
/// ```
|
|
||||||
pub async fn open(path: &str) -> Result<Self> {
|
|
||||||
debug!("Opening SQLite database");
|
|
||||||
|
|
||||||
// sqlite connection options
|
|
||||||
let mut options = SqliteConnectOptions::from_str(path).map_err(Error::OpenDatabase)?;
|
|
||||||
|
|
||||||
// set log level to Debug
|
|
||||||
options.log_statements(LevelFilter::Debug);
|
|
||||||
|
|
||||||
// create a database pool
|
|
||||||
let conn = SqlitePool::connect_with(options)
|
|
||||||
.await
|
|
||||||
.map_err(Error::ConnectDatabase)?;
|
|
||||||
|
|
||||||
// return `Database`
|
|
||||||
Ok(Self { conn })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create all required tabled for HomeDisk
|
|
||||||
/// ```
|
|
||||||
/// # async fn foo() -> homedisk_database::Result<()> {
|
|
||||||
/// # let db = homedisk_database::Database::open("sqlite::memory:").await?;
|
|
||||||
/// db.create_tables().await?;
|
|
||||||
///
|
|
||||||
/// # Ok(()) }
|
|
||||||
/// ```
|
|
||||||
pub async fn create_tables(&self) -> Result<SqliteQueryResult> {
|
|
||||||
let query = sqlx::query(include_str!("../../tables.sql"));
|
|
||||||
|
|
||||||
self.conn.execute(query).await.map_err(Error::Execute)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new User
|
|
||||||
/// ```
|
|
||||||
/// # async fn foo() -> homedisk_database::Result<()> {
|
|
||||||
/// # let db = homedisk_database::Database::open("sqlite::memory:").await?;
|
|
||||||
/// # db.create_tables().await?;
|
|
||||||
/// use homedisk_database::User;
|
|
||||||
///
|
|
||||||
/// // create `User` type
|
|
||||||
/// let user = User::new("username", "password");
|
|
||||||
///
|
|
||||||
/// // create a user in database
|
|
||||||
/// db.create_user(&user).await?;
|
|
||||||
///
|
|
||||||
/// # Ok(()) }
|
|
||||||
/// ```
|
|
||||||
pub async fn create_user(&self, user: &User) -> Result<SqliteQueryResult> {
|
|
||||||
debug!("Creating user - {}", user.username);
|
|
||||||
|
|
||||||
// insert user to a database
|
|
||||||
let query = sqlx::query("INSERT INTO user (id, username, password) VALUES (?, ?, ?)")
|
|
||||||
.bind(&user.id)
|
|
||||||
.bind(&user.username)
|
|
||||||
.bind(&user.password);
|
|
||||||
|
|
||||||
// execute query and return output
|
|
||||||
self.conn.execute(query).await.map_err(Error::Execute)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Search for a user
|
|
||||||
/// ```
|
|
||||||
/// # async fn foo() -> homedisk_database::Result<()> {
|
|
||||||
/// # let db = homedisk_database::Database::open("sqlite::memory:").await?;
|
|
||||||
/// # db.create_tables().await?;
|
|
||||||
/// use homedisk_database::User;
|
|
||||||
///
|
|
||||||
/// // create `User` type
|
|
||||||
/// let user = User::new("username", "password");
|
|
||||||
///
|
|
||||||
/// # db.create_user(&user).await?;
|
|
||||||
/// db.find_user(&user).await?;
|
|
||||||
///
|
|
||||||
/// # Ok(()) }
|
|
||||||
/// ```
|
|
||||||
pub async fn find_user(&self, user: &User) -> Result<User> {
|
|
||||||
debug!("Searching for a user - {}", user.username);
|
|
||||||
|
|
||||||
// create query request to database
|
|
||||||
let query =
|
|
||||||
sqlx::query_as::<_, User>("SELECT * FROM user WHERE username = ? AND password = ?")
|
|
||||||
.bind(&user.username)
|
|
||||||
.bind(&user.password);
|
|
||||||
|
|
||||||
// fetch query
|
|
||||||
let mut stream = self.conn.fetch(query);
|
|
||||||
|
|
||||||
// get rows from query
|
|
||||||
let row = stream
|
|
||||||
.try_next()
|
|
||||||
.await
|
|
||||||
.map_err(Error::Execute)?
|
|
||||||
.ok_or(Error::UserNotFound)?;
|
|
||||||
|
|
||||||
// get `id` row
|
|
||||||
let id = row.try_get("id").map_err(Error::GetRow)?;
|
|
||||||
// get `username` row
|
|
||||||
let username = row.try_get("username").map_err(Error::GetRow)?;
|
|
||||||
// get `password` row
|
|
||||||
let password = row.try_get("password").map_err(Error::GetRow)?;
|
|
||||||
|
|
||||||
// return `User`
|
|
||||||
Ok(User {
|
|
||||||
id,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Search for a user by UUID
|
|
||||||
/// ```
|
|
||||||
/// # async fn foo() -> homedisk_database::Result<()> {
|
|
||||||
/// # let db = homedisk_database::Database::open("sqlite::memory:").await?;
|
|
||||||
/// # db.create_tables().await?;
|
|
||||||
/// use homedisk_database::User;
|
|
||||||
///
|
|
||||||
/// // create `User` type
|
|
||||||
/// let user = User::new("username", "password");
|
|
||||||
///
|
|
||||||
/// # db.create_user(&user).await?;
|
|
||||||
/// db.find_user_by_id(&user.id).await?;
|
|
||||||
///
|
|
||||||
/// # Ok(()) }
|
|
||||||
/// ```
|
|
||||||
pub async fn find_user_by_id(&self, id: &str) -> Result<User> {
|
|
||||||
debug!("Searching for a user by UUID - {}", id);
|
|
||||||
|
|
||||||
// create query request to database
|
|
||||||
let query = sqlx::query_as::<_, User>("SELECT * FROM user WHERE id = ?").bind(id);
|
|
||||||
|
|
||||||
// fetch query
|
|
||||||
let mut stream = self.conn.fetch(query);
|
|
||||||
|
|
||||||
// get rows from query
|
|
||||||
let row = stream
|
|
||||||
.try_next()
|
|
||||||
.await
|
|
||||||
.map_err(Error::Execute)?
|
|
||||||
.ok_or(Error::UserNotFound)?;
|
|
||||||
|
|
||||||
// get `id` row
|
|
||||||
let id = row.try_get("id").map_err(Error::GetRow)?;
|
|
||||||
// get `username` row
|
|
||||||
let username = row.try_get("username").map_err(Error::GetRow)?;
|
|
||||||
// get `password` row
|
|
||||||
let password = row.try_get("password").map_err(Error::GetRow)?;
|
|
||||||
|
|
||||||
// return `User`
|
|
||||||
Ok(User {
|
|
||||||
id,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::{Database, User};
|
|
||||||
|
|
||||||
const USERNAME: &str = "medzik";
|
|
||||||
const PASSWORD: &str = "SuperSecretPassword123!";
|
|
||||||
|
|
||||||
/// Utils to open database in tests
|
|
||||||
async fn open_db() -> Database {
|
|
||||||
Database::open("sqlite::memory:").await.expect("open db")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utils to create a new user in tests
|
|
||||||
async fn new_user(db: &Database) {
|
|
||||||
// create tables
|
|
||||||
db.create_tables().await.expect("create tables");
|
|
||||||
|
|
||||||
// create new user
|
|
||||||
let user = User::new(USERNAME, PASSWORD);
|
|
||||||
db.create_user(&user).await.expect("create user");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test a create user
|
|
||||||
#[tokio::test]
|
|
||||||
async fn create_user() {
|
|
||||||
let db = open_db().await;
|
|
||||||
new_user(&db).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test a search for a user
|
|
||||||
#[tokio::test]
|
|
||||||
async fn find_user() {
|
|
||||||
let db = open_db().await;
|
|
||||||
|
|
||||||
new_user(&db).await;
|
|
||||||
|
|
||||||
let user = User::new(USERNAME, PASSWORD);
|
|
||||||
|
|
||||||
let user = db.find_user(&user).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(user.username, USERNAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test a search for a user by id
|
|
||||||
#[tokio::test]
|
|
||||||
async fn find_user_by_id() {
|
|
||||||
let db = open_db().await;
|
|
||||||
new_user(&db).await;
|
|
||||||
|
|
||||||
let user = User::new(USERNAME, PASSWORD);
|
|
||||||
|
|
||||||
let user = db.find_user_by_id(&user.id).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(user.username, USERNAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test a search for a user with an invalid password to see if the user is returned (it shouldn't be)
|
|
||||||
#[tokio::test]
|
|
||||||
async fn find_user_wrong_password() {
|
|
||||||
let db = open_db().await;
|
|
||||||
|
|
||||||
new_user(&db).await;
|
|
||||||
|
|
||||||
let user = User::new(USERNAME, "wrong password 123!");
|
|
||||||
|
|
||||||
let err = db.find_user(&user).await.unwrap_err();
|
|
||||||
|
|
||||||
assert_eq!(err.to_string(), "user not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test a search for a user who doesn't exist
|
|
||||||
#[tokio::test]
|
|
||||||
async fn find_user_wrong_username() {
|
|
||||||
let db = open_db().await;
|
|
||||||
|
|
||||||
new_user(&db).await;
|
|
||||||
|
|
||||||
let user = User::new("not_exists_user", PASSWORD);
|
|
||||||
|
|
||||||
let err = db.find_user(&user).await.unwrap_err();
|
|
||||||
|
|
||||||
assert_eq!(err.to_string(), "user not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test a search for a user by UUID who doesn't exist
|
|
||||||
#[tokio::test]
|
|
||||||
async fn find_user_wrong_id() {
|
|
||||||
let db = open_db().await;
|
|
||||||
|
|
||||||
new_user(&db).await;
|
|
||||||
|
|
||||||
let other_user = User::new("not_exists_user", "my secret passphrase");
|
|
||||||
|
|
||||||
let err = db.find_user_by_id(&other_user.id).await.unwrap_err();
|
|
||||||
|
|
||||||
assert_eq!(err.to_string(), "user not found")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -22,4 +22,4 @@
|
||||||
"automerge": true
|
"automerge": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "homedisk-server"
|
|
||||||
version = "0.0.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
axum = { version = "0.5.15", features = ["multipart"] }
|
|
||||||
axum-auth = "0.3.0"
|
|
||||||
base64 = "0.13.0"
|
|
||||||
byte-unit = "4.0.14"
|
|
||||||
futures = "0.3.23"
|
|
||||||
hyper = { version = "0.14.20", features = ["full"] }
|
|
||||||
log = "0.4.17"
|
|
||||||
crypto-utils = { version = "0.4.0", features = ["jwt"] }
|
|
||||||
serde = { version = "1.0.144", features = ["derive"] }
|
|
||||||
thiserror = "1.0.32"
|
|
||||||
tower-http = { version = "0.3.4", features = ["full"] }
|
|
||||||
homedisk-database = { path = "../database" }
|
|
||||||
homedisk-types = { path = "../types", features = ["axum"] }
|
|
|
@ -1,46 +0,0 @@
|
||||||
use axum::{extract::rejection::JsonRejection, Extension, Json};
|
|
||||||
use homedisk_database::{Database, Error, User};
|
|
||||||
use homedisk_types::{
|
|
||||||
auth::login::{Request, Response},
|
|
||||||
config::Config,
|
|
||||||
errors::{AuthError, ServerError},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::middleware::{create_token, validate_json};
|
|
||||||
|
|
||||||
pub async fn handle(
|
|
||||||
Extension(db): Extension<Database>,
|
|
||||||
Extension(config): Extension<Config>,
|
|
||||||
request: Result<Json<Request>, JsonRejection>,
|
|
||||||
) -> Result<Json<Response>, ServerError> {
|
|
||||||
// validate json request
|
|
||||||
let request = validate_json(request)?;
|
|
||||||
|
|
||||||
// create `User` type
|
|
||||||
let user = User::new(&request.username, &request.password);
|
|
||||||
|
|
||||||
// search for a user in database
|
|
||||||
let response = match db.find_user(&user).await {
|
|
||||||
Ok(user) => {
|
|
||||||
// create user token
|
|
||||||
let token = create_token(&user, config.jwt.secret.as_bytes(), config.jwt.expires)?;
|
|
||||||
|
|
||||||
Response {
|
|
||||||
access_token: token,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// error while searching for a user
|
|
||||||
Err(err) => {
|
|
||||||
return match err {
|
|
||||||
// user not found
|
|
||||||
Error::UserNotFound => Err(ServerError::AuthError(AuthError::UserNotFound)),
|
|
||||||
// other error
|
|
||||||
_ => Err(ServerError::AuthError(AuthError::Other(err.to_string()))),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// send response
|
|
||||||
Ok(Json(response))
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
mod login;
|
|
||||||
mod register;
|
|
||||||
mod whoami;
|
|
||||||
|
|
||||||
pub fn app() -> axum::Router {
|
|
||||||
use axum::routing::{get, post};
|
|
||||||
|
|
||||||
axum::Router::new()
|
|
||||||
.route("/login", post(login::handle))
|
|
||||||
.route("/register", post(register::handle))
|
|
||||||
.route("/whoami", get(whoami::handle))
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
use axum::{extract::rejection::JsonRejection, Extension, Json};
|
|
||||||
use homedisk_database::{Database, User};
|
|
||||||
use homedisk_types::{
|
|
||||||
auth::login::{Request, Response},
|
|
||||||
config::Config,
|
|
||||||
errors::{AuthError, FsError, ServerError},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::middleware::{create_token, validate_json};
|
|
||||||
|
|
||||||
pub async fn handle(
|
|
||||||
Extension(db): Extension<Database>,
|
|
||||||
Extension(config): Extension<Config>,
|
|
||||||
request: Result<Json<Request>, JsonRejection>,
|
|
||||||
) -> Result<Json<Response>, ServerError> {
|
|
||||||
// validate json request
|
|
||||||
let request = validate_json(request)?;
|
|
||||||
|
|
||||||
// username must contain at least 4 characters
|
|
||||||
if request.username.len() < 4 {
|
|
||||||
return Err(ServerError::AuthError(AuthError::UsernameTooShort));
|
|
||||||
}
|
|
||||||
|
|
||||||
// username must be less than 25 characters
|
|
||||||
if request.username.len() > 25 {
|
|
||||||
return Err(ServerError::AuthError(AuthError::UsernameTooLong));
|
|
||||||
}
|
|
||||||
|
|
||||||
// password must contain at least 8 characters
|
|
||||||
if request.password.len() < 8 {
|
|
||||||
return Err(ServerError::AuthError(AuthError::PasswordTooShort));
|
|
||||||
}
|
|
||||||
|
|
||||||
// create `User` type and hash password
|
|
||||||
let user = User::new(&request.username, &request.password);
|
|
||||||
|
|
||||||
// create user in the database
|
|
||||||
let response = match db.create_user(&user).await {
|
|
||||||
Ok(_result) => {
|
|
||||||
let token = create_token(&user, config.jwt.secret.as_bytes(), config.jwt.expires)?;
|
|
||||||
|
|
||||||
Response {
|
|
||||||
access_token: token,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// error while searching for a user
|
|
||||||
Err(err) => {
|
|
||||||
// user already exists
|
|
||||||
if err.to_string().contains("UNIQUE constraint failed") {
|
|
||||||
return Err(ServerError::AuthError(AuthError::UserAlreadyExists));
|
|
||||||
}
|
|
||||||
|
|
||||||
// other error
|
|
||||||
return Err(ServerError::AuthError(AuthError::Other(err.to_string())));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// create directory for the user files
|
|
||||||
fs::create_dir_all(&format!("{}/{}", config.storage.path, user.username,))
|
|
||||||
.map_err(|e| ServerError::FsError(FsError::CreateDirectory(e.to_string())))?;
|
|
||||||
|
|
||||||
// send response
|
|
||||||
Ok(Json(response))
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
use axum::{Extension, Json};
|
|
||||||
use axum_auth::AuthBearer;
|
|
||||||
use homedisk_database::{Database, Error};
|
|
||||||
use homedisk_types::{
|
|
||||||
auth::whoami::Response,
|
|
||||||
config::Config,
|
|
||||||
errors::{AuthError, ServerError},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::middleware::validate_jwt;
|
|
||||||
|
|
||||||
pub async fn handle(
|
|
||||||
db: Extension<Database>,
|
|
||||||
config: Extension<Config>,
|
|
||||||
AuthBearer(token): AuthBearer,
|
|
||||||
) -> Result<Json<Response>, ServerError> {
|
|
||||||
// validate user token
|
|
||||||
let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?;
|
|
||||||
|
|
||||||
// search for a user in database
|
|
||||||
let response = match db.find_user_by_id(&token.claims.sub).await {
|
|
||||||
Ok(user) => Response {
|
|
||||||
username: user.username,
|
|
||||||
},
|
|
||||||
|
|
||||||
// error while searching for a user
|
|
||||||
Err(err) => match err {
|
|
||||||
// user not found
|
|
||||||
Error::UserNotFound => return Err(ServerError::AuthError(AuthError::UserNotFound)),
|
|
||||||
// other error
|
|
||||||
_ => return Err(ServerError::AuthError(AuthError::Other(err.to_string()))),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// send response
|
|
||||||
Ok(Json(response))
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
// HTTP Error
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
// axum::Error
|
|
||||||
#[error("axum error - {0}")]
|
|
||||||
Axum(axum::Error),
|
|
||||||
// hyper::Error
|
|
||||||
#[error("hyper error - {0}")]
|
|
||||||
Hyper(hyper::Error),
|
|
||||||
// std::net::AddrParseError
|
|
||||||
#[error("std::net::AddrParseError - {0}")]
|
|
||||||
AddrParseError(std::net::AddrParseError),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Custom Result
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
impl From<axum::Error> for Error {
|
|
||||||
fn from(err: axum::Error) -> Self {
|
|
||||||
Error::Axum(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<hyper::Error> for Error {
|
|
||||||
fn from(err: hyper::Error) -> Self {
|
|
||||||
Error::Hyper(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<std::net::AddrParseError> for Error {
|
|
||||||
fn from(err: std::net::AddrParseError) -> Self {
|
|
||||||
Error::AddrParseError(err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
use axum::{extract::rejection::JsonRejection, Extension, Json};
|
|
||||||
use axum_auth::AuthBearer;
|
|
||||||
use homedisk_database::Database;
|
|
||||||
use homedisk_types::{
|
|
||||||
config::Config,
|
|
||||||
errors::{FsError, ServerError},
|
|
||||||
fs::create_dir::Request,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::middleware::{find_user, validate_json, validate_jwt, validate_path};
|
|
||||||
|
|
||||||
pub async fn handle(
|
|
||||||
Extension(db): Extension<Database>,
|
|
||||||
Extension(config): Extension<Config>,
|
|
||||||
AuthBearer(token): AuthBearer,
|
|
||||||
request: Result<Json<Request>, JsonRejection>,
|
|
||||||
) -> Result<(), ServerError> {
|
|
||||||
// validate json request
|
|
||||||
let Json(request) = validate_json(request)?;
|
|
||||||
|
|
||||||
// validate user token
|
|
||||||
let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?;
|
|
||||||
|
|
||||||
// validate the `path` can be used
|
|
||||||
validate_path(&request.path)?;
|
|
||||||
|
|
||||||
// search for a user by UUID from a token
|
|
||||||
let user = find_user(&db, &token.claims.sub).await?;
|
|
||||||
|
|
||||||
// directory where the file will be placed
|
|
||||||
let path = format!(
|
|
||||||
"{user_dir}/{req_dir}",
|
|
||||||
user_dir = user.user_dir(&config.storage.path),
|
|
||||||
req_dir = request.path
|
|
||||||
);
|
|
||||||
|
|
||||||
// create directories
|
|
||||||
fs::create_dir_all(path)
|
|
||||||
.map_err(|err| ServerError::FsError(FsError::CreateDirectory(err.to_string())))?;
|
|
||||||
|
|
||||||
// send an empty response
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
use std::{fs, path::Path};
|
|
||||||
|
|
||||||
use axum::{extract::Query, Extension};
|
|
||||||
use axum_auth::AuthBearer;
|
|
||||||
use homedisk_database::Database;
|
|
||||||
use homedisk_types::{
|
|
||||||
config::Config,
|
|
||||||
errors::{FsError, ServerError},
|
|
||||||
fs::delete::Request,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::middleware::{find_user, validate_jwt, validate_path};
|
|
||||||
|
|
||||||
pub async fn handle(
|
|
||||||
Extension(db): Extension<Database>,
|
|
||||||
Extension(config): Extension<Config>,
|
|
||||||
AuthBearer(token): AuthBearer,
|
|
||||||
query: Query<Request>,
|
|
||||||
) -> Result<(), ServerError> {
|
|
||||||
// validate user token
|
|
||||||
let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?;
|
|
||||||
|
|
||||||
// validate the `path` can be used
|
|
||||||
validate_path(&query.path)?;
|
|
||||||
|
|
||||||
// search for a user by UUID from a token
|
|
||||||
let user = find_user(&db, &token.claims.sub).await?;
|
|
||||||
|
|
||||||
// path to the file
|
|
||||||
let path = format!(
|
|
||||||
"{user_dir}/{request_path}",
|
|
||||||
user_dir = user.user_dir(&config.storage.path),
|
|
||||||
request_path = query.path
|
|
||||||
);
|
|
||||||
let path = Path::new(&path);
|
|
||||||
|
|
||||||
// if file does not exist return error
|
|
||||||
if !path.exists() {
|
|
||||||
return Err(ServerError::FsError(FsError::FileDoesNotExist));
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete file
|
|
||||||
if path.is_file() {
|
|
||||||
fs::remove_file(&path)
|
|
||||||
.map_err(|err| ServerError::FsError(FsError::DeleteFile(err.to_string())))?;
|
|
||||||
}
|
|
||||||
// delete directory
|
|
||||||
else if path.is_dir() {
|
|
||||||
fs::remove_dir(&path)
|
|
||||||
.map_err(|err| ServerError::FsError(FsError::DeleteDirectory(err.to_string())))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// send an empty response
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
use axum::{extract::Query, Extension};
|
|
||||||
use axum_auth::AuthBearer;
|
|
||||||
use homedisk_database::Database;
|
|
||||||
use homedisk_types::{
|
|
||||||
config::Config,
|
|
||||||
errors::{FsError, ServerError},
|
|
||||||
fs::upload::Pagination,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::middleware::{find_user, validate_jwt, validate_path};
|
|
||||||
|
|
||||||
pub async fn handle(
|
|
||||||
Extension(db): Extension<Database>,
|
|
||||||
Extension(config): Extension<Config>,
|
|
||||||
AuthBearer(token): AuthBearer,
|
|
||||||
query: Query<Pagination>,
|
|
||||||
) -> Result<Vec<u8>, ServerError> {
|
|
||||||
// validate user token
|
|
||||||
let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?;
|
|
||||||
|
|
||||||
// validate the `path` can be used
|
|
||||||
validate_path(&query.path)?;
|
|
||||||
|
|
||||||
// search for a user by UUID from a token
|
|
||||||
let user = find_user(&db, &token.claims.sub).await?;
|
|
||||||
|
|
||||||
// directory where the file will be placed
|
|
||||||
let path = format!(
|
|
||||||
"{user_dir}/{req_dir}",
|
|
||||||
user_dir = user.user_dir(&config.storage.path),
|
|
||||||
req_dir = query.path
|
|
||||||
);
|
|
||||||
|
|
||||||
// read file content
|
|
||||||
let content =
|
|
||||||
fs::read(path).map_err(|err| ServerError::FsError(FsError::ReadFile(err.to_string())))?;
|
|
||||||
|
|
||||||
// send file content in response
|
|
||||||
Ok(content)
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
use std::{fs, io, path::PathBuf, time::SystemTime};
|
|
||||||
|
|
||||||
use axum::{extract::rejection::JsonRejection, Extension, Json};
|
|
||||||
use axum_auth::AuthBearer;
|
|
||||||
use byte_unit::Byte;
|
|
||||||
use homedisk_database::Database;
|
|
||||||
use homedisk_types::{
|
|
||||||
config::Config,
|
|
||||||
errors::{FsError, ServerError},
|
|
||||||
fs::list::{DirInfo, FileInfo, Request, Response},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::middleware::{find_user, validate_json, validate_jwt, validate_path};
|
|
||||||
|
|
||||||
/// Get directory size on disk (size of all files in directory).
|
|
||||||
fn dir_size(path: impl Into<PathBuf>) -> io::Result<u64> {
|
|
||||||
fn dir_size(mut dir: fs::ReadDir) -> io::Result<u64> {
|
|
||||||
dir.try_fold(0, |acc, file| {
|
|
||||||
let file = file?;
|
|
||||||
let size = match file.metadata()? {
|
|
||||||
data if data.is_dir() => dir_size(fs::read_dir(file.path())?)?,
|
|
||||||
data => data.len(),
|
|
||||||
};
|
|
||||||
Ok(acc + size)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
dir_size(fs::read_dir(path.into())?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle(
|
|
||||||
Extension(db): Extension<Database>,
|
|
||||||
Extension(config): Extension<Config>,
|
|
||||||
AuthBearer(token): AuthBearer,
|
|
||||||
request: Result<Json<Request>, JsonRejection>,
|
|
||||||
) -> Result<Json<Response>, ServerError> {
|
|
||||||
// validate json request
|
|
||||||
let Json(request) = validate_json::<Request>(request)?;
|
|
||||||
|
|
||||||
// validate user token
|
|
||||||
let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?;
|
|
||||||
|
|
||||||
// validate the `path` can be used
|
|
||||||
validate_path(&request.path)?;
|
|
||||||
|
|
||||||
// search for a user by UUID from a token
|
|
||||||
let user = find_user(&db, &token.claims.sub).await?;
|
|
||||||
|
|
||||||
// directory where the file will be placed
|
|
||||||
let path = format!(
|
|
||||||
"{user_dir}/{req_dir}",
|
|
||||||
user_dir = user.user_dir(&config.storage.path),
|
|
||||||
req_dir = request.path
|
|
||||||
);
|
|
||||||
|
|
||||||
// get paths from dir
|
|
||||||
let paths = fs::read_dir(&path)
|
|
||||||
.map_err(|err| ServerError::FsError(FsError::ReadDirectory(err.to_string())))?;
|
|
||||||
|
|
||||||
let mut files = vec![];
|
|
||||||
let mut dirs = vec![];
|
|
||||||
|
|
||||||
for f in paths {
|
|
||||||
// handle Error
|
|
||||||
let f = f.map_err(|err| ServerError::FsError(FsError::Other(err.to_string())))?;
|
|
||||||
|
|
||||||
// get path metadata
|
|
||||||
let metadata = f
|
|
||||||
.metadata()
|
|
||||||
.map_err(|err| ServerError::FsError(FsError::Other(err.to_string())))?;
|
|
||||||
|
|
||||||
// get name of the path
|
|
||||||
let name = f.path().display().to_string().replace(&path, "");
|
|
||||||
|
|
||||||
// if path is directory
|
|
||||||
if metadata.is_dir() {
|
|
||||||
let size = Byte::from_bytes(
|
|
||||||
dir_size(f.path().display().to_string())
|
|
||||||
.map_err(|err| ServerError::FsError(FsError::Other(err.to_string())))?
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.get_appropriate_unit(true)
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
dirs.push(DirInfo { name, size })
|
|
||||||
}
|
|
||||||
// if path is file
|
|
||||||
else {
|
|
||||||
// get file size in bytes
|
|
||||||
let size = Byte::from_bytes(metadata.len().into())
|
|
||||||
.get_appropriate_unit(true)
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
// get modification time in unix time format
|
|
||||||
let modified = metadata
|
|
||||||
.modified()
|
|
||||||
.unwrap()
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
files.push(FileInfo {
|
|
||||||
name,
|
|
||||||
size,
|
|
||||||
modified,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// send response
|
|
||||||
Ok(Json(Response { files, dirs }))
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
mod create_dir;
|
|
||||||
mod delete;
|
|
||||||
mod download;
|
|
||||||
mod list;
|
|
||||||
mod upload;
|
|
||||||
|
|
||||||
pub fn app() -> axum::Router {
|
|
||||||
use axum::routing::{delete, get, post};
|
|
||||||
|
|
||||||
axum::Router::new()
|
|
||||||
.route("/list", post(list::handle))
|
|
||||||
.route("/upload", post(upload::handle))
|
|
||||||
.route("/delete", delete(delete::handle))
|
|
||||||
.route("/download", get(download::handle))
|
|
||||||
.route("/createdir", post(create_dir::handle))
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
use std::{fs, io::Write, path::Path};
|
|
||||||
|
|
||||||
use axum::{
|
|
||||||
extract::{Multipart, Query},
|
|
||||||
Extension,
|
|
||||||
};
|
|
||||||
use axum_auth::AuthBearer;
|
|
||||||
use futures::TryStreamExt;
|
|
||||||
use homedisk_database::Database;
|
|
||||||
use homedisk_types::{
|
|
||||||
config::Config,
|
|
||||||
errors::{FsError, ServerError},
|
|
||||||
fs::upload::Pagination,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::middleware::{find_user, validate_jwt, validate_path};
|
|
||||||
|
|
||||||
pub async fn handle(
|
|
||||||
Extension(db): Extension<Database>,
|
|
||||||
Extension(config): Extension<Config>,
|
|
||||||
AuthBearer(token): AuthBearer,
|
|
||||||
mut multipart: Multipart,
|
|
||||||
query: Query<Pagination>,
|
|
||||||
) -> Result<(), ServerError> {
|
|
||||||
// validate user token
|
|
||||||
let token = validate_jwt(config.jwt.secret.as_bytes(), &token)?;
|
|
||||||
|
|
||||||
// validate the `path` can be used
|
|
||||||
validate_path(&query.path)?;
|
|
||||||
|
|
||||||
// search for a user by UUID from a token
|
|
||||||
let user = find_user(&db, &token.claims.sub).await?;
|
|
||||||
|
|
||||||
// path to the file
|
|
||||||
let file_path = format!(
|
|
||||||
"{user_dir}/{request_path}",
|
|
||||||
user_dir = user.user_dir(&config.storage.path),
|
|
||||||
request_path = query.path
|
|
||||||
);
|
|
||||||
let file_path = Path::new(&file_path);
|
|
||||||
|
|
||||||
// check if the file currently exists to avoid overwriting it
|
|
||||||
if file_path.exists() {
|
|
||||||
return Err(ServerError::FsError(FsError::FileAlreadyExists));
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a directory where the file will be placed
|
|
||||||
// e.g. path ==> `/secret/files/images/screenshot.png`
|
|
||||||
// directories up to `{storage dir}/{username}/secret/files/images/` will be created
|
|
||||||
if let Some(prefix) = file_path.parent() {
|
|
||||||
fs::create_dir_all(&prefix)
|
|
||||||
.map_err(|err| ServerError::FsError(FsError::CreateFile(err.to_string())))?
|
|
||||||
}
|
|
||||||
|
|
||||||
// get multipart field
|
|
||||||
let field = multipart
|
|
||||||
.next_field()
|
|
||||||
.await
|
|
||||||
.map_err(|_| ServerError::FsError(FsError::MultipartError))?
|
|
||||||
.ok_or(ServerError::FsError(FsError::MultipartError))?;
|
|
||||||
|
|
||||||
// create file
|
|
||||||
let file = std::fs::File::create(&file_path)
|
|
||||||
.map_err(|err| ServerError::FsError(FsError::CreateFile(err.to_string())))?;
|
|
||||||
|
|
||||||
// write file (chunk by chunk)
|
|
||||||
field
|
|
||||||
.try_fold((file, 0u64), |(mut file, written_len), bytes| async move {
|
|
||||||
file.write_all(bytes.as_ref())
|
|
||||||
.expect("failed to write chunk to file");
|
|
||||||
|
|
||||||
Ok((file, written_len + bytes.len() as u64))
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|err| ServerError::FsError(FsError::WriteFile(err.to_string())))?;
|
|
||||||
|
|
||||||
// send an empty response
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
#![doc(html_root_url = "https://homedisk-doc.medzik.xyz")]
|
|
||||||
|
|
||||||
mod auth;
|
|
||||||
mod error;
|
|
||||||
mod fs;
|
|
||||||
mod middleware;
|
|
||||||
|
|
||||||
use axum::{http::HeaderValue, routing::get, Extension, Router, Server};
|
|
||||||
use homedisk_database::Database;
|
|
||||||
use homedisk_types::config::Config;
|
|
||||||
use log::{debug, info};
|
|
||||||
use tower_http::cors::{AllowOrigin, CorsLayer};
|
|
||||||
|
|
||||||
/// Handle `/health-check` requests
|
|
||||||
async fn health_check() -> &'static str {
|
|
||||||
"I'm alive!"
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start HTTP server
|
|
||||||
pub async fn serve_http(
|
|
||||||
host: String,
|
|
||||||
origins: Vec<HeaderValue>,
|
|
||||||
db: Database,
|
|
||||||
config: Config,
|
|
||||||
) -> error::Result<()> {
|
|
||||||
debug!("Starting http server");
|
|
||||||
info!("Website available at: http://{host}");
|
|
||||||
|
|
||||||
// create http Router
|
|
||||||
let app = Router::new()
|
|
||||||
.route("/health-check", get(health_check))
|
|
||||||
.nest("/api/auth", auth::app())
|
|
||||||
.nest("/api/fs", fs::app())
|
|
||||||
.layer(CorsLayer::new().allow_origin(AllowOrigin::list(origins)))
|
|
||||||
.layer(Extension(db))
|
|
||||||
.layer(Extension(config));
|
|
||||||
|
|
||||||
// bind the provided address and serve Router
|
|
||||||
Server::bind(&host.parse()?)
|
|
||||||
.serve(app.into_make_service())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
use crypto_utils::jsonwebtoken::{Token, TokenData};
|
|
||||||
use homedisk_types::errors::{AuthError, ServerError};
|
|
||||||
|
|
||||||
/// Validate user token
|
|
||||||
pub fn validate_jwt(secret: &[u8], token: &str) -> Result<TokenData, ServerError> {
|
|
||||||
match Token::decode(secret, token.to_string()) {
|
|
||||||
// if success return claims
|
|
||||||
Ok(claims) => Ok(claims),
|
|
||||||
// invalid token
|
|
||||||
Err(_) => Err(ServerError::AuthError(AuthError::InvalidToken)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use homedisk_database::User;
|
|
||||||
|
|
||||||
use super::validate_jwt;
|
|
||||||
use crate::middleware::create_token;
|
|
||||||
|
|
||||||
const USERNAME: &str = "username";
|
|
||||||
const PASSWORD: &str = "password";
|
|
||||||
|
|
||||||
const SECRET: &[u8] = b"secret";
|
|
||||||
const INVALID_SECRET: &[u8] = b"invalid secret";
|
|
||||||
|
|
||||||
/// Test a token validation
|
|
||||||
#[test]
|
|
||||||
fn validate_token() {
|
|
||||||
let user = User::new(USERNAME, PASSWORD);
|
|
||||||
|
|
||||||
let token = create_token(&user, SECRET, 1).unwrap();
|
|
||||||
|
|
||||||
validate_jwt(SECRET, &token).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test a token validation (invalid secret)
|
|
||||||
#[test]
|
|
||||||
fn validate_token_invalid_secret() {
|
|
||||||
let user = User::new(USERNAME, PASSWORD);
|
|
||||||
|
|
||||||
let token = create_token(&user, SECRET, 1).unwrap();
|
|
||||||
|
|
||||||
validate_jwt(INVALID_SECRET, &token).unwrap_err();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
use crypto_utils::jsonwebtoken::{Claims, Token};
|
|
||||||
use homedisk_database::{Database, User};
|
|
||||||
use homedisk_types::errors::{AuthError, ServerError};
|
|
||||||
|
|
||||||
/// Create user token
|
|
||||||
pub fn create_token(user: &User, secret: &[u8], expires: i64) -> Result<String, ServerError> {
|
|
||||||
let token = Token::new(secret, Claims::new(&user.id, expires))
|
|
||||||
.map_err(|_| ServerError::AuthError(AuthError::TokenGenerate))?;
|
|
||||||
|
|
||||||
Ok(token.encoded)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Search for a user
|
|
||||||
pub async fn find_user(db: &Database, user_id: &str) -> Result<User, ServerError> {
|
|
||||||
match db.find_user_by_id(user_id).await {
|
|
||||||
// if success return user
|
|
||||||
Ok(user) => Ok(user),
|
|
||||||
// errors
|
|
||||||
Err(err) => match err {
|
|
||||||
// user not found
|
|
||||||
homedisk_database::Error::UserNotFound => {
|
|
||||||
Err(ServerError::AuthError(AuthError::UserNotFound))
|
|
||||||
},
|
|
||||||
// other error
|
|
||||||
_ => Err(ServerError::AuthError(AuthError::Other(err.to_string()))),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use homedisk_database::User;
|
|
||||||
|
|
||||||
use super::create_token;
|
|
||||||
|
|
||||||
const SECRET: &[u8] = b"secret";
|
|
||||||
|
|
||||||
const USERNAME: &str = "username";
|
|
||||||
const PASSWORD: &str = "password";
|
|
||||||
|
|
||||||
/// Test a token creation
|
|
||||||
#[test]
|
|
||||||
fn test_create_token() {
|
|
||||||
let secret = SECRET;
|
|
||||||
let user = User::new(USERNAME, PASSWORD);
|
|
||||||
|
|
||||||
create_token(&user, secret, 1).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
mod auth;
|
|
||||||
mod jwt;
|
|
||||||
mod validate_json;
|
|
||||||
mod validate_path;
|
|
||||||
|
|
||||||
pub use auth::*;
|
|
||||||
pub use jwt::*;
|
|
||||||
pub use validate_json::*;
|
|
||||||
pub use validate_path::*;
|
|
|
@ -1,20 +0,0 @@
|
||||||
use axum::{extract::rejection::JsonRejection, Json};
|
|
||||||
use homedisk_types::errors::ServerError;
|
|
||||||
|
|
||||||
/// Validate json request
|
|
||||||
pub fn validate_json<T>(payload: Result<Json<T>, JsonRejection>) -> Result<Json<T>, ServerError> {
|
|
||||||
match payload {
|
|
||||||
// if success return payload
|
|
||||||
Ok(payload) => Ok(payload),
|
|
||||||
// mission json in Content-Type Header
|
|
||||||
Err(JsonRejection::MissingJsonContentType(_)) => Err(ServerError::InvalidContentType),
|
|
||||||
// failed to deserialize json
|
|
||||||
Err(JsonRejection::JsonDataError(_)) => Err(ServerError::JsonDataError),
|
|
||||||
// syntax error in json
|
|
||||||
Err(JsonRejection::JsonSyntaxError(_)) => Err(ServerError::JsonSyntaxError),
|
|
||||||
// failed to extract the request body
|
|
||||||
Err(JsonRejection::BytesRejection(_)) => Err(ServerError::BytesRejection),
|
|
||||||
// other error
|
|
||||||
Err(err) => Err(ServerError::Other(err.to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
use homedisk_types::errors::{FsError, ServerError};
|
|
||||||
|
|
||||||
/// Validate path param provided in the request
|
|
||||||
pub fn validate_path(path: &str) -> Result<(), ServerError> {
|
|
||||||
// `path` can't contain `..`
|
|
||||||
// to prevent attack attempts because by using a `..` you can access the previous folder
|
|
||||||
if path.contains("..") {
|
|
||||||
return Err(ServerError::FsError(FsError::ReadDirectory(
|
|
||||||
"the `path` can't contain `..`".to_string(),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// `path` can't contain `~`
|
|
||||||
// to prevent attack attempts because `~` can get up a directory on `$HOME`
|
|
||||||
if path.contains('~') {
|
|
||||||
return Err(ServerError::FsError(FsError::ReadDirectory(
|
|
||||||
"the `path` can't not contain `~`".to_string(),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validate_path() {
|
|
||||||
// Successfully
|
|
||||||
assert!(validate_path("Directory/path/to/test.png").is_ok());
|
|
||||||
assert!(validate_path("/test.png").is_ok()); // `/` doesn't point to the system root
|
|
||||||
assert!(validate_path("./test.png").is_ok());
|
|
||||||
|
|
||||||
// Errors
|
|
||||||
assert!(validate_path("../../test.png").is_err());
|
|
||||||
assert!(validate_path("../test.png").is_err());
|
|
||||||
assert!(validate_path("~/test.png").is_err());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,6 +11,8 @@ pub enum Error {
|
||||||
ConnectDatabase(sqlx::Error),
|
ConnectDatabase(sqlx::Error),
|
||||||
#[error("failed to get row: {0}")]
|
#[error("failed to get row: {0}")]
|
||||||
GetRow(sqlx::Error),
|
GetRow(sqlx::Error),
|
||||||
|
#[error("failed to create all required tables: {0}")]
|
||||||
|
CreateTables(sqlx::Error),
|
||||||
#[error("failed to execute the query: {0}")]
|
#[error("failed to execute the query: {0}")]
|
||||||
Execute(sqlx::Error),
|
Execute(sqlx::Error),
|
||||||
}
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
//! [SQLite database functions](sqlite::Database).
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
mod sqlite;
|
||||||
|
mod user;
|
||||||
|
|
||||||
|
pub use sqlite::*;
|
||||||
|
pub use user::*;
|
|
@ -0,0 +1,174 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use futures_util::TryStreamExt;
|
||||||
|
use sqlx::{
|
||||||
|
sqlite::{SqliteConnectOptions, SqliteQueryResult, SqliteRow},
|
||||||
|
ConnectOptions, Executor, Row, SqlitePool,
|
||||||
|
};
|
||||||
|
use tracing::{debug, info, log::LevelFilter};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
error::{Error, Result},
|
||||||
|
User,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// SQLite database
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Database {
|
||||||
|
/// Sqlite connection pool
|
||||||
|
pub pool: SqlitePool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check UUID
|
||||||
|
impl Database {
|
||||||
|
/// Open a SQLite database
|
||||||
|
pub async fn open(path: &str) -> Result<Self> {
|
||||||
|
debug!("Opening SQLite database: {}", path);
|
||||||
|
|
||||||
|
// sqlite connection options
|
||||||
|
let mut options = SqliteConnectOptions::from_str(path).map_err(Error::OpenDatabase)?;
|
||||||
|
|
||||||
|
// set log level to Debug
|
||||||
|
options.log_statements(LevelFilter::Debug);
|
||||||
|
|
||||||
|
// create a database pool
|
||||||
|
let pool = SqlitePool::connect_with(options)
|
||||||
|
.await
|
||||||
|
.map_err(Error::ConnectDatabase)?;
|
||||||
|
|
||||||
|
info!("Connected to database!");
|
||||||
|
|
||||||
|
Ok(Self { pool })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create all required tables for HomeDisk.
|
||||||
|
pub async fn create_tables(&self) -> Result<SqliteQueryResult> {
|
||||||
|
self.pool
|
||||||
|
.execute(include_str!("../../tables.sql"))
|
||||||
|
.await
|
||||||
|
.map_err(Error::CreateTables)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create new user in the database.
|
||||||
|
pub async fn create_user(&self, user: &User) -> Result<SqliteQueryResult> {
|
||||||
|
debug!("Creating user - {}", user.username);
|
||||||
|
|
||||||
|
// build sql query
|
||||||
|
let query = sqlx::query("INSERT INTO user (id, username, password) VALUES (?, ?, ?)")
|
||||||
|
.bind(&user.id)
|
||||||
|
.bind(&user.username)
|
||||||
|
.bind(&user.password);
|
||||||
|
|
||||||
|
self.pool.execute(query).await.map_err(Error::Execute)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search for a user.
|
||||||
|
pub async fn find_user(&self, user: &User) -> Result<User> {
|
||||||
|
debug!("Searching for a user - {}", user.username);
|
||||||
|
|
||||||
|
// create query request to database
|
||||||
|
let query = sqlx::query("SELECT * FROM user WHERE username = ? AND password = ?")
|
||||||
|
.bind(&user.username)
|
||||||
|
.bind(&user.password);
|
||||||
|
|
||||||
|
// fetch query
|
||||||
|
let mut stream = self.pool.fetch(query);
|
||||||
|
|
||||||
|
// get rows from query
|
||||||
|
let row = stream
|
||||||
|
.try_next()
|
||||||
|
.await
|
||||||
|
.map_err(Error::Execute)?
|
||||||
|
.ok_or(Error::UserNotFound)?;
|
||||||
|
|
||||||
|
Self::find(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search for a user by UUID.
|
||||||
|
pub async fn find_user_by_id(&self, id: &str) -> Result<User> {
|
||||||
|
debug!("Searching for a user by UUID - {}", id);
|
||||||
|
|
||||||
|
// create query request to database
|
||||||
|
let query = sqlx::query("SELECT * FROM user WHERE id = ?").bind(id);
|
||||||
|
|
||||||
|
// fetch query
|
||||||
|
let mut stream = self.pool.fetch(query);
|
||||||
|
|
||||||
|
// get rows from query
|
||||||
|
let row = stream
|
||||||
|
.try_next()
|
||||||
|
.await
|
||||||
|
.map_err(Error::Execute)?
|
||||||
|
.ok_or(Error::UserNotFound)?;
|
||||||
|
|
||||||
|
Self::find(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find(row: SqliteRow) -> Result<User> {
|
||||||
|
// get `id` row
|
||||||
|
let id = row.try_get("id").map_err(Error::GetRow)?;
|
||||||
|
// get `username` row
|
||||||
|
let username = row.try_get("username").map_err(Error::GetRow)?;
|
||||||
|
// get `password` row
|
||||||
|
let password = row.try_get("password").map_err(Error::GetRow)?;
|
||||||
|
|
||||||
|
Ok(User {
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const USERNAME: &str = "medzik";
|
||||||
|
const PASSWORD: &str = "SuperSecretPassword123!";
|
||||||
|
|
||||||
|
async fn open_db() -> Database {
|
||||||
|
Database::open("sqlite::memory:")
|
||||||
|
.await
|
||||||
|
.expect("Failed to open database in memory")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn new_user() -> Database {
|
||||||
|
let db = open_db().await;
|
||||||
|
|
||||||
|
// create tables
|
||||||
|
db.create_tables().await.expect("create tables");
|
||||||
|
|
||||||
|
// create new user
|
||||||
|
let user = User::new(USERNAME, PASSWORD, true);
|
||||||
|
db.create_user(&user).await.expect("create user");
|
||||||
|
|
||||||
|
db
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_user() {
|
||||||
|
new_user().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_find_user() {
|
||||||
|
let db = new_user().await;
|
||||||
|
|
||||||
|
let user = db.find_user(&User::new(USERNAME, PASSWORD, false)).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(user.username, USERNAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_find_user_wrong_password() {
|
||||||
|
let db = new_user().await;
|
||||||
|
|
||||||
|
let err = db
|
||||||
|
.find_user(&User::new(USERNAME, "wrong password 123!", false))
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(err.to_string(), "user not found")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
use crypto_utils::sha::{Algorithm, CryptographicHash};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// SQL user entry
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: String,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
/// The function create a unique UUID and compute SHA-512 hash from salted user password
|
||||||
|
/// and returns the [User] type.
|
||||||
|
///
|
||||||
|
/// **Note**: This function **does not** create a user in database!
|
||||||
|
pub fn new(username: &str, password: &str, gen_uuid: bool) -> Self {
|
||||||
|
// change username to lowercase
|
||||||
|
let username = username.to_lowercase();
|
||||||
|
|
||||||
|
// salting the password
|
||||||
|
let password = format!("{username}${password}");
|
||||||
|
|
||||||
|
// hash the password using SHA-512 algorithm and encode it into String.
|
||||||
|
let password = hex::encode(CryptographicHash::hash(
|
||||||
|
Algorithm::SHA512,
|
||||||
|
password.as_bytes(),
|
||||||
|
));
|
||||||
|
|
||||||
|
// generate UUID
|
||||||
|
let id = if gen_uuid {
|
||||||
|
Uuid::new_v4().to_string()
|
||||||
|
} else {
|
||||||
|
"none".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The function returns the directory where the user files is located.
|
||||||
|
pub fn user_dir(&self, storage: &str) -> String {
|
||||||
|
format!("{storage}/{username}", username = self.username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Check if the username is in lowercase.
|
||||||
|
#[test]
|
||||||
|
fn test_username_is_lowercase() {
|
||||||
|
// example user data
|
||||||
|
let username = "MeDZiK";
|
||||||
|
let password = "password";
|
||||||
|
|
||||||
|
// expected username
|
||||||
|
let username_expected = "medzik";
|
||||||
|
|
||||||
|
let user = User::new(username, password, false);
|
||||||
|
|
||||||
|
// username validation
|
||||||
|
assert_eq!(user.username, username_expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the password is hashed.
|
||||||
|
#[test]
|
||||||
|
fn test_password_hashed() {
|
||||||
|
// example user data
|
||||||
|
let username = "username";
|
||||||
|
let password = "password";
|
||||||
|
|
||||||
|
let user = User::new(username, password, false);
|
||||||
|
|
||||||
|
assert_ne!(user.password, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_user_dir() {
|
||||||
|
let storage_dir: &str = "/storage";
|
||||||
|
|
||||||
|
let user = User::new("medzik", "whatever", false);
|
||||||
|
|
||||||
|
let user_dir = user.user_dir(storage_dir);
|
||||||
|
|
||||||
|
assert_eq!(user_dir, "/storage/medzik");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
//! Initialize logger.
|
||||||
|
|
||||||
|
use std::{panic, thread};
|
||||||
|
|
||||||
|
use tracing::{error, level_filters::LevelFilter};
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
const MAX_LEVEL: LevelFilter = LevelFilter::DEBUG;
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
const MAX_LEVEL: LevelFilter = LevelFilter::INFO;
|
||||||
|
|
||||||
|
/// Initialize logger (tracing and panic hook).
|
||||||
|
pub fn init() {
|
||||||
|
tracing_subscriber::fmt().with_max_level(MAX_LEVEL).init();
|
||||||
|
|
||||||
|
// catch panic and log them using tracing instead of default output to StdErr
|
||||||
|
panic::set_hook(Box::new(|info| {
|
||||||
|
let thread = thread::current();
|
||||||
|
let thread = thread.name().unwrap_or("unknown");
|
||||||
|
|
||||||
|
let msg = match info.payload().downcast_ref::<&'static str>() {
|
||||||
|
Some(s) => *s,
|
||||||
|
None => match info.payload().downcast_ref::<String>() {
|
||||||
|
Some(s) => &**s,
|
||||||
|
None => "Box<Any>",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let backtrace = backtrace::Backtrace::new();
|
||||||
|
|
||||||
|
match info.location() {
|
||||||
|
Some(location) => {
|
||||||
|
// without backtrace
|
||||||
|
if msg.starts_with("notrace - ") {
|
||||||
|
error!(
|
||||||
|
target: "panic", "thread '{}' panicked at '{}': {}:{}",
|
||||||
|
thread,
|
||||||
|
msg.replace("notrace - ", ""),
|
||||||
|
location.file(),
|
||||||
|
location.line()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// with backtrace
|
||||||
|
else {
|
||||||
|
error!(
|
||||||
|
target: "panic", "thread '{}' panicked at '{}': {}:{}\n{:?}",
|
||||||
|
thread,
|
||||||
|
msg,
|
||||||
|
location.file(),
|
||||||
|
location.line(),
|
||||||
|
backtrace
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
// without backtrace
|
||||||
|
if msg.starts_with("notrace - ") {
|
||||||
|
error!(
|
||||||
|
target: "panic", "thread '{}' panicked at '{}'",
|
||||||
|
thread,
|
||||||
|
msg.replace("notrace - ", ""),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// with backtrace
|
||||||
|
else {
|
||||||
|
error!(
|
||||||
|
target: "panic", "thread '{}' panicked at '{}'\n{:?}",
|
||||||
|
thread,
|
||||||
|
msg,
|
||||||
|
backtrace
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
//! HomeDisk cloud server
|
||||||
|
//!
|
||||||
|
//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
|
||||||
|
//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
|
||||||
|
//! [total-lines]: https://img.shields.io/tokei/lines/github/MedzikUser/HomeDisk?style=for-the-badge&logo=github&color=fede00
|
||||||
|
//! [code-size]: https://img.shields.io/github/languages/code-size/MedzikUser/HomeDisk?style=for-the-badge&color=c8df52&logo=github
|
||||||
|
//! [ci]: https://img.shields.io/github/workflow/status/MedzikUser/HomeDisk/Rust/main?style=for-the-badge
|
||||||
|
//!
|
||||||
|
//! [home-screenshot]: https://raw.githubusercontent.com/HomeDisk/.github/main/screenshots/home.png
|
||||||
|
//!
|
||||||
|
//! [![github]](https://github.com/MedzikUser/HomeDisk)
|
||||||
|
//! [![docs-rs]](https://homedisk-doc.vercel.app)
|
||||||
|
//! [![total-lines]](https://github.com/MedzikUser/HomeDisk)
|
||||||
|
//! [![code-size]](https://github.com/MedzikUser/HomeDisk)
|
||||||
|
//! [![ci]](https://github.com/MedzikUser/HomeDisk/actions/workflows/rust.yml)
|
||||||
|
//!
|
||||||
|
//! ![home-screenshot]
|
||||||
|
//!
|
||||||
|
//! ## 👨💻 Building
|
||||||
|
//!
|
||||||
|
//! First clone the repository: `git clone https://github.com/MedzikUser/HomeDisk.git`
|
||||||
|
//!
|
||||||
|
//! ### Requirements
|
||||||
|
//! - [Rust](https://rust-lang.org)
|
||||||
|
//!
|
||||||
|
//! To build run the command: `cargo build --release`
|
||||||
|
//!
|
||||||
|
//! The compiled binary can be found in `./target/release/homedisk`
|
||||||
|
//!
|
||||||
|
//! ## Configure
|
||||||
|
//!
|
||||||
|
//! Go to [config] module
|
||||||
|
|
||||||
|
use std::{fs::File, path::Path};
|
||||||
|
|
||||||
|
use config::Config;
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
use crate::database::Database;
|
||||||
|
|
||||||
|
#[path = "./types/config.rs"]
|
||||||
|
mod config;
|
||||||
|
mod database;
|
||||||
|
mod logger;
|
||||||
|
mod server;
|
||||||
|
|
||||||
|
/// Default SQLite file path for the database.
|
||||||
|
pub const DATABASE_FILE: &str = "homedisk.db";
|
||||||
|
/// Default configuration file.
|
||||||
|
pub const CONFIG_FILE: &str = "config.toml";
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
logger::init();
|
||||||
|
|
||||||
|
let config = Config::parse(CONFIG_FILE).expect("notrace - Failed to parse configuration file");
|
||||||
|
|
||||||
|
// open database connection
|
||||||
|
let db =
|
||||||
|
// if database file doesn't exists create it
|
||||||
|
if !Path::new(DATABASE_FILE).exists() {
|
||||||
|
warn!("Database file doesn't exists.");
|
||||||
|
info!("Creating database file...");
|
||||||
|
|
||||||
|
// create an empty database file
|
||||||
|
File::create(DATABASE_FILE).expect("notrace - Failed to create a database file");
|
||||||
|
|
||||||
|
// open database file
|
||||||
|
let db = Database::open(DATABASE_FILE)
|
||||||
|
.await
|
||||||
|
.expect("notrace - Failed to open database file");
|
||||||
|
|
||||||
|
// create tables in the database
|
||||||
|
db.create_tables()
|
||||||
|
.await
|
||||||
|
.expect("notrace - Failed to create tables in the database");
|
||||||
|
|
||||||
|
db
|
||||||
|
}
|
||||||
|
// if database file exists
|
||||||
|
else {
|
||||||
|
Database::open(DATABASE_FILE)
|
||||||
|
.await
|
||||||
|
.expect("notrace - Failed to open database file")
|
||||||
|
};
|
||||||
|
|
||||||
|
server::start_server(config, db)
|
||||||
|
.await
|
||||||
|
.expect("notrace - HTTP Server error");
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
use axum::{Extension, Json};
|
||||||
|
use crypto_utils::jsonwebtoken::{Claims, Token};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
database::{error::Error as DatabaseError, Database, User},
|
||||||
|
server::error::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn login(
|
||||||
|
Extension(db): Extension<Database>,
|
||||||
|
Extension(config): Extension<Config>,
|
||||||
|
request: Json<Request>,
|
||||||
|
) -> Result<Json<Response>> {
|
||||||
|
let user = User::new(&request.username, &request.password, false);
|
||||||
|
|
||||||
|
let response = match db.find_user(&user).await {
|
||||||
|
Ok(user) => {
|
||||||
|
let token = Token::new(
|
||||||
|
config.jwt.secret.as_bytes(),
|
||||||
|
Claims::new(&user.id, config.jwt.expires),
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::GenerateToken)?;
|
||||||
|
|
||||||
|
Response {
|
||||||
|
access_token: token.encoded,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
return match err {
|
||||||
|
DatabaseError::UserNotFound => Err(Error::UserNotFound),
|
||||||
|
_ => Err(Error::Database),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Request {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub access_token: String,
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
mod login;
|
||||||
|
mod register;
|
||||||
|
mod whoami;
|
||||||
|
|
||||||
|
use axum::routing::*;
|
||||||
|
|
||||||
|
pub fn app() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/login", post(login::login))
|
||||||
|
.route("/register", post(register::register))
|
||||||
|
.route("/whoami", get(whoami::whoami))
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
use axum::{Extension, Json};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::{Database, User},
|
||||||
|
server::error::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn register(
|
||||||
|
Extension(db): Extension<Database>,
|
||||||
|
request: Json<Request>,
|
||||||
|
) -> Result<Json<Response>> {
|
||||||
|
// username must contain at least 4 characters
|
||||||
|
if request.username.len() < 4 {
|
||||||
|
return Err(Error::UsernameTooShort);
|
||||||
|
}
|
||||||
|
|
||||||
|
// username must be less than 25 characters
|
||||||
|
if request.username.len() > 25 {
|
||||||
|
return Err(Error::UsernameTooLong);
|
||||||
|
}
|
||||||
|
|
||||||
|
// password must contain at least 8 characters
|
||||||
|
if request.password.len() < 8 {
|
||||||
|
return Err(Error::PasswordTooShort);
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = User::new(&request.username, &request.password, true);
|
||||||
|
|
||||||
|
if let Err(err) = db.create_user(&user).await {
|
||||||
|
if err.to_string().contains("UNIQUE constraint failed") {
|
||||||
|
return Err(Error::UserAlreadyExists);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(Error::Database);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(Response { success: true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Request {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
use axum::Json;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::server::{error::*, utils::token::Token};
|
||||||
|
|
||||||
|
pub async fn whoami(Token(user): Token) -> Result<Json<Response>> {
|
||||||
|
Ok(Json(Response {
|
||||||
|
user: user.username,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub user: String,
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
use std::{fs, path::Path};
|
||||||
|
|
||||||
|
use axum::{Extension, Json};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
server::{
|
||||||
|
error::*,
|
||||||
|
utils::{
|
||||||
|
path::{validate_path, PathQuery},
|
||||||
|
token::Token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn create_dir(
|
||||||
|
Extension(config): Extension<Config>,
|
||||||
|
path: PathQuery,
|
||||||
|
Token(user): Token,
|
||||||
|
) -> Result<Json<Response>> {
|
||||||
|
let path = validate_path(path)?;
|
||||||
|
|
||||||
|
let path = format!("{}/{}", user.user_dir(&config.storage.path), path);
|
||||||
|
|
||||||
|
if Path::new(&path).exists() {
|
||||||
|
return Err(Error::AlreadyExists);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::create_dir_all(path).map_err(|_| Error::CreateDirectory)?;
|
||||||
|
|
||||||
|
Ok(Json(Response { success: true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
use std::{fs, path::Path};
|
||||||
|
|
||||||
|
use axum::{Extension, Json};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
server::{
|
||||||
|
error::*,
|
||||||
|
utils::{
|
||||||
|
path::{validate_path, PathQuery},
|
||||||
|
token::Token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn delete(
|
||||||
|
Extension(config): Extension<Config>,
|
||||||
|
path: PathQuery,
|
||||||
|
Token(user): Token,
|
||||||
|
) -> Result<Json<Response>> {
|
||||||
|
let path = validate_path(path)?;
|
||||||
|
|
||||||
|
let path = format!("{}/{}", user.user_dir(&config.storage.path), path);
|
||||||
|
let path = Path::new(&path);
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(Error::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
fs::remove_dir(&path).map_err(|_| Error::DeleteDirectory)?;
|
||||||
|
} else {
|
||||||
|
fs::remove_file(&path).map_err(|_| Error::DeleteFile)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(Response { success: true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
use std::{fs, time::SystemTime};
|
||||||
|
|
||||||
|
use axum::{Extension, Json};
|
||||||
|
use byte_unit::Byte;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
server::{
|
||||||
|
error::*,
|
||||||
|
utils::{
|
||||||
|
path::{validate_path, PathQuery},
|
||||||
|
token::Token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn list(
|
||||||
|
Extension(config): Extension<Config>,
|
||||||
|
path: PathQuery,
|
||||||
|
Token(user): Token,
|
||||||
|
) -> Result<Json<Response>> {
|
||||||
|
let path = validate_path(path)?;
|
||||||
|
|
||||||
|
let mut response = Response::default();
|
||||||
|
|
||||||
|
let path = format!("{}/{}", user.user_dir(&config.storage.path), path);
|
||||||
|
|
||||||
|
let paths = fs::read_dir(&path).map_err(|_| Error::FailedReadDirectory)?;
|
||||||
|
|
||||||
|
for dir_entry in paths {
|
||||||
|
let dir_entry = dir_entry.unwrap();
|
||||||
|
|
||||||
|
let metadata = dir_entry.metadata().unwrap();
|
||||||
|
|
||||||
|
let name = dir_entry
|
||||||
|
.path()
|
||||||
|
.display()
|
||||||
|
.to_string()
|
||||||
|
.replace(&format!("{path}/"), "")
|
||||||
|
.replace(&path, "");
|
||||||
|
|
||||||
|
if metadata.is_dir() {
|
||||||
|
// TODO: add size and modification date
|
||||||
|
response.dirs.push(Entry::new(name, 0.to_string(), 0))
|
||||||
|
} else {
|
||||||
|
let size = Byte::from_bytes(metadata.len().into())
|
||||||
|
.get_appropriate_unit(true)
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let modified = metadata
|
||||||
|
.modified()
|
||||||
|
.unwrap()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
response.files.push(Entry::new(name, size, modified));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub dirs: Vec<Entry>,
|
||||||
|
pub files: Vec<Entry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct Entry {
|
||||||
|
pub name: String,
|
||||||
|
pub size: String,
|
||||||
|
pub modified: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entry {
|
||||||
|
pub fn new(name: String, size: String, modified: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
modified,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
mod create_dir;
|
||||||
|
mod delete;
|
||||||
|
mod list;
|
||||||
|
mod upload;
|
||||||
|
|
||||||
|
use axum::routing::*;
|
||||||
|
|
||||||
|
pub fn app() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/list", get(list::list))
|
||||||
|
.route("/createDir", get(create_dir::create_dir))
|
||||||
|
.route("/delete", delete(delete::delete))
|
||||||
|
.route("/upload", post(upload::upload))
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
use std::{fs, io::Write, path::Path};
|
||||||
|
|
||||||
|
use axum::{extract::Multipart, Extension, Json};
|
||||||
|
use futures_util::TryStreamExt;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
server::{
|
||||||
|
error::*,
|
||||||
|
utils::{
|
||||||
|
path::{validate_path, PathQuery},
|
||||||
|
token::Token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn upload(
|
||||||
|
Extension(config): Extension<Config>,
|
||||||
|
path: PathQuery,
|
||||||
|
Token(user): Token,
|
||||||
|
mut multipart: Multipart,
|
||||||
|
) -> Result<Json<Response>> {
|
||||||
|
let path = validate_path(path)?;
|
||||||
|
|
||||||
|
let path = format!("{}/{}", user.user_dir(&config.storage.path), path);
|
||||||
|
let path = Path::new(&path);
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
return Err(Error::AlreadyExists);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a directory where the file will be placed
|
||||||
|
// e.g. path ==> `/secret/files/images/screenshot.png`
|
||||||
|
// directories up to `{storage dir}/{username}/secret/files/images/` will be created
|
||||||
|
if let Some(prefix) = path.parent() {
|
||||||
|
fs::create_dir_all(&prefix).map_err(|_| Error::CreateDirectory)?
|
||||||
|
}
|
||||||
|
|
||||||
|
let field = multipart
|
||||||
|
.next_field()
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::Multipart)?
|
||||||
|
.ok_or(Error::Multipart)?;
|
||||||
|
|
||||||
|
let file = fs::File::create(path).map_err(|_| Error::CreateFile)?;
|
||||||
|
|
||||||
|
field
|
||||||
|
.try_fold((file, 0), |(mut file, written_len), bytes| async move {
|
||||||
|
file.write_all(bytes.as_ref())
|
||||||
|
.expect("failed to write chunk to file");
|
||||||
|
|
||||||
|
Ok((file, written_len + bytes.len() as u64))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::WriteFile)?;
|
||||||
|
|
||||||
|
Ok(Json(Response { success: true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
mod auth;
|
||||||
|
mod fs;
|
||||||
|
|
||||||
|
use axum::routing::*;
|
||||||
|
|
||||||
|
pub async fn health() -> &'static str {
|
||||||
|
"I am working!"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn app() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.nest("/auth", auth::app())
|
||||||
|
.nest("/fs", fs::app())
|
||||||
|
.route("/health", get(health))
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
use serde::Serialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
// auth error
|
||||||
|
#[error("User not found")]
|
||||||
|
UserNotFound,
|
||||||
|
#[error("User already exists")]
|
||||||
|
UserAlreadyExists,
|
||||||
|
#[error("Failed to generate access token")]
|
||||||
|
GenerateToken,
|
||||||
|
#[error("An error occurred in database request")]
|
||||||
|
Database,
|
||||||
|
#[error("Username is too short")]
|
||||||
|
UsernameTooShort,
|
||||||
|
#[error("Username is too short")]
|
||||||
|
UsernameTooLong,
|
||||||
|
#[error("Password is too short")]
|
||||||
|
PasswordTooShort,
|
||||||
|
#[error("Invalid token")]
|
||||||
|
InvalidToken,
|
||||||
|
#[error("`{0}` header is missing")]
|
||||||
|
MissingHeader(&'static str),
|
||||||
|
#[error("`Authorization` header must be a bearer token")]
|
||||||
|
MissingBearer,
|
||||||
|
// fs error
|
||||||
|
#[error("Invalid `path` query parameter")]
|
||||||
|
InvalidPath,
|
||||||
|
#[error("Failed to read files from directory")]
|
||||||
|
FailedReadDirectory,
|
||||||
|
#[error("Failed to create directory")]
|
||||||
|
CreateDirectory,
|
||||||
|
#[error("Failed to create file")]
|
||||||
|
CreateFile,
|
||||||
|
#[error("File or directory already exists")]
|
||||||
|
AlreadyExists,
|
||||||
|
#[error("File or directory does not exists")]
|
||||||
|
NotFound,
|
||||||
|
#[error("Failed to delete directory (directory is not empty?)")]
|
||||||
|
DeleteDirectory,
|
||||||
|
#[error("Failed to delete file")]
|
||||||
|
DeleteFile,
|
||||||
|
#[error("Multipart error")]
|
||||||
|
Multipart,
|
||||||
|
#[error("Write to file failed")]
|
||||||
|
WriteFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
enum ResponseError {
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl axum::response::IntoResponse for Error {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
error!("Error: {:?}", self);
|
||||||
|
|
||||||
|
let status = match self {
|
||||||
|
Error::GenerateToken => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Error::Database => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
_ => StatusCode::BAD_REQUEST,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut response = axum::Json(ResponseError::Error(self.to_string())).into_response();
|
||||||
|
*response.status_mut() = status;
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
|
@ -0,0 +1,30 @@
|
||||||
|
mod api;
|
||||||
|
pub mod error;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use axum::{routing::get, Extension, Router, Server};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::{config::Config, database::Database};
|
||||||
|
|
||||||
|
pub async fn start_server(config: Config, db: Database) -> anyhow::Result<()> {
|
||||||
|
info!(
|
||||||
|
"🚀 Server has launched on http://{}:{}",
|
||||||
|
config.http.host, config.http.port
|
||||||
|
);
|
||||||
|
|
||||||
|
let host = format!("{}:{}", config.http.host, config.http.port);
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.nest("/api", api::app())
|
||||||
|
.route("/", get(api::health))
|
||||||
|
.layer(Extension(config))
|
||||||
|
.layer(Extension(db));
|
||||||
|
|
||||||
|
Server::bind(&host.parse()?)
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Err(anyhow!("Server unexpected stopped!"))
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod token;
|
||||||
|
pub mod path;
|
|
@ -0,0 +1,29 @@
|
||||||
|
use axum::extract::Query;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::server::error::{Error, Result};
|
||||||
|
|
||||||
|
pub type PathQuery = Query<Path>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct Path {
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_path(path: PathQuery) -> Result<String> {
|
||||||
|
let path = path.path.clone();
|
||||||
|
|
||||||
|
// `path` can't contain `..`
|
||||||
|
// to prevent attack attempts because by using a `..` you can access the previous folder
|
||||||
|
if path.contains("..") {
|
||||||
|
return Err(Error::InvalidPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `path` can't contain `~`
|
||||||
|
// to prevent attack attempts because `~` can get up a directory on `$HOME`
|
||||||
|
if path.contains('~') {
|
||||||
|
return Err(Error::InvalidPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(path)
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
use axum::{
|
||||||
|
async_trait,
|
||||||
|
extract::FromRequestParts,
|
||||||
|
http::{header::AUTHORIZATION, request::Parts},
|
||||||
|
};
|
||||||
|
use crypto_utils::jsonwebtoken;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
database::{error::Error as DatabaseError, Database, User},
|
||||||
|
server::error::Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Token(pub User);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for Token
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = Error;
|
||||||
|
|
||||||
|
async fn from_request_parts(req: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let extensions = &req.extensions;
|
||||||
|
|
||||||
|
let config = extensions.get::<Config>().unwrap();
|
||||||
|
let db = extensions.get::<Database>().unwrap();
|
||||||
|
|
||||||
|
// Get authorisation header
|
||||||
|
let authorisation = req
|
||||||
|
.headers
|
||||||
|
.get(AUTHORIZATION)
|
||||||
|
.ok_or(Error::MissingHeader("Authorization"))?
|
||||||
|
.to_str()
|
||||||
|
.map_err(|_| Error::InvalidToken)?;
|
||||||
|
|
||||||
|
// Check that its a well-formed bearer
|
||||||
|
let split = authorisation.split_once(' ');
|
||||||
|
|
||||||
|
let token = match split {
|
||||||
|
Some((name, contents)) if name == "Bearer" => contents.to_string(),
|
||||||
|
_ => return Err(Error::MissingBearer),
|
||||||
|
};
|
||||||
|
|
||||||
|
let token = match jsonwebtoken::Token::decode(config.jwt.secret.as_bytes(), token) {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(_) => return Err(Error::InvalidToken),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user = match db.find_user_by_id(&token.claims.sub).await {
|
||||||
|
Ok(user) => user,
|
||||||
|
Err(err) => match err {
|
||||||
|
DatabaseError::UserNotFound => return Err(Error::UserNotFound),
|
||||||
|
_ => return Err(Error::Database),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self(user))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
//! [Configuration file types](Config).
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Settings configuration.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
/// HTTP Settings.
|
||||||
|
pub http: ConfigHTTP,
|
||||||
|
/// Json Web Token Settings.
|
||||||
|
pub jwt: ConfigJWT,
|
||||||
|
/// Storage Settings.
|
||||||
|
pub storage: ConfigStorage,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HTTP Settings.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ConfigHTTP {
|
||||||
|
/// HTTP Host.
|
||||||
|
pub host: String,
|
||||||
|
/// Port HTTP Port.
|
||||||
|
pub port: u16,
|
||||||
|
/// [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) Domains (e.g ["site1.example.com", "site2.example.com"]).
|
||||||
|
pub cors: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Json Web Token Settings.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ConfigJWT {
|
||||||
|
/// JWT Secret string (used to sign tokens).
|
||||||
|
pub secret: String,
|
||||||
|
/// Token expiration time in hours.
|
||||||
|
pub expires: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Storage Settings.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ConfigStorage {
|
||||||
|
/// Directory where user files will be stored.
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// Parse configuration file.
|
||||||
|
pub fn parse(path: &str) -> anyhow::Result<Self> {
|
||||||
|
let config_str = fs::read_to_string(path)?;
|
||||||
|
|
||||||
|
let config = toml::from_str(&config_str)?;
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Test default configuration file.
|
||||||
|
#[test]
|
||||||
|
fn test_config() {
|
||||||
|
Config::parse("./config.toml").expect("Failed to parse configuration file");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "homedisk-types"
|
|
||||||
version = "0.0.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
config = ["toml", "dirs"]
|
|
||||||
database = ["crypto-utils", "hex", "sqlx"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
thiserror = "1.0.32"
|
|
||||||
uuid = "1.1.2"
|
|
||||||
anyhow = "1.0.62"
|
|
||||||
serde = { version = "1.0.144", features = ["derive"] }
|
|
||||||
axum = { version = "0.5.15", optional = true }
|
|
||||||
toml = { version = "0.5.9", optional = true }
|
|
||||||
dirs = { version = "4.0.0", optional = true }
|
|
||||||
crypto-utils = { version = "0.4.0", features = ["sha"], optional = true }
|
|
||||||
hex = { version = "0.4.3", optional = true }
|
|
||||||
sqlx = { version = "0.6.1", features = ["sqlite"], optional = true }
|
|
|
@ -1,16 +0,0 @@
|
||||||
//! HTTP `/auth/login` Request and Response types
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// HTTP `/auth/login` Request
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Request {
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// HTTP `/auth/login` Response
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Response {
|
|
||||||
pub access_token: String,
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
//! HTTP `/auth/*` types for Request and Response
|
|
||||||
|
|
||||||
pub mod login;
|
|
||||||
pub mod whoami;
|
|
|
@ -1,10 +0,0 @@
|
||||||
//! HTTP `/auth/whoami` Response type
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// HTTP `/auth/whoami` Response
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Response {
|
|
||||||
/// Logged user username
|
|
||||||
pub username: String,
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
//! # Configuration file
|
|
||||||
//!
|
|
||||||
//! Path to a config file is `config.toml` in the work directory.
|
|
||||||
//!
|
|
||||||
//! ## Example config
|
|
||||||
//!
|
|
||||||
//! ```toml
|
|
||||||
//! [http]
|
|
||||||
//! host = "0.0.0.0" # HTTP Host
|
|
||||||
//! port = 8080 # HTTP Port
|
|
||||||
//! cors = ["homedisk.medzik.xyz"] # Domains allowed for CORS
|
|
||||||
//!
|
|
||||||
//! [jwt]
|
|
||||||
//! secret = "secret key used to sign tokens" # JWT Secret string (used to sign tokens)
|
|
||||||
//! expires = 24 # Token expiration time (in hours)
|
|
||||||
//!
|
|
||||||
//! [storage]
|
|
||||||
//! path = "/home/homedisk" # # Directory where user files will be stored
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! ### External docs
|
|
||||||
//! - [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
|
||||||
|
|
||||||
mod types;
|
|
||||||
|
|
||||||
pub use types::*;
|
|
|
@ -1,55 +0,0 @@
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Config {
|
|
||||||
/// Configure HTTP settings
|
|
||||||
pub http: ConfigHTTP,
|
|
||||||
/// Configure Json Web Token settings
|
|
||||||
pub jwt: ConfigJWT,
|
|
||||||
/// Configure storage settings
|
|
||||||
pub storage: ConfigStorage,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ConfigHTTP {
|
|
||||||
/// HTTP Host
|
|
||||||
pub host: String,
|
|
||||||
/// Port HTTP Port
|
|
||||||
pub port: u16,
|
|
||||||
/// [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) Domains (e.g ["site1.example.com", "site2.example.com"])
|
|
||||||
pub cors: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ConfigJWT {
|
|
||||||
/// JWT Secret string (used to sign tokens)
|
|
||||||
pub secret: String,
|
|
||||||
/// Token expiration time in hours
|
|
||||||
pub expires: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ConfigStorage {
|
|
||||||
/// Directory where user files will be stored
|
|
||||||
pub path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "config")]
|
|
||||||
impl Config {
|
|
||||||
/// Parse configuration file.
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// use homedisk_types::config::Config;
|
|
||||||
///
|
|
||||||
/// let config = Config::parse().unwrap();
|
|
||||||
/// ```
|
|
||||||
pub fn parse() -> anyhow::Result<Self> {
|
|
||||||
let config_str = fs::read_to_string("config.toml")?;
|
|
||||||
|
|
||||||
let config = toml::from_str(&config_str)?;
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
//! Types for a database
|
|
||||||
|
|
||||||
mod user;
|
|
||||||
|
|
||||||
pub use user::*;
|
|
|
@ -1,138 +0,0 @@
|
||||||
use crypto_utils::sha::{Algorithm, CryptographicHash};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
/// SQL user table
|
|
||||||
#[derive(Debug, sqlx::FromRow)]
|
|
||||||
pub struct User {
|
|
||||||
/// UUID of the user
|
|
||||||
pub id: String,
|
|
||||||
/// Username
|
|
||||||
pub username: String,
|
|
||||||
/// Encryped user password
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl User {
|
|
||||||
/// The function create a unique user UUID and create SHA-512 hash from salted user password
|
|
||||||
/// and returns the [User] type.
|
|
||||||
///
|
|
||||||
/// **Note: This doesn't create a user in the database!**
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use homedisk_types::database::User;
|
|
||||||
///
|
|
||||||
/// let user = User::new("medzik", "SuperSecretPassword123!");
|
|
||||||
///
|
|
||||||
/// # assert_eq!(user.username, "medzik")
|
|
||||||
/// ```
|
|
||||||
pub fn new(username: &str, password: &str) -> Self {
|
|
||||||
// change username to lowercase
|
|
||||||
let username = username.to_lowercase();
|
|
||||||
|
|
||||||
// salting the password
|
|
||||||
let password = format!("{username}${password}");
|
|
||||||
|
|
||||||
// hash password using SHA-512 and encode it to String from Vec<u8>
|
|
||||||
let password = hex::encode(CryptographicHash::hash(
|
|
||||||
Algorithm::SHA512,
|
|
||||||
password.as_bytes(),
|
|
||||||
));
|
|
||||||
|
|
||||||
// generate a user UUID
|
|
||||||
let id_sha1 = CryptographicHash::hash(
|
|
||||||
Algorithm::SHA1,
|
|
||||||
(format!("{username}${password}")).as_bytes(),
|
|
||||||
);
|
|
||||||
let id = Uuid::new_v5(&Uuid::NAMESPACE_X500, &id_sha1).to_string();
|
|
||||||
|
|
||||||
// return `User`
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The function returns the directory where the user file is located.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use homedisk_types::database::User;
|
|
||||||
///
|
|
||||||
/// let user = User::new("medzik", "whatever");
|
|
||||||
///
|
|
||||||
/// let dir = user.user_dir("/storage"); // will return `/storage/medzik`
|
|
||||||
///
|
|
||||||
/// assert_eq!(dir, "/storage/medzik")
|
|
||||||
/// ```
|
|
||||||
pub fn user_dir(&self, storage: &str) -> String {
|
|
||||||
// get a user storage path
|
|
||||||
let path = format!(
|
|
||||||
"{path}/{username}",
|
|
||||||
path = storage,
|
|
||||||
username = self.username,
|
|
||||||
);
|
|
||||||
|
|
||||||
// return user storage path
|
|
||||||
path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crypto_utils::sha::{Algorithm, CryptographicHash};
|
|
||||||
|
|
||||||
use super::User;
|
|
||||||
|
|
||||||
/// Check if the id is reproducable
|
|
||||||
#[test]
|
|
||||||
fn check_id_reproducable() {
|
|
||||||
// example user data
|
|
||||||
let username = "test";
|
|
||||||
let password = "password";
|
|
||||||
|
|
||||||
let user_a = User::new(username, password);
|
|
||||||
let user_b = User::new(username, password);
|
|
||||||
|
|
||||||
assert_eq!(user_a.id, user_b.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the username is in lowercase
|
|
||||||
#[test]
|
|
||||||
fn check_username_is_in_lowercase() {
|
|
||||||
// example user data
|
|
||||||
let username = "mEDZIk";
|
|
||||||
let password = "password";
|
|
||||||
|
|
||||||
// username in lowercase (expected username)
|
|
||||||
let username_expected = "medzik";
|
|
||||||
|
|
||||||
// create a new `User` type
|
|
||||||
let user = User::new(username, password);
|
|
||||||
|
|
||||||
// username validation with expected username
|
|
||||||
assert_eq!(user.username, username_expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check that the password is a checksum with a salt
|
|
||||||
#[test]
|
|
||||||
fn check_if_password_is_hashed_and_salted() {
|
|
||||||
// example user data
|
|
||||||
let username = "username";
|
|
||||||
let password = "password";
|
|
||||||
|
|
||||||
// create a new `User` type
|
|
||||||
let user = User::new(username, password);
|
|
||||||
|
|
||||||
// expected password salt (string)
|
|
||||||
let password_expected_salt = format!("{username}${password}");
|
|
||||||
|
|
||||||
// expected password (hashed)
|
|
||||||
let password_expected = hex::encode(CryptographicHash::hash(
|
|
||||||
Algorithm::SHA512,
|
|
||||||
password_expected_salt.as_bytes(),
|
|
||||||
));
|
|
||||||
|
|
||||||
// password validation with expected password salt
|
|
||||||
assert_eq!(user.password, password_expected)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("user not found")]
|
|
||||||
UserNotFound,
|
|
||||||
#[error("user already exists")]
|
|
||||||
UserAlreadyExists,
|
|
||||||
#[error("username is too short")]
|
|
||||||
UsernameTooShort,
|
|
||||||
#[error("username is too long")]
|
|
||||||
UsernameTooLong,
|
|
||||||
#[error("password is too short")]
|
|
||||||
PasswordTooShort,
|
|
||||||
#[error("failed to generate jwt token")]
|
|
||||||
TokenGenerate,
|
|
||||||
#[error("invalid jwt token")]
|
|
||||||
InvalidToken,
|
|
||||||
#[error("other error - {0}")]
|
|
||||||
Other(String),
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("file doesn't exists")]
|
|
||||||
FileDoesNotExist,
|
|
||||||
#[error("file already exists")]
|
|
||||||
FileAlreadyExists,
|
|
||||||
#[error("unexpected multipart error")]
|
|
||||||
MultipartError,
|
|
||||||
#[error("failed to create create a file - {0}")]
|
|
||||||
CreateFile(String),
|
|
||||||
#[error("failed to create a directory - {0}")]
|
|
||||||
CreateDirectory(String),
|
|
||||||
#[error("failed to delete file: {0}")]
|
|
||||||
DeleteFile(String),
|
|
||||||
#[error("failed to delete directory: {0}")]
|
|
||||||
DeleteDirectory(String),
|
|
||||||
#[error("failed to write content to file: {0}")]
|
|
||||||
WriteFile(String),
|
|
||||||
#[error("failed to decode base64: {0}")]
|
|
||||||
Base64(String),
|
|
||||||
#[error("failed to read directory: {0}")]
|
|
||||||
ReadDirectory(String),
|
|
||||||
#[error("failed to read file content: {0}")]
|
|
||||||
ReadFile(String),
|
|
||||||
#[error("other error - {0}")]
|
|
||||||
Other(String),
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
//! Error types
|
|
||||||
|
|
||||||
mod auth;
|
|
||||||
#[cfg(feature = "database")]
|
|
||||||
mod database;
|
|
||||||
mod fs;
|
|
||||||
mod server;
|
|
||||||
|
|
||||||
pub use auth::Error as AuthError;
|
|
||||||
pub use database::{Error as DatabaseError, Result as DatabaseResult};
|
|
||||||
pub use fs::Error as FsError;
|
|
||||||
pub use server::Error as ServerError;
|
|
|
@ -1,71 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use super::{AuthError, FsError};
|
|
||||||
|
|
||||||
/// HTTP Server Error
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Error)]
|
|
||||||
#[serde(tag = "error", content = "error_message", rename_all = "kebab-case")]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("auth error: {0}")]
|
|
||||||
AuthError(#[from] AuthError),
|
|
||||||
#[error("fs error: {0}")]
|
|
||||||
FsError(#[from] FsError),
|
|
||||||
#[error("too may requests, please slow down")]
|
|
||||||
TooManyRequests,
|
|
||||||
#[error("invalid Content-Type")]
|
|
||||||
InvalidContentType,
|
|
||||||
#[error("failed to deserialize json")]
|
|
||||||
JsonDataError,
|
|
||||||
#[error("syntax error in json")]
|
|
||||||
JsonSyntaxError,
|
|
||||||
#[error("failed to extract the request body")]
|
|
||||||
BytesRejection,
|
|
||||||
#[error("other error: {0}")]
|
|
||||||
Other(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
enum ResponseError {
|
|
||||||
Error(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error {
|
|
||||||
fn into_response(self) -> ResponseError {
|
|
||||||
ResponseError::Error(self.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "axum")]
|
|
||||||
impl axum::response::IntoResponse for Error {
|
|
||||||
fn into_response(self) -> axum::response::Response {
|
|
||||||
use axum::http::StatusCode;
|
|
||||||
|
|
||||||
let status = match self {
|
|
||||||
Self::AuthError(ref err) => match err {
|
|
||||||
AuthError::TokenGenerate => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
AuthError::Other(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
_ => StatusCode::BAD_REQUEST,
|
|
||||||
},
|
|
||||||
Self::FsError(ref err) => match err {
|
|
||||||
FsError::CreateFile(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
FsError::CreateDirectory(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
FsError::DeleteFile(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
FsError::DeleteDirectory(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
FsError::WriteFile(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
FsError::Other(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
_ => StatusCode::BAD_REQUEST,
|
|
||||||
},
|
|
||||||
Self::TooManyRequests => StatusCode::TOO_MANY_REQUESTS,
|
|
||||||
Self::BytesRejection => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
Self::Other(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
_ => StatusCode::BAD_REQUEST,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut response = axum::Json(self.into_response()).into_response();
|
|
||||||
*response.status_mut() = status;
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
//! HTTP `/fs/createdir` Request and Response types
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// HTTP `/fs/createdir` Request
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Request {
|
|
||||||
/// Path to directory wich will be created
|
|
||||||
pub path: String,
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
//! HTTP `/fs/delete` Request type
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// HTTP `/fs/delete` Request
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Request {
|
|
||||||
/// Path of file/directory to be delete
|
|
||||||
pub path: String,
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
//! HTTP `/fs/download` Request type
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// HTTP `/fs/download` Request
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Request {
|
|
||||||
/// Path of file to be download
|
|
||||||
pub path: String,
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
//! HTTP `/fs/list` Request, Response, FileInfo and DirInfo types
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// HTTP `/fs/list` Request
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Request {
|
|
||||||
/// Path to directory
|
|
||||||
pub path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// HTTP `/fs/list` Response
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Response {
|
|
||||||
/// Vector with files info
|
|
||||||
pub files: Vec<FileInfo>,
|
|
||||||
/// Vector with directories info
|
|
||||||
pub dirs: Vec<DirInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Info about a file
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct FileInfo {
|
|
||||||
/// File name
|
|
||||||
pub name: String,
|
|
||||||
/// File size
|
|
||||||
pub size: String,
|
|
||||||
/// Latest modification of this file
|
|
||||||
pub modified: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Info about a directory
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct DirInfo {
|
|
||||||
/// Directory name
|
|
||||||
pub name: String,
|
|
||||||
/// Directory size (size of all files in directory)
|
|
||||||
pub size: String,
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
//! HTTP `/fs/*` types for Request and Response
|
|
||||||
|
|
||||||
pub mod create_dir;
|
|
||||||
pub mod delete;
|
|
||||||
pub mod download;
|
|
||||||
pub mod list;
|
|
||||||
pub mod upload;
|
|
|
@ -1,10 +0,0 @@
|
||||||
//! HTTP `/fs/upload` Request and Response types
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// HTTP `/fs/upload` Queries
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Pagination {
|
|
||||||
/// Path where the file will be uploaded
|
|
||||||
pub path: String,
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
#![doc(html_root_url = "https://homedisk-doc.medzik.xyz")]
|
|
||||||
|
|
||||||
pub mod auth;
|
|
||||||
pub mod config;
|
|
||||||
#[cfg(feature = "database")]
|
|
||||||
pub mod database;
|
|
||||||
pub mod errors;
|
|
||||||
pub mod fs;
|
|
Loading…
Reference in New Issue