refactor: ♻️ client server

This commit is contained in:
Ahmet Kaan GÜMÜŞ 2024-11-20 00:16:31 +03:00
parent 679e9b99bd
commit fecd38fda2
12 changed files with 12 additions and 3 deletions

15
server/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "rust-package-manager-server"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "0.7.7", features = ["multipart"] }
futures = "0.3.31"
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
sha3 = "0.10.8"
surrealdb = "2.0.4"
tokio = { version = "1.40.0", features = ["full"] }
tokio-util = { version = "0.7.12", features = ["io"] }
tower-http = { version = "0.6.1", features = ["cors"] }

53
server/src/database.rs Normal file
View file

@ -0,0 +1,53 @@
use std::{sync::LazyLock, time::Duration};
use surrealdb::{
engine::remote::ws::{Client, Ws},
Surreal,
};
use tokio::time::sleep;
use crate::package::package::Package;
static DB: LazyLock<Surreal<Client>> = LazyLock::new(Surreal::init);
pub async fn establish_connection() -> Result<(), surrealdb::Error> {
DB.connect::<Ws>("localhost:8000").await?;
DB.use_ns("Packages").await?;
DB.use_db("Packages").await
}
pub async fn is_alive() -> bool {
tokio::select! {
db_result = DB.health() => { match db_result {
Ok(_) => true,
Err(_) => false,
} },
_ = sleep(Duration::from_secs(1)) => false
}
}
pub async fn create_package(package: Package) -> Option<Package> {
DB.create::<Option<Package>>(package.get_name())
.content(package)
.await
.map_or_else(|_| None, |package| package)
}
pub async fn read_package(package_name: String) -> Option<Package> {
DB.select::<Vec<Package>>(package_name)
.await
.map_or_else(|_| None, |mut package| package.pop())
}
pub async fn update_package(package_name: String, package: Package) -> Option<Package> {
DB.update::<Vec<Package>>(package_name)
.content(package)
.await
.map_or_else(|_| None, |mut package| package.pop())
}
pub async fn delete_package(package_name: String) -> Option<Package> {
DB.delete::<Vec<Package>>(package_name)
.await
.map_or_else(|_| None, |mut package| package.pop())
}

1
server/src/http.rs Normal file
View file

@ -0,0 +1 @@

9
server/src/lib.rs Normal file
View file

@ -0,0 +1,9 @@
pub mod database;
pub mod http;
pub mod package;
pub mod routing;
pub const PACKAGE_PATH: &str = "/packages";
#[derive(Debug, Clone)]
pub struct AppState {}

13
server/src/main.rs Normal file
View file

@ -0,0 +1,13 @@
use rust_package_manager_server::{database, routing, AppState};
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
println!("Hello, world!");
let listener = TcpListener::bind("localhost:2345").await.unwrap();
database::establish_connection().await.unwrap();
let app_state = AppState {};
let router = routing::route(axum::extract::State(app_state)).await;
axum::serve(listener, router).await.unwrap();
}

2
server/src/package.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod package;
pub mod utils;

View file

