refactor: rewrite
parent
eef5cd8e02
commit
4dd9013521
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,40 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"core",
|
||||
"database",
|
||||
"server",
|
||||
"types",
|
||||
]
|
||||
resolver = "2"
|
||||
[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-dev"
|
||||
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());
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
//! [SQLite database functions](sqlite::Database).
|
||||
|
||||
pub mod error;
|
||||
mod sqlite;
|
||||
mod user;
|
||||
|
||||
pub use sqlite::*;
|
||||
pub use user::*;
|
@ -0,0 +1,174 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use futures_util::TryStreamExt;
|
||||
use sqlx::{
|
||||
sqlite::{SqliteConnectOptions, SqliteQueryResult, SqliteRow},
|
||||
ConnectOptions, Executor, Row, SqlitePool,
|
||||
};
|
||||
use tracing::{debug, info, log::LevelFilter};
|
||||
|
||||
use super::{
|
||||
error::{Error, Result},
|
||||
User,
|
||||
};
|
||||
|
||||
/// SQLite database
|
||||
#[derive(Clone)]
|
||||
pub struct Database {
|
||||
/// Sqlite connection pool
|
||||
pub pool: SqlitePool,
|
||||
}
|
||||
|
||||
// TODO: Check UUID
|
||||
impl Database {
|
||||
/// Open a SQLite database
|
||||
pub async fn open(path: &str) -> Result<Self> {
|
||||
debug!("Opening SQLite database: {}", path);
|
||||
|
||||
// sqlite connection options
|
||||
let mut options = SqliteConnectOptions::from_str(path).map_err(Error::OpenDatabase)?;
|
||||
|
||||
// set log level to Debug
|
||||
options.log_statements(LevelFilter::Debug);
|
||||
|
||||
// create a database pool
|
||||
let pool = SqlitePool::connect_with(options)
|
||||
.await
|
||||
.map_err(Error::ConnectDatabase)?;
|
||||
|
||||
info!("Connected to database!");
|
||||
|
||||
Ok(Self { pool })
|
||||
}
|
||||
|
||||
/// Create all required tables for HomeDisk.
|
||||
pub async fn create_tables(&self) -> Result<SqliteQueryResult> {
|
||||
self.pool
|
||||
.execute(include_str!("../../tables.sql"))
|
||||
.await
|
||||
.map_err(Error::CreateTables)
|
||||
}
|
||||
|
||||
/// Create new user in the database.
|
||||
pub async fn create_user(&self, user: &User) -> Result<SqliteQueryResult> {
|
||||
debug!("Creating user - {}", user.username);
|
||||
|
||||
// build sql query
|
||||
let query = sqlx::query("INSERT INTO user (id, username, password) VALUES (?, ?, ?)")
|
||||
.bind(&user.id)
|
||||
.bind(&user.username)
|
||||
.bind(&user.password);
|
||||
|
||||
self.pool.execute(query).await.map_err(Error::Execute)
|
||||
}
|
||||
|
||||
/// Search for a user.
|
||||
pub async fn find_user(&self, user: &User) -> Result<User> {
|
||||
debug!("Searching for a user - {}", user.username);
|
||||
|
||||
// create query request to database
|
||||
let query = sqlx::query("SELECT * FROM user WHERE username = ? AND password = ?")
|
||||
.bind(&user.username |