Init
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
config.toml
|
||||||
|
oxspeak.db
|
||||||
|
target/
|
||||||
|
# artefact ...
|
||||||
|
oxspeak - Copie.db:Zone.Identifier
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
12
.idea/dataSources.xml
generated
Normal file
12
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="oxspeak.db" uuid="6f1740ef-4686-4ce9-b224-e6b0fd312908">
|
||||||
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:sqlite:file:\\wsl$\Debian\home\Nell\linux_dev\unix_oxspeak_server_v2\oxspeak.db?nolock=1</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/unix_oxspeak_server_v2.iml" filepath="$PROJECT_DIR$/.idea/unix_oxspeak_server_v2.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/unix_oxspeak_server_v2.iml
generated
Normal file
12
.idea/unix_oxspeak_server_v2.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="EMPTY_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/migration/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
3626
Cargo.lock
generated
Normal file
3626
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
63
Cargo.toml
Normal file
63
Cargo.toml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
[package]
|
||||||
|
name = "ox_speak_server"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
# The `_lib` suffix may seem redundant but it is necessary
|
||||||
|
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||||
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
|
name = "ox_speak_server_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = [".", "migration"]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
#debug = true
|
||||||
|
# poid minimal, rapidité baissé
|
||||||
|
#strip = true # Retire les symboles
|
||||||
|
#lto = true # Link Time Optimization
|
||||||
|
#codegen-units = 1 # Meilleure optimisation
|
||||||
|
#opt-level = "z" # Optimise pour la taille
|
||||||
|
|
||||||
|
# poid élevé, vitesse maximal
|
||||||
|
strip = true # Retire les symboles
|
||||||
|
lto = "thin" # LTO "léger", bon compromis vitesse/taille
|
||||||
|
codegen-units = 16 # Parallélise la compilation (défaut), plus rapide à compiler
|
||||||
|
opt-level = 3 # Optimisation maximale pour la vitesse
|
||||||
|
#panic = "abort" # Réduit la taille ET améliore légèrement les perfs
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Async
|
||||||
|
tokio = {version = "1.48", features = ["full"]}
|
||||||
|
|
||||||
|
# HTTP
|
||||||
|
#actix-web = "4.12"
|
||||||
|
#poem = "3.1"
|
||||||
|
#poem-openapi = { version="5.1", features = ["swagger-ui", "url", "chrono"]}
|
||||||
|
axum = { version = "0.8", features = ["macros", "ws"] }
|
||||||
|
#utoipa = "5.4"
|
||||||
|
#utoipa-swagger-ui = { version = "9.0", features = ["axum"] }
|
||||||
|
#tower = "0.5"
|
||||||
|
#tower-http = { version = "0.6", features = ["trace", "cors", "timeout"] }
|
||||||
|
|
||||||
|
# UDP
|
||||||
|
socket2 = "0.6"
|
||||||
|
|
||||||
|
# db
|
||||||
|
sea-orm = { version = "2.0.0-rc", features = ["sqlx-sqlite", "sqlx-postgres", "sqlx-mysql", "runtime-tokio", "with-chrono", "with-uuid", "with-json", "schema-sync"] }
|
||||||
|
migration = { path = "migration" }
|
||||||
|
|
||||||
|
# logs
|
||||||
|
log = "0.4.28"
|
||||||
|
env_logger = "0.11.8"
|
||||||
|
|
||||||
|
# utils
|
||||||
|
chrono = "0.4"
|
||||||
|
parking_lot = "0.12"
|
||||||
|
serde = { version = "1.0", features = ["default", "derive"] }
|
||||||
|
serde_json = { version = "1.0.145", features = ["default"]}
|
||||||
|
toml = "0.9"
|
||||||
|
validator = { version = "0.20", features = ["derive"] }
|
||||||
|
uuid = {version = "1", features = ["v4", "v7", "fast-rng", "serde"]}
|
||||||
41
config.example.toml
Normal file
41
config.example.toml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# OxSpeak Server Configuration
|
||||||
|
# Copy this file to config.toml and modify the values according to your needs
|
||||||
|
|
||||||
|
[server]
|
||||||
|
# Server listening address + port (TCP+UDP)
|
||||||
|
bind_addr = "0.0.0.0:7000"
|
||||||
|
# Mode: "debug" or "release"
|
||||||
|
mode = "release"
|
||||||
|
|
||||||
|
[database]
|
||||||
|
# Database type: "sqlite", "postgres" or "mysql"
|
||||||
|
type = "sqlite"
|
||||||
|
|
||||||
|
# SQLite configuration (default)
|
||||||
|
# Path to the database file
|
||||||
|
path = "./oxspeak.db"
|
||||||
|
|
||||||
|
# PostgreSQL configuration (uncomment and configure if type = "postgres")
|
||||||
|
# host = "localhost"
|
||||||
|
# port = 5432
|
||||||
|
# user = "postgres"
|
||||||
|
# password = "your_password"
|
||||||
|
# dbname = "oxspeak"
|
||||||
|
# sslmode = "disable"
|
||||||
|
|
||||||
|
# MySQL configuration (uncomment and configure if type = "mysql")
|
||||||
|
# host = "localhost"
|
||||||
|
# port = 3306
|
||||||
|
# user = "root"
|
||||||
|
# password = "your_password"
|
||||||
|
# dbname = "oxspeak"
|
||||||
|
|
||||||
|
[jwt]
|
||||||
|
# Secret key for JWT token generation (CHANGE THIS VALUE!)
|
||||||
|
secret = "your_very_secure_jwt_secret_to_change"
|
||||||
|
# Token validity duration in hours
|
||||||
|
expiration = 24
|
||||||
|
|
||||||
|
[udp]
|
||||||
|
# UDP server port (voice)
|
||||||
|
port = 9000
|
||||||
22
migration/Cargo.toml
Normal file
22
migration/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "migration"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "migration"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
||||||
|
|
||||||
|
[dependencies.sea-orm-migration]
|
||||||
|
version = "2.0.0-rc"
|
||||||
|
features = [
|
||||||
|
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
|
||||||
|
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
|
||||||
|
# e.g.
|
||||||
|
# "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
|
||||||
|
# "sqlx-postgres", # `DATABASE_DRIVER` feature
|
||||||
|
]
|
||||||
41
migration/README.md
Normal file
41
migration/README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Running Migrator CLI
|
||||||
|
|
||||||
|
- Generate a new migration file
|
||||||
|
```sh
|
||||||
|
cargo run -- generate MIGRATION_NAME
|
||||||
|
```
|
||||||
|
- Apply all pending migrations
|
||||||
|
```sh
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
```sh
|
||||||
|
cargo run -- up
|
||||||
|
```
|
||||||
|
- Apply first 10 pending migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- up -n 10
|
||||||
|
```
|
||||||
|
- Rollback last applied migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- down
|
||||||
|
```
|
||||||
|
- Rollback last 10 applied migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- down -n 10
|
||||||
|
```
|
||||||
|
- Drop all tables from the database, then reapply all migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- fresh
|
||||||
|
```
|
||||||
|
- Rollback all applied migrations, then reapply all migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- refresh
|
||||||
|
```
|
||||||
|
- Rollback all applied migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- reset
|
||||||
|
```
|
||||||
|
- Check the status of all migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- status
|
||||||
|
```
|
||||||
12
migration/src/lib.rs
Normal file
12
migration/src/lib.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
pub use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
mod m20220101_000001_create_table;
|
||||||
|
|
||||||
|
pub struct Migrator;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigratorTrait for Migrator {
|
||||||
|
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||||
|
vec![Box::new(m20220101_000001_create_table::Migration)]
|
||||||
|
}
|
||||||
|
}
|
||||||
555
migration/src/m20220101_000001_create_table.rs
Normal file
555
migration/src/m20220101_000001_create_table.rs
Normal file
@@ -0,0 +1,555 @@
|
|||||||
|
use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
// Create table `server`
|
||||||
|
manager.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Alias::new("server"))
|
||||||
|
.if_not_exists()
|
||||||
|
.col(
|
||||||
|
ColumnDef::new("id")
|
||||||
|
.uuid()
|
||||||
|
.primary_key()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new("name")
|
||||||
|
.string()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new("password")
|
||||||
|
.string()
|
||||||
|
.null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("created_at"))
|
||||||
|
.date_time()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp())
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("updated_at"))
|
||||||
|
.date_time()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp())
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// Create table `category`
|
||||||
|
manager.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Alias::new("category"))
|
||||||
|
.if_not_exists()
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
.primary_key()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("server_id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("name"))
|
||||||
|
.string()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("created_at"))
|
||||||
|
.date_time()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp())
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("updated_at"))
|
||||||
|
.date_time()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp())
|
||||||
|
)
|
||||||
|
// L'index sera créé après via manager.create_index
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_category_server")
|
||||||
|
.from(Alias::new("category"), Alias::new("server_id"))
|
||||||
|
.to(Alias::new("server"), Alias::new("id"))
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade)
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// Create table `channel`
|
||||||
|
manager.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Alias::new("channel"))
|
||||||
|
.if_not_exists()
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
.primary_key()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("server_id"))
|
||||||
|
.uuid()
|
||||||
|
.null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("category_id"))
|
||||||
|
.uuid()
|
||||||
|
.null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("position"))
|
||||||
|
.integer()
|
||||||
|
.not_null()
|
||||||
|
.default(0)
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("channel_type"))
|
||||||
|
.integer()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("name"))
|
||||||
|
.string()
|
||||||
|
.null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("created_at"))
|
||||||
|
.date_time()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp())
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("updated_at"))
|
||||||
|
.date_time()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp())
|
||||||
|
)
|
||||||
|
// Indexes créés après
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_channel_server")
|
||||||
|
.from(Alias::new("channel"), Alias::new("server_id"))
|
||||||
|
.to(Alias::new("server"), Alias::new("id"))
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
)
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_channel_category")
|
||||||
|
.from(Alias::new("channel"), Alias::new("category_id"))
|
||||||
|
.to(Alias::new("category"), Alias::new("id"))
|
||||||
|
.on_delete(ForeignKeyAction::SetNull)
|
||||||
|
)
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// Create table `user`
|
||||||
|
manager.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Alias::new("user"))
|
||||||
|
.if_not_exists()
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
.primary_key()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("username"))
|
||||||
|
.string()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("pub_key"))
|
||||||
|
.text()
|
||||||
|
.not_null()
|
||||||
|
.unique_key()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("created_at"))
|
||||||
|
.date_time()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp())
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("updated_at"))
|
||||||
|
.date_time()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp())
|
||||||
|
)
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// Create table `message`
|
||||||
|
manager.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Alias::new("message"))
|
||||||
|
.if_not_exists()
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
.primary_key()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("channel_id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("user_id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("content"))
|
||||||
|
.text()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("created_at"))
|
||||||
|
.date_time()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp())
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("edited_at"))
|
||||||
|
.date_time()
|
||||||
|
.null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("reply_to_id"))
|
||||||
|
.uuid()
|
||||||
|
.null()
|
||||||
|
)
|
||||||
|
// Indexes créés après
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_message_channel")
|
||||||
|
.from(Alias::new("message"), Alias::new("channel_id"))
|
||||||
|
.to(Alias::new("channel"), Alias::new("id"))
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
)
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_message_user")
|
||||||
|
.from(Alias::new("message"), Alias::new("user_id"))
|
||||||
|
.to(Alias::new("user"), Alias::new("id"))
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
)
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_message_reply_to")
|
||||||
|
.from(Alias::new("message"), Alias::new("reply_to_id"))
|
||||||
|
.to(Alias::new("message"), Alias::new("id"))
|
||||||
|
.on_delete(ForeignKeyAction::SetNull)
|
||||||
|
)
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// Create table `attachment`
|
||||||
|
manager.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Alias::new("attachment"))
|
||||||
|
.if_not_exists()
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
.primary_key()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("message_id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("filename"))
|
||||||
|
.string()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("file_size"))
|
||||||
|
.big_integer()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("mime_type"))
|
||||||
|
.string()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("created_at"))
|
||||||
|
.date_time()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp())
|
||||||
|
)
|
||||||
|
// Index créé après
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_attachment_message")
|
||||||
|
.from(Alias::new("attachment"), Alias::new("message_id"))
|
||||||
|
.to(Alias::new("message"), Alias::new("id"))
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
)
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// Create M2M table `server_user`
|
||||||
|
manager.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Alias::new("server_user"))
|
||||||
|
.if_not_exists()
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
.primary_key()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("server_id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("user_id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("username"))
|
||||||
|
.string()
|
||||||
|
.null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("joined_at"))
|
||||||
|
.date_time()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp())
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("updated_at"))
|
||||||
|
.date_time()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
// Indexes créés après
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_server_user_server")
|
||||||
|
.from(Alias::new("server_user"), Alias::new("server_id"))
|
||||||
|
.to(Alias::new("server"), Alias::new("id"))
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
)
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_server_user_user")
|
||||||
|
.from(Alias::new("server_user"), Alias::new("user_id"))
|
||||||
|
.to(Alias::new("user"), Alias::new("id"))
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
)
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// Create M2M table `channel_user`
|
||||||
|
manager.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Alias::new("channel_user"))
|
||||||
|
.if_not_exists()
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
.primary_key()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("channel_id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("user_id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null()
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("role"))
|
||||||
|
.string()
|
||||||
|
.not_null()
|
||||||
|
.default("member".to_owned())
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("joined_at"))
|
||||||
|
.date_time()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp())
|
||||||
|
)
|
||||||
|
// Indexes créés après
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_channel_user_channel")
|
||||||
|
.from(Alias::new("channel_user"), Alias::new("channel_id"))
|
||||||
|
.to(Alias::new("channel"), Alias::new("id"))
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
)
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_channel_user_user")
|
||||||
|
.from(Alias::new("channel_user"), Alias::new("user_id"))
|
||||||
|
.to(Alias::new("user"), Alias::new("id"))
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
)
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
// Création des INDEX après les tables
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
|
||||||
|
// category(server_id)
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_category_server_id")
|
||||||
|
.table(Alias::new("category"))
|
||||||
|
.col(Alias::new("server_id"))
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// channel(server_id)
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_channel_server_id")
|
||||||
|
.table(Alias::new("channel"))
|
||||||
|
.col(Alias::new("server_id"))
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// channel(category_id)
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_channel_category_id")
|
||||||
|
.table(Alias::new("channel"))
|
||||||
|
.col(Alias::new("category_id"))
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// message(channel_id)
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_message_channel_id")
|
||||||
|
.table(Alias::new("message"))
|
||||||
|
.col(Alias::new("channel_id"))
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// message(user_id)
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_message_user_id")
|
||||||
|
.table(Alias::new("message"))
|
||||||
|
.col(Alias::new("user_id"))
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// message(reply_to_id)
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_message_reply_to_id")
|
||||||
|
.table(Alias::new("message"))
|
||||||
|
.col(Alias::new("reply_to_id"))
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// attachment(message_id)
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_attachment_message_id")
|
||||||
|
.table(Alias::new("attachment"))
|
||||||
|
.col(Alias::new("message_id"))
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// server_user(server_id)
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_server_user_server_id")
|
||||||
|
.table(Alias::new("server_user"))
|
||||||
|
.col(Alias::new("server_id"))
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// server_user(user_id)
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_server_user_user_id")
|
||||||
|
.table(Alias::new("server_user"))
|
||||||
|
.col(Alias::new("user_id"))
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// unique (server_id, user_id)
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("uk_server_user_server_user")
|
||||||
|
.table(Alias::new("server_user"))
|
||||||
|
.col(Alias::new("server_id"))
|
||||||
|
.col(Alias::new("user_id"))
|
||||||
|
.unique()
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// channel_user(channel_id)
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_channel_user_channel_id")
|
||||||
|
.table(Alias::new("channel_user"))
|
||||||
|
.col(Alias::new("channel_id"))
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// channel_user(user_id)
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_channel_user_user_id")
|
||||||
|
.table(Alias::new("channel_user"))
|
||||||
|
.col(Alias::new("user_id"))
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// unique (channel_id, user_id)
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("uk_channel_user_channel_user")
|
||||||
|
.table(Alias::new("channel_user"))
|
||||||
|
.col(Alias::new("channel_id"))
|
||||||
|
.col(Alias::new("user_id"))
|
||||||
|
.unique()
|
||||||
|
.to_owned()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager.drop_table(Table::drop().table(Alias::new("channel_user")).to_owned()).await?;
|
||||||
|
manager.drop_table(Table::drop().table(Alias::new("server_user")).to_owned()).await?;
|
||||||
|
manager.drop_table(Table::drop().table(Alias::new("attachment")).to_owned()).await?;
|
||||||
|
manager.drop_table(Table::drop().table(Alias::new("message")).to_owned()).await?;
|
||||||
|
manager.drop_table(Table::drop().table(Alias::new("channel")).to_owned()).await?;
|
||||||
|
manager.drop_table(Table::drop().table(Alias::new("category")).to_owned()).await?;
|
||||||
|
manager.drop_table(Table::drop().table(Alias::new("user")).to_owned()).await?;
|
||||||
|
manager.drop_table(Table::drop().table(Alias::new("server")).to_owned()).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
6
migration/src/main.rs
Normal file
6
migration/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
#[async_std::main]
|
||||||
|
async fn main() {
|
||||||
|
cli::run_cli(migration::Migrator).await;
|
||||||
|
}
|
||||||
59
src/app/app.rs
Normal file
59
src/app/app.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
use crate::app::AppState;
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::database::Database;
|
||||||
|
use crate::network::http::HTTPServer;
|
||||||
|
use crate::network::udp::UDPServer;
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
config: Config,
|
||||||
|
|
||||||
|
db: Database,
|
||||||
|
udp_server: UDPServer,
|
||||||
|
http_server: HTTPServer
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub async fn init(config: Config) -> Self {
|
||||||
|
|
||||||
|
let db = Database::init(&config.database_url()).await.expect("Failed to initialize database");
|
||||||
|
|
||||||
|
let state = AppState::new(db.clone());
|
||||||
|
// let state = AppState::new();
|
||||||
|
|
||||||
|
let udp_server = UDPServer::new(config.bind_addr());
|
||||||
|
let http_server = HTTPServer::new(config.bind_addr(), state);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
db,
|
||||||
|
udp_server,
|
||||||
|
http_server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&self) {
|
||||||
|
println!("Application démarrée. Appuyez sur Ctrl+C pour arrêter.");
|
||||||
|
|
||||||
|
// Le select arbitre la course
|
||||||
|
tokio::select! {
|
||||||
|
// Branche 1 : Le serveur tourne. S'il crash ou finit (peu probable), on sort.
|
||||||
|
|
||||||
|
_ = self.udp_server.run() => {
|
||||||
|
println!("Le serveur UDP s'est arrêté de lui-même (erreur ?).");
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = self.http_server.run() => {
|
||||||
|
println!("Le serveur HTTP s'est arrêté de lui-même (erreur ?).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branche 2 : On écoute le signal d'arrêt.
|
||||||
|
_ = tokio::signal::ctrl_c() => {
|
||||||
|
println!("Signal d'arrêt reçu !");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Nettoyage et fermeture de l'application.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
5
src/app/mod.rs
Normal file
5
src/app/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
mod app;
|
||||||
|
mod state;
|
||||||
|
|
||||||
|
pub use app::App;
|
||||||
|
pub use state::AppState;
|
||||||
23
src/app/state.rs
Normal file
23
src/app/state.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use crate::database::Database;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub db: Database
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new(db: Database) -> Self {
|
||||||
|
Self { db }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[derive(Clone)]
|
||||||
|
// pub struct AppState {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// impl AppState {
|
||||||
|
// pub fn new() -> Self {
|
||||||
|
// Self { }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
160
src/config/config.rs
Normal file
160
src/config/config.rs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub server: ServerConfig,
|
||||||
|
pub database: DatabaseConfig,
|
||||||
|
pub jwt: JwtConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct ServerConfig {
|
||||||
|
pub bind_addr: String,
|
||||||
|
pub mode: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct DatabaseConfig {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub db_type: String,
|
||||||
|
|
||||||
|
// SQLite
|
||||||
|
pub path: Option<String>,
|
||||||
|
|
||||||
|
// PostgreSQL / MySQL
|
||||||
|
pub host: Option<String>,
|
||||||
|
pub port: Option<u16>,
|
||||||
|
pub user: Option<String>,
|
||||||
|
pub password: Option<String>,
|
||||||
|
pub dbname: Option<String>,
|
||||||
|
|
||||||
|
// PostgreSQL specific
|
||||||
|
pub sslmode: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct JwtConfig {
|
||||||
|
pub secret: String,
|
||||||
|
pub expiration: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// Loads configuration from a TOML file
|
||||||
|
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
|
||||||
|
let content = fs::read_to_string(path)
|
||||||
|
.map_err(|e| ConfigError::IoError(e.to_string()))?;
|
||||||
|
|
||||||
|
let config: Config = toml::from_str(&content)
|
||||||
|
.map_err(|e| ConfigError::ParseError(e.to_string()))?;
|
||||||
|
|
||||||
|
config.validate()?;
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates the configuration
|
||||||
|
fn validate(&self) -> Result<(), ConfigError> {
|
||||||
|
// Validate server mode
|
||||||
|
if self.server.mode != "debug" && self.server.mode != "release" {
|
||||||
|
return Err(ConfigError::ValidationError(
|
||||||
|
format!("Mode invalide: '{}'. Doit être 'debug' ou 'release'", self.server.mode)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate database type
|
||||||
|
match self.database.db_type.as_str() {
|
||||||
|
"sqlite" => {
|
||||||
|
if self.database.path.is_none() {
|
||||||
|
return Err(ConfigError::ValidationError(
|
||||||
|
"SQLite database path is required".to_string()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"postgres" | "mysql" => {
|
||||||
|
if self.database.host.is_none()
|
||||||
|
|| self.database.port.is_none()
|
||||||
|
|| self.database.user.is_none()
|
||||||
|
|| self.database.password.is_none()
|
||||||
|
|| self.database.dbname.is_none() {
|
||||||
|
return Err(ConfigError::ValidationError(
|
||||||
|
format!("Incomplete configuration for {}", self.database.db_type)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(ConfigError::ValidationError(
|
||||||
|
format!("Invalid database type: '{}'. Must be 'sqlite', 'postgres' or 'mysql'",
|
||||||
|
self.database.db_type)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate JWT secret
|
||||||
|
if self.jwt.secret == "your_very_secure_jwt_secret_to_change" {
|
||||||
|
eprintln!("⚠️ WARNING: You are using the default JWT secret! Change it in production!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.jwt.secret.len() < 32 {
|
||||||
|
eprintln!("⚠️ WARNING: JWT secret is too short (minimum 32 characters recommended)");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the database connection string
|
||||||
|
pub fn database_url(&self) -> String {
|
||||||
|
match self.database.db_type.as_str() {
|
||||||
|
"sqlite" => {
|
||||||
|
format!("sqlite://{}?mode=rwc", self.database.path.as_ref().unwrap())
|
||||||
|
}
|
||||||
|
"postgres" => {
|
||||||
|
format!(
|
||||||
|
"postgresql://{}:{}@{}:{}/{}?sslmode={}",
|
||||||
|
self.database.user.as_ref().unwrap(),
|
||||||
|
self.database.password.as_ref().unwrap(),
|
||||||
|
self.database.host.as_ref().unwrap(),
|
||||||
|
self.database.port.unwrap(),
|
||||||
|
self.database.dbname.as_ref().unwrap(),
|
||||||
|
self.database.sslmode.as_ref().unwrap_or(&"disable".to_string())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"mysql" => {
|
||||||
|
format!(
|
||||||
|
"mysql://{}:{}@{}:{}/{}",
|
||||||
|
self.database.user.as_ref().unwrap(),
|
||||||
|
self.database.password.as_ref().unwrap(),
|
||||||
|
self.database.host.as_ref().unwrap(),
|
||||||
|
self.database.port.unwrap(),
|
||||||
|
self.database.dbname.as_ref().unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => panic!("Unsupported database type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the SocketAddr to bind to
|
||||||
|
pub fn bind_addr(&self) -> std::net::SocketAddr {
|
||||||
|
self.server.bind_addr.parse().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
IoError(String),
|
||||||
|
ParseError(String),
|
||||||
|
ValidationError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ConfigError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ConfigError::IoError(msg) => write!(f, "IO Error: {}", msg),
|
||||||
|
ConfigError::ParseError(msg) => write!(f, "Parsing Error: {}", msg),
|
||||||
|
ConfigError::ValidationError(msg) => write!(f, "Validation Error: {}", msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ConfigError {}
|
||||||
3
src/config/mod.rs
Normal file
3
src/config/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod config;
|
||||||
|
|
||||||
|
pub use config::Config;
|
||||||
34
src/database/database.rs
Normal file
34
src/database/database.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
use sea_orm::{ConnectOptions, Database as SeaDatabase, DatabaseConnection, DbErr};
|
||||||
|
use migration::{Migrator, MigratorTrait};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Database {
|
||||||
|
pub connection: DatabaseConnection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub async fn init(dsn: &str) -> Result<Self, DbErr> {
|
||||||
|
let mut opt = ConnectOptions::new(dsn);
|
||||||
|
opt.max_connections(100)
|
||||||
|
.min_connections(5)
|
||||||
|
.connect_timeout(Duration::from_secs(8))
|
||||||
|
.acquire_timeout(Duration::from_secs(8))
|
||||||
|
.sqlx_logging(true)
|
||||||
|
.sqlx_logging_level(log::LevelFilter::Debug);
|
||||||
|
|
||||||
|
let connection = SeaDatabase::connect(opt).await?;
|
||||||
|
|
||||||
|
// On lance les migrations ici.
|
||||||
|
// Si ça échoue, le programme s'arrête proprement à l'init.
|
||||||
|
Migrator::up(&connection, None).await?;
|
||||||
|
|
||||||
|
Ok(Self { connection })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tu peux ajouter ici tes méthodes helpers si tu veux encapsuler SeaORM
|
||||||
|
// ex: pub async fn find_user(...)
|
||||||
|
pub fn get_connection(&self) -> &DatabaseConnection {
|
||||||
|
&self.connection
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/database/mod.rs
Normal file
3
src/database/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod database;
|
||||||
|
|
||||||
|
pub use database::Database;
|
||||||
9
src/lib.rs
Normal file
9
src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
pub mod config;
|
||||||
|
pub mod app;
|
||||||
|
pub mod network;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
pub mod database;
|
||||||
|
pub mod models;
|
||||||
|
pub mod serializers;
|
||||||
|
pub mod repositories;
|
||||||
17
src/main.rs
Normal file
17
src/main.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use ox_speak_server_lib::app::App;
|
||||||
|
use ox_speak_server_lib::config;
|
||||||
|
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let config = match config::Config::from_file("config.toml") {
|
||||||
|
Ok(config) => config,
|
||||||
|
Err(e) => panic!("Error loading configuration: {}", e)
|
||||||
|
};
|
||||||
|
println!("Configuration loaded: {:?}", config);
|
||||||
|
|
||||||
|
let app = App::init(config).await;
|
||||||
|
app.run().await;
|
||||||
|
}
|
||||||
45
src/models/attachment.rs
Normal file
45
src/models/attachment.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use sea_orm::prelude::async_trait::async_trait;
|
||||||
|
use sea_orm::Set;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
#[sea_orm(table_name = "attachment")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub message_id: Uuid,
|
||||||
|
pub filename: String,
|
||||||
|
pub file_size: i32,
|
||||||
|
pub mime_type: String,
|
||||||
|
pub created_at: DateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::message::Entity",
|
||||||
|
from = "Column::MessageId",
|
||||||
|
to = "super::message::Column::Id",
|
||||||
|
on_update = "NoAction",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
Message,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::message::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Message.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ActiveModelBehavior for ActiveModel {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: Set(Uuid::new_v4()),
|
||||||
|
..ActiveModelTrait::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/models/category.rs
Normal file
52
src/models/category.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use sea_orm::prelude::async_trait::async_trait;
|
||||||
|
use sea_orm::Set;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
#[sea_orm(table_name = "category")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub server_id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub created_at: DateTime,
|
||||||
|
pub updated_at: DateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_many = "super::channel::Entity")]
|
||||||
|
Channel,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::server::Entity",
|
||||||
|
from = "Column::ServerId",
|
||||||
|
to = "super::server::Column::Id",
|
||||||
|
on_update = "Cascade",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
Server,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::channel::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Channel.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::server::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Server.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ActiveModelBehavior for ActiveModel {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: Set(Uuid::new_v4()),
|
||||||
|
..ActiveModelTrait::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/models/channel.rs
Normal file
77
src/models/channel.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use sea_orm::prelude::async_trait::async_trait;
|
||||||
|
use sea_orm::Set;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
#[sea_orm(table_name = "channel")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub server_id: Option<Uuid>,
|
||||||
|
pub category_id: Option<Uuid>,
|
||||||
|
pub position: i32,
|
||||||
|
pub channel_type: i32,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub created_at: DateTime,
|
||||||
|
pub updated_at: DateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::category::Entity",
|
||||||
|
from = "Column::CategoryId",
|
||||||
|
to = "super::category::Column::Id",
|
||||||
|
on_update = "NoAction",
|
||||||
|
on_delete = "SetNull"
|
||||||
|
)]
|
||||||
|
Category,
|
||||||
|
#[sea_orm(has_many = "super::channel_user::Entity")]
|
||||||
|
ChannelUser,
|
||||||
|
#[sea_orm(has_many = "super::message::Entity")]
|
||||||
|
Message,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::server::Entity",
|
||||||
|
from = "Column::ServerId",
|
||||||
|
to = "super::server::Column::Id",
|
||||||
|
on_update = "NoAction",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
Server,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::category::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Category.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::channel_user::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::ChannelUser.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::message::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Message.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::server::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Server.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ActiveModelBehavior for ActiveModel {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: Set(Uuid::new_v4()),
|
||||||
|
..ActiveModelTrait::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/models/channel_user.rs
Normal file
58
src/models/channel_user.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use sea_orm::prelude::async_trait::async_trait;
|
||||||
|
use sea_orm::Set;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
#[sea_orm(table_name = "channel_user")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub channel_id: Uuid,
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub role: String,
|
||||||
|
pub joined_at: DateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::channel::Entity",
|
||||||
|
from = "Column::ChannelId",
|
||||||
|
to = "super::channel::Column::Id",
|
||||||
|
on_update = "NoAction",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
Channel,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::user::Entity",
|
||||||
|
from = "Column::UserId",
|
||||||
|
to = "super::user::Column::Id",
|
||||||
|
on_update = "NoAction",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
User,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::channel::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Channel.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::user::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::User.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ActiveModelBehavior for ActiveModel {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: Set(Uuid::new_v4()),
|
||||||
|
..ActiveModelTrait::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/models/message.rs
Normal file
77
src/models/message.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use sea_orm::prelude::async_trait::async_trait;
|
||||||
|
use sea_orm::Set;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
#[sea_orm(table_name = "message")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub channel_id: Uuid,
|
||||||
|
pub user_id: Uuid,
|
||||||
|
#[sea_orm(column_type = "Text")]
|
||||||
|
pub content: String,
|
||||||
|
pub created_at: DateTime,
|
||||||
|
pub edited_at: Option<DateTime>,
|
||||||
|
pub reply_to_id: Option<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_many = "super::attachment::Entity")]
|
||||||
|
Attachment,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::channel::Entity",
|
||||||
|
from = "Column::ChannelId",
|
||||||
|
to = "super::channel::Column::Id",
|
||||||
|
on_update = "NoAction",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
Channel,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "Entity",
|
||||||
|
from = "Column::ReplyToId",
|
||||||
|
to = "Column::Id",
|
||||||
|
on_update = "NoAction",
|
||||||
|
on_delete = "SetNull"
|
||||||
|
)]
|
||||||
|
SelfRef,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::user::Entity",
|
||||||
|
from = "Column::UserId",
|
||||||
|
to = "super::user::Column::Id",
|
||||||
|
on_update = "NoAction",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
User,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::attachment::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Attachment.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::channel::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Channel.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::user::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::User.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ActiveModelBehavior for ActiveModel {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: Set(Uuid::new_v4()),
|
||||||
|
..ActiveModelTrait::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/models/mod.rs
Normal file
12
src/models/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||||
|
|
||||||
|
pub mod prelude;
|
||||||
|
|
||||||
|
pub mod attachment;
|
||||||
|
pub mod category;
|
||||||
|
pub mod channel;
|
||||||
|
pub mod channel_user;
|
||||||
|
pub mod message;
|
||||||
|
pub mod server;
|
||||||
|
pub mod server_user;
|
||||||
|
pub mod user;
|
||||||
10
src/models/prelude.rs
Normal file
10
src/models/prelude.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||||
|
|
||||||
|
pub use super::attachment::Entity as Attachment;
|
||||||
|
pub use super::category::Entity as Category;
|
||||||
|
pub use super::channel::Entity as Channel;
|
||||||
|
pub use super::channel_user::Entity as ChannelUser;
|
||||||
|
pub use super::message::Entity as Message;
|
||||||
|
pub use super::server::Entity as Server;
|
||||||
|
pub use super::server_user::Entity as ServerUser;
|
||||||
|
pub use super::user::Entity as User;
|
||||||
54
src/models/server.rs
Normal file
54
src/models/server.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use sea_orm::prelude::async_trait::async_trait;
|
||||||
|
use sea_orm::Set;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
#[sea_orm(table_name = "server")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub password: Option<String>,
|
||||||
|
pub created_at: DateTime,
|
||||||
|
pub updated_at: DateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_many = "super::category::Entity")]
|
||||||
|
Category,
|
||||||
|
#[sea_orm(has_many = "super::channel::Entity")]
|
||||||
|
Channel,
|
||||||
|
#[sea_orm(has_many = "super::server_user::Entity")]
|
||||||
|
ServerUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::category::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Category.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::channel::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Channel.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::server_user::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::ServerUser.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ActiveModelBehavior for ActiveModel {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: Set(Uuid::new_v4()),
|
||||||
|
..ActiveModelTrait::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/models/server_user.rs
Normal file
59
src/models/server_user.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use sea_orm::prelude::async_trait::async_trait;
|
||||||
|
use sea_orm::Set;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
#[sea_orm(table_name = "server_user")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub server_id: Uuid,
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub username: Option<String>,
|
||||||
|
pub joined_at: DateTime,
|
||||||
|
pub updated_at: DateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::server::Entity",
|
||||||
|
from = "Column::ServerId",
|
||||||
|
to = "super::server::Column::Id",
|
||||||
|
on_update = "NoAction",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
Server,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::user::Entity",
|
||||||
|
from = "Column::UserId",
|
||||||
|
to = "super::user::Column::Id",
|
||||||
|
on_update = "NoAction",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
User,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::server::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Server.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::user::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::User.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ActiveModelBehavior for ActiveModel {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: Set(Uuid::new_v4()),
|
||||||
|
..ActiveModelTrait::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/models/user.rs
Normal file
55
src/models/user.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use sea_orm::prelude::async_trait::async_trait;
|
||||||
|
use sea_orm::Set;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
#[sea_orm(table_name = "user")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub username: String,
|
||||||
|
#[sea_orm(column_type = "Text", unique)]
|
||||||
|
pub pub_key: String,
|
||||||
|
pub created_at: DateTime,
|
||||||
|
pub updated_at: DateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_many = "super::channel_user::Entity")]
|
||||||
|
ChannelUser,
|
||||||
|
#[sea_orm(has_many = "super::message::Entity")]
|
||||||
|
Message,
|
||||||
|
#[sea_orm(has_many = "super::server_user::Entity")]
|
||||||
|
ServerUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::channel_user::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::ChannelUser.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::message::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Message.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::server_user::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::ServerUser.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ActiveModelBehavior for ActiveModel {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: Set(Uuid::new_v4()),
|
||||||
|
..ActiveModelTrait::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/network/http/context.rs
Normal file
20
src/network/http/context.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
use std::time::Instant;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::app::AppState;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct RequestContext {
|
||||||
|
pub request_id: Uuid,
|
||||||
|
pub started_at: Instant,
|
||||||
|
pub method: axum::http::Method,
|
||||||
|
pub uri: axum::http::Uri,
|
||||||
|
pub user: Option<CurrentUser>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CurrentUser {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
49
src/network/http/error.rs
Normal file
49
src/network/http/error.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use axum::Json;
|
||||||
|
use serde_json::json;
|
||||||
|
use sea_orm::DbErr;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum HTTPError {
|
||||||
|
Database(DbErr),
|
||||||
|
NotFound,
|
||||||
|
BadRequest(String),
|
||||||
|
InternalServerError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversion automatique depuis DbErr (erreurs SeaORM)
|
||||||
|
impl From<DbErr> for HTTPError {
|
||||||
|
fn from(err: DbErr) -> Self {
|
||||||
|
HTTPError::Database(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversion depuis ParseError (pour UUID, etc.)
|
||||||
|
impl From<uuid::Error> for HTTPError {
|
||||||
|
fn from(err: uuid::Error) -> Self {
|
||||||
|
HTTPError::BadRequest(format!("Invalid UUID: {}", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implémentation pour Axum : transformer AppError en réponse HTTP
|
||||||
|
impl IntoResponse for HTTPError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let (status, error_message) = match self {
|
||||||
|
HTTPError::Database(err) => {
|
||||||
|
eprintln!("Database error: {:?}", err);
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "Database error")
|
||||||
|
}
|
||||||
|
HTTPError::NotFound => (StatusCode::NOT_FOUND, "Resource not found"),
|
||||||
|
HTTPError::BadRequest(msg) => {
|
||||||
|
return (StatusCode::BAD_REQUEST, Json(json!({ "error": msg }))).into_response();
|
||||||
|
}
|
||||||
|
HTTPError::InternalServerError(msg) => {
|
||||||
|
eprintln!("Internal error: {}", msg);
|
||||||
|
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": msg }))).into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(status, Json(json!({ "error": error_message }))).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/network/http/middleware.rs
Normal file
56
src/network/http/middleware.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
http::Request,
|
||||||
|
middleware::Next,
|
||||||
|
response::Response,
|
||||||
|
};
|
||||||
|
use std::time::Instant;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::app::AppState;
|
||||||
|
use crate::network::http::context::{CurrentUser, RequestContext};
|
||||||
|
|
||||||
|
pub async fn context_middleware(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
mut req: Request<axum::body::Body>,
|
||||||
|
next: Next,
|
||||||
|
) -> Response {
|
||||||
|
let request_id = Uuid::new_v4();
|
||||||
|
let started_at = Instant::now();
|
||||||
|
|
||||||
|
// Infos "type Django request"
|
||||||
|
let method = req.method().clone();
|
||||||
|
let uri = req.uri().clone();
|
||||||
|
|
||||||
|
// Exemple: récupérer un user depuis un token (pseudo-code)
|
||||||
|
// Ici je laisse volontairement une logique minimaliste/placeholder.
|
||||||
|
// Le but: montrer où tu branches ta vraie auth.
|
||||||
|
let user: Option<CurrentUser> = {
|
||||||
|
let _maybe_auth = req
|
||||||
|
.headers()
|
||||||
|
.get(axum::http::header::AUTHORIZATION)
|
||||||
|
.and_then(|v| v.to_str().ok());
|
||||||
|
|
||||||
|
// TODO: vérifier token -> user_id -> charger en DB avec app_state
|
||||||
|
// Some(CurrentUser { id: ..., username: ... })
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Injecte le contexte dans la requête
|
||||||
|
req.extensions_mut().insert(RequestContext {
|
||||||
|
request_id,
|
||||||
|
started_at,
|
||||||
|
method: method.clone(),
|
||||||
|
uri: uri.clone(),
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
|
||||||
|
println!(">>> Incoming [{}] {} {}", request_id, method, uri);
|
||||||
|
|
||||||
|
// Passe la requête au reste de la stack
|
||||||
|
let resp = next.run(req).await;
|
||||||
|
|
||||||
|
println!("<<< Response [{}]: {}", request_id, resp.status());
|
||||||
|
|
||||||
|
resp
|
||||||
|
}
|
||||||
15
src/network/http/mod.rs
Normal file
15
src/network/http/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use axum::Router;
|
||||||
|
use crate::app::AppState;
|
||||||
|
|
||||||
|
mod server;
|
||||||
|
mod router;
|
||||||
|
mod middleware;
|
||||||
|
mod web;
|
||||||
|
mod error;
|
||||||
|
mod context;
|
||||||
|
|
||||||
|
pub use server::HTTPServer;
|
||||||
|
pub use error::HTTPError;
|
||||||
|
pub use context::RequestContext;
|
||||||
|
|
||||||
|
pub type AppRouter = Router<AppState>;
|
||||||
12
src/network/http/router.rs
Normal file
12
src/network/http/router.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use axum::{middleware, Router};
|
||||||
|
use crate::app::AppState;
|
||||||
|
use crate::network::http::middleware::context_middleware;
|
||||||
|
use crate::network::http::{web, AppRouter};
|
||||||
|
|
||||||
|
pub fn setup_route(app_state: AppState) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.merge(web::setup_route())
|
||||||
|
.layer(middleware::from_fn_with_state(app_state.clone(), context_middleware))
|
||||||
|
.with_state(app_state)
|
||||||
|
}
|
||||||
24
src/network/http/server.rs
Normal file
24
src/network/http/server.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use crate::app::AppState;
|
||||||
|
use crate::network::http::router::setup_route;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HTTPServer {
|
||||||
|
bind_addr: SocketAddr,
|
||||||
|
app_state: AppState
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HTTPServer {
|
||||||
|
pub fn new(bind_addr: SocketAddr, app_state: AppState) -> Self {
|
||||||
|
Self {bind_addr, app_state}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&self) -> std::io::Result<()> {
|
||||||
|
let route = setup_route(self.app_state.clone());
|
||||||
|
let listener = TcpListener::bind(&self.bind_addr).await?;
|
||||||
|
|
||||||
|
axum::serve(listener, route).await
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/network/http/web/api/category.rs
Normal file
87
src/network/http/web/api/category.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
use axum::{Extension, Json};
|
||||||
|
use axum::extract::{Path, State};
|
||||||
|
use axum::http::{Extensions, StatusCode};
|
||||||
|
use axum::routing::{delete, get, post, put};
|
||||||
|
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::app::AppState;
|
||||||
|
use crate::models::category;
|
||||||
|
use crate::network::http::{AppRouter, HTTPError};
|
||||||
|
use crate::network::http::RequestContext;
|
||||||
|
use crate::serializers::CategorySerializer;
|
||||||
|
|
||||||
|
pub fn setup_route() -> AppRouter {
|
||||||
|
AppRouter::new()
|
||||||
|
.route("/categories/", get(category_list))
|
||||||
|
.route("/categories/{id}/", get(category_detail))
|
||||||
|
.route("/categories/", post(category_create))
|
||||||
|
.route("/categories/{id}/", put(category_update))
|
||||||
|
.route("/categories/{id}/", delete(category_delete))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn category_list(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Extension(ctx): Extension<RequestContext>
|
||||||
|
) -> Result<Json<Vec<CategorySerializer>>, HTTPError> {
|
||||||
|
let categories = category::Entity::find()
|
||||||
|
.all(app_state.db.get_connection())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(categories.into_iter().map(CategorySerializer::from).collect()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn category_detail(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>
|
||||||
|
) -> Result<Json<CategorySerializer>, HTTPError> {
|
||||||
|
let category = category::Entity::find_by_id(id)
|
||||||
|
.one(app_state.db.get_connection())
|
||||||
|
.await?
|
||||||
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
|
Ok(Json(CategorySerializer::from(category)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn category_create(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Json(serializer): Json<CategorySerializer>
|
||||||
|
) -> Result<Json<CategorySerializer>, HTTPError> {
|
||||||
|
let active = serializer.into_active_model();
|
||||||
|
let category: category::Model = active.insert(app_state.db.get_connection()).await?;
|
||||||
|
|
||||||
|
Ok(Json(CategorySerializer::from(category)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn category_update(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
Json(serializer): Json<CategorySerializer>,
|
||||||
|
) -> Result<Json<CategorySerializer>, HTTPError> {
|
||||||
|
let category = category::Entity::find_by_id(id)
|
||||||
|
.one(app_state.db.get_connection())
|
||||||
|
.await?
|
||||||
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
|
let active = category.into_active_model();
|
||||||
|
|
||||||
|
let category: category::Model = serializer.apply_to_active_model(active)
|
||||||
|
.update(app_state.db.get_connection())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(CategorySerializer::from(category)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn category_delete(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>
|
||||||
|
) -> Result<StatusCode, HTTPError> {
|
||||||
|
let result = category::Entity::delete_by_id(id)
|
||||||
|
.exec(app_state.db.get_connection())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if result.rows_affected == 0 {
|
||||||
|
Err(HTTPError::NotFound)
|
||||||
|
} else {
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/network/http/web/api/channel.rs
Normal file
85
src/network/http/web/api/channel.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
use axum::Json;
|
||||||
|
use axum::extract::{Path, State};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::routing::{delete, get, post, put};
|
||||||
|
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::app::AppState;
|
||||||
|
use crate::models::channel;
|
||||||
|
use crate::network::http::{AppRouter, HTTPError};
|
||||||
|
use crate::serializers::ChannelSerializer;
|
||||||
|
|
||||||
|
pub fn setup_route() -> AppRouter {
|
||||||
|
AppRouter::new()
|
||||||
|
.route("/channels/", get(channel_list))
|
||||||
|
.route("/channels/{id}/", get(channel_detail))
|
||||||
|
.route("/channels/", post(channel_create))
|
||||||
|
.route("/channels/{id}/", put(channel_update))
|
||||||
|
.route("/channels/{id}/", delete(channel_delete))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn channel_list(
|
||||||
|
State(app_state): State<AppState>
|
||||||
|
) -> Result<Json<Vec<ChannelSerializer>>, HTTPError> {
|
||||||
|
let channels = channel::Entity::find()
|
||||||
|
.all(app_state.db.get_connection())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(channels.into_iter().map(ChannelSerializer::from).collect()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn channel_detail(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>
|
||||||
|
) -> Result<Json<ChannelSerializer>, HTTPError> {
|
||||||
|
let channel = channel::Entity::find_by_id(id)
|
||||||
|
.one(app_state.db.get_connection())
|
||||||
|
.await?
|
||||||
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
|
Ok(Json(ChannelSerializer::from(channel)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn channel_create(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Json(serializer): Json<ChannelSerializer>
|
||||||
|
) -> Result<Json<ChannelSerializer>, HTTPError> {
|
||||||
|
let active = serializer.into_active_model();
|
||||||
|
let channel: channel::Model = active.insert(app_state.db.get_connection()).await?;
|
||||||
|
|
||||||
|
Ok(Json(ChannelSerializer::from(channel)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn channel_update(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
Json(serializer): Json<ChannelSerializer>,
|
||||||
|
) -> Result<Json<ChannelSerializer>, HTTPError> {
|
||||||
|
let channel = channel::Entity::find_by_id(id)
|
||||||
|
.one(app_state.db.get_connection())
|
||||||
|
.await?
|
||||||
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
|
let active = channel.into_active_model();
|
||||||
|
|
||||||
|
let channel: channel::Model = serializer.apply_to_active_model(active)
|
||||||
|
.update(app_state.db.get_connection())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(ChannelSerializer::from(channel)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn channel_delete(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>
|
||||||
|
) -> Result<StatusCode, HTTPError> {
|
||||||
|
let result = channel::Entity::delete_by_id(id)
|
||||||
|
.exec(app_state.db.get_connection())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if result.rows_affected > 0 {
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
|
} else {
|
||||||
|
Err(HTTPError::NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/network/http/web/api/message.rs
Normal file
84
src/network/http/web/api/message.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use axum::Json;
|
||||||
|
use axum::extract::{Path, State};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::routing::{delete, get, post, put};
|
||||||
|
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::app::AppState;
|
||||||
|
use crate::models::message;
|
||||||
|
use crate::network::http::{AppRouter, HTTPError};
|
||||||
|
use crate::serializers::MessageSerializer;
|
||||||
|
|
||||||
|
pub fn setup_route() -> AppRouter {
|
||||||
|
AppRouter::new()
|
||||||
|
.route("/messages/", get(message_list))
|
||||||
|
.route("/messages/{id}/", get(message_detail))
|
||||||
|
.route("/messages/", post(message_create))
|
||||||
|
.route("/messages/{id}/", put(message_update))
|
||||||
|
.route("/messages/{id}/", delete(message_delete))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn message_list(
|
||||||
|
State(app_state): State<AppState>
|
||||||
|
) -> Result<Json<Vec<MessageSerializer>>, HTTPError> {
|
||||||
|
let messages = message::Entity::find()
|
||||||
|
.all(app_state.db.get_connection())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(messages.into_iter().map(MessageSerializer::from).collect()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn message_detail(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>
|
||||||
|
) -> Result<Json<MessageSerializer>, HTTPError> {
|
||||||
|
let message = message::Entity::find_by_id(id)
|
||||||
|
.one(app_state.db.get_connection())
|
||||||
|
.await?
|
||||||
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
|
Ok(Json(MessageSerializer::from(message)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn message_create(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Json(serializer): Json<MessageSerializer>
|
||||||
|
) -> Result<Json<MessageSerializer>, HTTPError> {
|
||||||
|
let active = serializer.into_active_model();
|
||||||
|
let message: message::Model = active.insert(app_state.db.get_connection()).await?;
|
||||||
|
|
||||||
|
Ok(Json(MessageSerializer::from(message)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn message_update(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
Json(serializer): Json<MessageSerializer>,
|
||||||
|
) -> Result<Json<MessageSerializer>, HTTPError> {
|
||||||
|
let message = message::Entity::find_by_id(id)
|
||||||
|
.one(app_state.db.get_connection())
|
||||||
|
.await?
|
||||||
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
|
let active = message.into_active_model();
|
||||||
|
let message: message::Model = serializer.apply_to_active_model(active)
|
||||||
|
.update(app_state.db.get_connection())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(MessageSerializer::from(message)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn message_delete(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>
|
||||||
|
) -> Result<StatusCode, HTTPError> {
|
||||||
|
let result = message::Entity::delete_by_id(id)
|
||||||
|
.exec(app_state.db.get_connection())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if result.rows_affected == 0 {
|
||||||
|
Err(HTTPError::NotFound)
|
||||||
|
} else {
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/network/http/web/api/mod.rs
Normal file
15
src/network/http/web/api/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use crate::network::http::AppRouter;
|
||||||
|
|
||||||
|
mod category;
|
||||||
|
mod channel;
|
||||||
|
mod message;
|
||||||
|
mod server;
|
||||||
|
|
||||||
|
pub fn setup_route() -> AppRouter {
|
||||||
|
|
||||||
|
AppRouter::new()
|
||||||
|
.nest("/category", category::setup_route())
|
||||||
|
.nest("/channel", channel::setup_route())
|
||||||
|
.nest("/message", message::setup_route())
|
||||||
|
.nest("/server", server::setup_route())
|
||||||
|
}
|
||||||
85
src/network/http/web/api/server.rs
Normal file
85
src/network/http/web/api/server.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
use axum::Json;
|
||||||
|
use axum::extract::{Path, State};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::routing::{delete, get, post, put};
|
||||||
|
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::app::AppState;
|
||||||
|
use crate::models::server;
|
||||||
|
use crate::network::http::{AppRouter, HTTPError};
|
||||||
|
use crate::serializers::ServerSerializer;
|
||||||
|
|
||||||
|
pub fn setup_route() -> AppRouter {
|
||||||
|
AppRouter::new()
|
||||||
|
.route("/servers/", get(server_list))
|
||||||
|
.route("/servers/{id}/", get(server_detail))
|
||||||
|
.route("/servers/{id}/", post(server_create))
|
||||||
|
.route("/servers/{id}/", put(server_update))
|
||||||
|
.route("/servers/{id}/", delete(server_delete))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn server_list(
|
||||||
|
State(app_state): State<AppState>
|
||||||
|
) -> Result<Json<Vec<ServerSerializer>>, HTTPError> {
|
||||||
|
let servers = server::Entity::find()
|
||||||
|
.all(app_state.db.get_connection())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(servers.into_iter().map(ServerSerializer::from).collect()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn server_detail(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>
|
||||||
|
) -> Result<Json<ServerSerializer>, HTTPError> {
|
||||||
|
let server = server::Entity::find_by_id(id)
|
||||||
|
.one(app_state.db.get_connection())
|
||||||
|
.await?
|
||||||
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
|
Ok(Json(ServerSerializer::from(server)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn server_create(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Json(serializer): Json<ServerSerializer>
|
||||||
|
) -> Result<Json<ServerSerializer>, HTTPError> {
|
||||||
|
let active = serializer.into_active_model();
|
||||||
|
let server: server::Model = active.insert(app_state.db.get_connection()).await?;
|
||||||
|
|
||||||
|
Ok(Json(ServerSerializer::from(server)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn server_update(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
Json(serializer): Json<ServerSerializer>,
|
||||||
|
) -> Result<Json<ServerSerializer>, HTTPError> {
|
||||||
|
let server = server::Entity::find_by_id(id)
|
||||||
|
.one(app_state.db.get_connection())
|
||||||
|
.await?
|
||||||
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
|
let active = server.into_active_model();
|
||||||
|
|
||||||
|
let server: server::Model = serializer.apply_to_active_model(active)
|
||||||
|
.update(app_state.db.get_connection())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(ServerSerializer::from(server)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn server_delete(
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>
|
||||||
|
) -> Result<StatusCode, HTTPError> {
|
||||||
|
let result = server::Entity::delete_by_id(id)
|
||||||
|
.exec(app_state.db.get_connection())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if result.rows_affected == 0 {
|
||||||
|
Err(HTTPError::NotFound)
|
||||||
|
} else {
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/network/http/web/auth.rs
Normal file
0
src/network/http/web/auth.rs
Normal file
11
src/network/http/web/mod.rs
Normal file
11
src/network/http/web/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
use axum::Router;
|
||||||
|
use crate::app::AppState;
|
||||||
|
use crate::network::http::AppRouter;
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
|
||||||
|
|
||||||
|
pub fn setup_route() -> AppRouter {
|
||||||
|
AppRouter::new()
|
||||||
|
.nest("/api", api::setup_route())
|
||||||
|
}
|
||||||
2
src/network/mod.rs
Normal file
2
src/network/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod udp;
|
||||||
|
pub mod http;
|
||||||
3
src/network/udp/mod.rs
Normal file
3
src/network/udp/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod server;
|
||||||
|
|
||||||
|
pub use server::UDPServer;
|
||||||
114
src/network/udp/server.rs
Normal file
114
src/network/udp/server.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread::available_parallelism;
|
||||||
|
use std::io;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use tokio::task;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct UDPServer {
|
||||||
|
table_router: Arc<RwLock<HashMap<String, SocketAddr>>>,
|
||||||
|
bind_addr: SocketAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UDPServer {
|
||||||
|
pub fn new(bind_addr: SocketAddr) -> Self {
|
||||||
|
Self {
|
||||||
|
table_router: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
bind_addr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&self) -> io::Result<()> {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
self.run_unix().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
self.run_windows().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(unix, windows)))]
|
||||||
|
{
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "Unsupported platform"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn run_unix(&self) -> io::Result<()> {
|
||||||
|
use socket2::{Domain, Protocol, Socket, Type};
|
||||||
|
|
||||||
|
let mut workers = Vec::new();
|
||||||
|
|
||||||
|
for id in available_parallelism() {
|
||||||
|
let bind_addr = self.bind_addr.clone();
|
||||||
|
|
||||||
|
let domain = match bind_addr {
|
||||||
|
SocketAddr::V4(_) => Domain::IPV4,
|
||||||
|
SocketAddr::V6(_) => Domain::IPV6,
|
||||||
|
};
|
||||||
|
|
||||||
|
let sock = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
|
||||||
|
sock.set_reuse_address(true)?;
|
||||||
|
sock.set_reuse_port(true)?;
|
||||||
|
sock.bind(&bind_addr.into())?;
|
||||||
|
|
||||||
|
let std_sock = std::net::UdpSocket::from(sock);
|
||||||
|
std_sock.set_nonblocking(true)?;
|
||||||
|
let udp = UdpSocket::from_std(std_sock)?;
|
||||||
|
|
||||||
|
let buffer_size = 1500;
|
||||||
|
let worker = task::spawn(async move {
|
||||||
|
if let Err(e) = Self::worker_loop(udp, buffer_size).await {
|
||||||
|
eprintln!("Worker loop error: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
workers.push(worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
for worker in workers {
|
||||||
|
let _ = worker.await;
|
||||||
|
}
|
||||||
|
println!("All UDP workers stopped.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
async fn run_windows(&self) -> io::Result<()> {
|
||||||
|
let udp = UdpSocket::bind(self.bind_addr).await?;
|
||||||
|
let udp = Arc::new(udp);
|
||||||
|
|
||||||
|
let mut workers = Vec::with_capacity(self.workers);
|
||||||
|
for id in 0..self.workers {
|
||||||
|
let sock = udp.clone();
|
||||||
|
let buf_size = 1500;
|
||||||
|
let worker = task::spawn(async move {
|
||||||
|
if let Err(e) = Self::worker_loop(udp, buffer_size) {
|
||||||
|
eprintln!("Worker loop error: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
workers.push(worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
for worker in workers {
|
||||||
|
let _ = worker.await;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn worker_loop(socket: UdpSocket, buffer_size: usize) -> io::Result<()>{
|
||||||
|
let mut buffer = vec![0u8; buffer_size];
|
||||||
|
loop {
|
||||||
|
let (size, peer) = socket.recv_from(&mut buffer).await?;
|
||||||
|
Self::handle_packet(&socket, &buffer[..size]).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_packet(socket: &UdpSocket, packet: &[u8]){
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/repositories/mod.rs
Normal file
26
src/repositories/mod.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
use crate::repositories::server::ServerRepository;
|
||||||
|
|
||||||
|
mod server;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RepositoryContext {
|
||||||
|
db: DatabaseConnection,
|
||||||
|
// pub events: EventBus, // si tu veux publier des events “post-save” plus tard
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Repositories {
|
||||||
|
pub server: ServerRepository,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Repositories {
|
||||||
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
|
let context = Arc::new(RepositoryContext { db });
|
||||||
|
|
||||||
|
Self {
|
||||||
|
server: ServerRepository {context: context.clone()},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/repositories/server.rs
Normal file
33
src/repositories/server.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use sea_orm::{DbErr, EntityTrait, ActiveModelTrait};
|
||||||
|
use crate::models::server;
|
||||||
|
use crate::repositories::RepositoryContext;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ServerRepository {
|
||||||
|
pub context: Arc<RepositoryContext>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerRepository {
|
||||||
|
pub async fn get_by_id(&self, id: uuid::Uuid) -> Result<Option<server::Model>, DbErr> {
|
||||||
|
server::Entity::find_by_id(id).one(&self.context.db).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update(&self, active: server::ActiveModel) -> Result<server::Model, DbErr> {
|
||||||
|
let model = active.update(&self.context.db).await?;
|
||||||
|
// plus tard: self.context.events.publish(...)
|
||||||
|
Ok(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create(&self, active: server::ActiveModel) -> Result<server::Model, DbErr> {
|
||||||
|
let model = active.insert(&self.context.db).await?;
|
||||||
|
// plus tard: emit post-save
|
||||||
|
Ok(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> {
|
||||||
|
server::Entity::delete_by_id(id).exec(&self.context.db).await?;
|
||||||
|
// plus tard: emit post-delete
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/serializers/category.rs
Normal file
48
src/serializers/category.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::models::category;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct CategorySerializer {
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
pub id: Option<Uuid>,
|
||||||
|
|
||||||
|
pub server_id: Uuid,
|
||||||
|
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<category::Model> for CategorySerializer {
|
||||||
|
fn from(model: category::Model) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Some(model.id),
|
||||||
|
server_id: model.server_id,
|
||||||
|
name: model.name,
|
||||||
|
created_at: Some(model.created_at.to_string()),
|
||||||
|
updated_at: Some(model.updated_at.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CategorySerializer {
|
||||||
|
pub fn into_active_model(self) -> category::ActiveModel {
|
||||||
|
category::ActiveModel {
|
||||||
|
server_id: Set(self.server_id),
|
||||||
|
name: Set(self.name),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_to_active_model(self, mut active_model: category::ActiveModel) -> category::ActiveModel {
|
||||||
|
active_model.server_id = Set(self.server_id);
|
||||||
|
active_model.name = Set(self.name);
|
||||||
|
active_model
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/serializers/channel.rs
Normal file
57
src/serializers/channel.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::models::channel;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct ChannelSerializer {
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
pub id: Option<Uuid>,
|
||||||
|
pub server_id: Option<Uuid>,
|
||||||
|
pub category_id: Option<Uuid>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub position: Option<i32>,
|
||||||
|
pub channel_type: i32,
|
||||||
|
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<channel::Model> for ChannelSerializer {
|
||||||
|
fn from(model: channel::Model) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Some(model.id),
|
||||||
|
server_id: model.server_id,
|
||||||
|
category_id: model.category_id,
|
||||||
|
name: model.name,
|
||||||
|
position: Some(model.position),
|
||||||
|
channel_type: model.channel_type,
|
||||||
|
created_at: Some(model.created_at.to_string()),
|
||||||
|
updated_at: Some(model.updated_at.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelSerializer {
|
||||||
|
pub fn into_active_model(self) -> channel::ActiveModel {
|
||||||
|
channel::ActiveModel {
|
||||||
|
server_id: Set(self.server_id),
|
||||||
|
category_id: Set(self.category_id),
|
||||||
|
name: Set(self.name),
|
||||||
|
position: Set(self.position.unwrap_or(0)),
|
||||||
|
channel_type: Set(self.channel_type),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_to_active_model(self, mut active_model: channel::ActiveModel) -> channel::ActiveModel {
|
||||||
|
active_model.server_id = Set(self.server_id);
|
||||||
|
active_model.category_id = Set(self.category_id);
|
||||||
|
active_model.name = Set(self.name);
|
||||||
|
active_model.position = Set(self.position.unwrap_or(0));
|
||||||
|
active_model.channel_type = Set(self.channel_type);
|
||||||
|
active_model
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/serializers/message.rs
Normal file
49
src/serializers/message.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::models::message;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct MessageSerializer {
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
pub id: Option<Uuid>,
|
||||||
|
pub channel_id: Option<Uuid>,
|
||||||
|
pub author_id: Option<Uuid>,
|
||||||
|
pub content: String,
|
||||||
|
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<message::Model> for MessageSerializer {
|
||||||
|
fn from(model: message::Model) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Some(model.id),
|
||||||
|
channel_id: Some(model.channel_id),
|
||||||
|
author_id: Some(model.user_id),
|
||||||
|
content: model.content,
|
||||||
|
created_at: Some(model.created_at.to_string()),
|
||||||
|
updated_at: Some(model.edited_at.unwrap_or(model.created_at).to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageSerializer {
|
||||||
|
pub fn into_active_model(self) -> message::ActiveModel {
|
||||||
|
message::ActiveModel {
|
||||||
|
channel_id: Set(self.channel_id.unwrap()),
|
||||||
|
user_id: Set(self.author_id.unwrap()),
|
||||||
|
content: Set(self.content),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_to_active_model(self, mut active_model: message::ActiveModel) -> message::ActiveModel {
|
||||||
|
active_model.channel_id = Set(self.channel_id.unwrap());
|
||||||
|
active_model.user_id = Set(self.author_id.unwrap());
|
||||||
|
active_model.content = Set(self.content);
|
||||||
|
active_model
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/serializers/mod.rs
Normal file
9
src/serializers/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
mod server;
|
||||||
|
mod category;
|
||||||
|
mod channel;
|
||||||
|
mod message;
|
||||||
|
|
||||||
|
pub use server::*;
|
||||||
|
pub use category::*;
|
||||||
|
pub use channel::*;
|
||||||
|
pub use message::*;
|
||||||
54
src/serializers/server.rs
Normal file
54
src/serializers/server.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::models::server;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct ServerSerializer {
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
pub id: Option<Uuid>,
|
||||||
|
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
pub password: Option<String>,
|
||||||
|
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// On part du Model (données « propres » venant de la BDD),
|
||||||
|
// pas de l'ActiveModel (qui contient des ActiveValue<T>).
|
||||||
|
impl From<server::Model> for ServerSerializer {
|
||||||
|
fn from(model: server::Model) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Some(model.id),
|
||||||
|
name: model.name,
|
||||||
|
password: model.password,
|
||||||
|
created_at: Some(model.created_at.to_string()),
|
||||||
|
updated_at: Some(model.updated_at.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerSerializer {
|
||||||
|
/// équivalent de `create()` d’un serializer DRF
|
||||||
|
pub fn into_active_model(self) -> server::ActiveModel {
|
||||||
|
server::ActiveModel {
|
||||||
|
name: Set(self.name),
|
||||||
|
// champ Option<String> -> tu peux le passer directement à Set(...)
|
||||||
|
password: Set(self.password),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// équivalent de `update(instance, validated_data)` en DRF
|
||||||
|
pub fn apply_to_active_model(self, mut active_model: server::ActiveModel) -> server::ActiveModel {
|
||||||
|
active_model.name = Set(self.name);
|
||||||
|
active_model.password = Set(self.password);
|
||||||
|
active_model
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/utils/mod.rs
Normal file
1
src/utils/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod toolbox;
|
||||||
9
src/utils/toolbox.rs
Normal file
9
src/utils/toolbox.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
pub fn number_of_cpus() -> usize {
|
||||||
|
match std::thread::available_parallelism() {
|
||||||
|
Ok(n) => n.get(),
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("Warning: Could not determine number of CPUs, defaulting to 1");
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user