mirror of
https://github.com/MedzikUser/HomeDisk.git
synced 2024-08-14 21:46:53 +00:00
refactor: rewrite
This commit is contained in:
parent
eef5cd8e02
commit
4dd9013521
69 changed files with 1322 additions and 2124 deletions
579
Cargo.lock
generated
579
Cargo.lock
generated
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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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),
|
||||||
}
|
}
|
8
src/database/mod.rs
Normal file
8
src/database/mod.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
//! [SQLite database functions](sqlite::Database).
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
mod sqlite;
|
||||||
|
mod user;
|
||||||
|
|
||||||
|
pub use sqlite::*;
|
||||||
|
pub use user::*;
|
174
src/database/sqlite.rs
Normal file
174
src/database/sqlite.rs
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
93
src/database/user.rs
Normal file
93
src/database/user.rs
Normal file
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
77
src/logger.rs
Normal file
77
src/logger.rs
Normal file
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
90
src/main.rs
Normal file
90
src/main.rs
Normal file
|
@ -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");
|
||||||
|
}
|
50
src/server/api/auth/login.rs
Normal file
50
src/server/api/auth/login.rs
Normal file
|
@ -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,
|
||||||
|
}
|
12
src/server/api/auth/mod.rs
Normal file
12
src/server/api/auth/mod.rs
Normal file
|
@ -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))
|
||||||
|
}
|
50
src/server/api/auth/register.rs
Normal file
50
src/server/api/auth/register.rs
Normal file
|
@ -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,
|
||||||
|
}
|
15
src/server/api/auth/whoami.rs
Normal file
15
src/server/api/auth/whoami.rs
Normal file
|
@ -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,
|
||||||
|
}
|
38
src/server/api/fs/create_dir.rs
Normal file
38
src/server/api/fs/create_dir.rs
Normal file
|
@ -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,
|
||||||
|
}
|
43
src/server/api/fs/delete.rs
Normal file
43
src/server/api/fs/delete.rs
Normal file
|
@ -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,
|
||||||
|
}
|
86
src/server/api/fs/list.rs
Normal file
86
src/server/api/fs/list.rs
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/server/api/fs/mod.rs
Normal file
14
src/server/api/fs/mod.rs
Normal file
|
@ -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))
|
||||||
|
}
|
64
src/server/api/fs/upload.rs
Normal file
64
src/server/api/fs/upload.rs
Normal file
|
@ -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,
|
||||||
|
}
|
15
src/server/api/mod.rs
Normal file
15
src/server/api/mod.rs
Normal file
|
@ -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))
|
||||||
|
}
|
77
src/server/error.rs
Normal file
77
src/server/error.rs
Normal file
|
@ -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>;
|
30
src/server/mod.rs
Normal file
30
src/server/mod.rs
Normal file
|
@ -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!"))
|
||||||
|
}
|
2
src/server/utils/mod.rs
Normal file
2
src/server/utils/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod token;
|
||||||
|
pub mod path;
|
29
src/server/utils/path.rs
Normal file
29
src/server/utils/path.rs
Normal file
|
@ -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)
|
||||||
|
}
|
60
src/server/utils/token.rs
Normal file
60
src/server/utils/token.rs
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
64
src/types/config.rs
Normal file
64
src/types/config.rs
Normal file
|
@ -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 a new issue