@ -0,0 +1,92 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use sha3::{Digest, Sha3_512};
use surrealdb::sql::Datetime;
use tokio::{fs::File, io::AsyncReadExt};
use tokio_util::io::ReaderStream;
#[derive(Debug, Serialize, Deserialize)]
pub struct Package {
name: String,
publisher: Publisher,
version: Version,
size: u64,
hash: Vec<u8>,
dependencies: Vec<String>,
publish_date_time: Datetime,
last_update_date_time: Datetime,
location: String,
}
impl Package {
pub fn new(name: String, publisher: Publisher, version: Version) -> Self {
Self {
name,
publisher,
version,
size: 0,
hash: vec![],
dependencies: vec![],
publish_date_time: Datetime::default(),
last_update_date_time: Datetime::default(),
location: String::new(),
}
}
pub fn get_name(&self) -> String {
self.name.to_string()
}
pub fn get_location(&self) -> String {
self.location.to_string()
}
pub async fn serve(&self) -> Option<ReaderStream<File>> {
match File::open(self.get_location()).await {
Ok(package_file) => Some(ReaderStream::new(package_file)),
Err(_) => None,
}
}
pub fn set_location(&mut self, location: &String) {
self.location = location.to_owned()
}
pub fn get_dependencies(&self) -> &[String] {
self.dependencies.as_slice()
}
pub fn set_last_update_date_time(&mut self) {
self.last_update_date_time = Datetime::default();
}
pub async fn set_hash(&mut self) {
if let Ok(mut package_file) = File::open(self.get_location()).await {
let mut hasher = Sha3_512::new();
let mut data_buffer = vec![];
if let Ok(_) = package_file.read_to_end(&mut data_buffer).await {
hasher.update(data_buffer);
self.hash = hasher.finalize().to_vec();
}
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Version {
first: u8,
second: u8,
third: u8,
}
impl Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.first, self.second, self.third)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Publisher {
name: String,
}

View file

@ -0,0 +1,56 @@
use std::path::PathBuf;
use axum::extract::Multipart;
use tokio::{fs::File, io::AsyncWriteExt};
use tokio_util::io::ReaderStream;
use crate::{database, routing, PACKAGE_PATH};
use super::package::Package;
pub async fn create_package(package: routing::Package) -> Option<Package> {
let package = Package::new(package.name, package.publisher, package.version);
database::create_package(package).await
}
pub async fn read_package(package_name: String) -> Option<Package> {
database::read_package(package_name).await
}
pub async fn update_package(package_name: String, mut package: Package) -> Option<Package> {
for dependency in package.get_dependencies() {
database::read_package(dependency.to_string()).await?;
}
package.set_last_update_date_time();
database::update_package(package_name, package).await
}
pub async fn delete_package(package_name: String) -> Option<Package> {
database::delete_package(package_name).await
}
pub async fn download_package(package_name: String) -> Option<ReaderStream<File>> {
let package = crate::package::utils::read_package(package_name).await?;
let package_file_stream = package.serve().await?;
Some(package_file_stream)
}
pub async fn upload_package(mut package_file: Multipart) -> Option<Package> {
let package_file_part = package_file.next_field().await.ok()??;
let package_file_name = package_file_part.file_name()?.to_string();
let file_location = format!("./{}/{}", PACKAGE_PATH, package_file_name);
let file_location = PathBuf::from(file_location).canonicalize().ok()?;
let file_location = file_location.to_str()?;
let package_file_data = package_file_part.bytes().await.ok()?;
let mut package = crate::package::utils::read_package(package_file_name).await?;
let mut file_descriptor = File::create_new(&file_location).await.ok()?;
file_descriptor.write_all(&package_file_data).await.ok()?;
package.set_location(&file_location.to_string());
package.set_hash().await;
Some(package)
}

94
server/src/routing.rs Normal file
View file

@ -0,0 +1,94 @@
use axum::{
body::Body,
extract::{Multipart, Path, State},
http::StatusCode,
response::IntoResponse,
routing::{delete, get, patch, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use tower_http::cors::CorsLayer;
use crate::{
database,
package::package::{Publisher, Version},
AppState,
};
#[derive(Debug, Serialize, Deserialize)]
pub struct Package {
pub name: String,
pub publisher: Publisher,
pub version: Version,
}
pub async fn route(State(app_state): State<AppState>) -> Router {
Router::new()
.route("/", get(alive))
.route("/package", post(create_package))
.route("/package/:package_name", get(read_package))
.route("/package/:package_name", patch(update_package))
.route("/package/:package_name", delete(delete_package))
.route("/package/download/:package_name", get(download_package))
.route("/package/upload", post(upload_package))
.layer(CorsLayer::permissive())
.with_state(app_state)
}
async fn alive() -> impl IntoResponse {
let db_status = match database::is_alive().await {
true => "alive",
false => "dead",
};
let alive_json = Json(serde_json::json!({
"server_status": "alive",
"database_status": db_status
}));
(StatusCode::OK, alive_json)
}
async fn create_package(Json(package): Json<Package>) -> impl IntoResponse {
match crate::package::utils::create_package(package).await {
Some(package) => (StatusCode::CREATED, Json(serde_json::json!(package))),
None => (StatusCode::BAD_REQUEST, Json(serde_json::json!(""))),
}
}
async fn read_package(Path(package_name): Path<String>) -> impl IntoResponse {
match crate::package::utils::read_package(package_name).await {
Some(package) => (StatusCode::OK, Json(serde_json::json!(package))),
None => (StatusCode::BAD_REQUEST, Json(serde_json::json!(""))),
}
}
async fn update_package(
Path(package_name): Path<String>,
Json(package): Json<crate::package::package::Package>,
) -> impl IntoResponse {
match crate::package::utils::update_package(package_name, package).await {
Some(package) => (StatusCode::ACCEPTED, Json(serde_json::json!(package))),
None => (StatusCode::BAD_REQUEST, Json(serde_json::json!(""))),
}
}
async fn delete_package(Path(package_name): Path<String>) -> impl IntoResponse {
match crate::package::utils::delete_package(package_name).await {
Some(package) => (StatusCode::NO_CONTENT, Json(serde_json::json!(package))),
None => (StatusCode::BAD_REQUEST, Json(serde_json::json!(""))),
}
}
async fn download_package(Path(package_name): Path<String>) -> impl IntoResponse {
match crate::package::utils::download_package(package_name).await {
Some(package_file_stream) => (StatusCode::OK, Body::from_stream(package_file_stream)),
None => (StatusCode::BAD_REQUEST, Body::empty()),
}
}
async fn upload_package(package_file: Multipart) -> impl IntoResponse {
match crate::package::utils::upload_package(package_file).await {
Some(package) => (StatusCode::ACCEPTED, Json(serde_json::json!(package))),
None => (StatusCode::BAD_REQUEST, Json(serde_json::json!(""))),
}
}