Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ version = "0.1.0"
edition = "2024"

[dependencies]
axum = { version = "0.8", features = ["ws"] }
axum = { version = "0.8", features = ["ws","multipart"] }
tokio = { version = "1", features = ["full"] }
futures = "0.3"
tower-http = { version = "0.5", features = ["fs"] }
dotenv = "0.15.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = "0.4.42"
uuid = { version = "1", features = ["v4"] }
124 changes: 124 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,72 @@ A **minimal, lightweight WebSocket chat server** written in **Rust** using **Axu

---


## 📁 File Upload & Attachments

### 🚀 Upload Endpoint
Files can now be uploaded via HTTP:

POST /upload

✨ Details:
- Uses `multipart/form-data`
- Files are stored on disk (default: `static/uploads`)
- Returns file metadata as JSON

📦 Example response:
{
"url": "/uploads/uuid-filename.png",
"filename": "photo.png",
"mime": "image/png"
}

---

## 📎 Attachments in Chat Messages

Chat messages now support:
- 📝 Text only
- 📎 File only
- 📝 + 📎 Text and file together

⚠️ Empty messages are ignored **unless an attachment exists**.

🧪 Example WebSocket payload:
{
"text": "optional text",
"attachment": {
"url": "/uploads/file.png",
"filename": "file.png",
"mime": "image/png"
}
}

---


## 🧹 Automatic Cleanup of Uploaded Files

✨ Behavior:
- Uploaded files are linked to chat messages
- When old messages are removed (due to retention limits):
- 🗑️ Their files are automatically deleted from disk
- Prevents unused files from accumulating in `static/uploads`

---

## 🧠 Configurable Message Retention (Updated)

⚙️ Configuration:
MESSAGE_LIMIT=50

📌 Rules:
- MESSAGE_LIMIT > 0 → keep only the last **N** messages
- MESSAGE_LIMIT = 0 → keep **all** messages (no deletion)

📁 File cleanup follows the same rule.


## 📡 Architecture

```text
Expand Down Expand Up @@ -95,6 +161,18 @@ export TOKEN=super-secret-token
```


---


## 🔒 MIME Type Whitelisting

Only explicitly allowed MIME types can be uploaded.

✅ Example:
ALLOWED_MIMES=image/jpeg,image/png,image/webp,image/gif,application/pdf,application/zip

❌ Unsupported types are rejected automatically.

---

## How become an admin
Expand Down Expand Up @@ -200,3 +278,49 @@ In-memory History (Last 50 messages)
* History is sent to **new clients** on connect
* Backend handles `join`, `leave`, `message` events



## 🧾 MIME Types Reference

### 🖼 Images
image/jpeg
image/png
image/webp
image/gif
image/bmp
image/heic
image/heif
image/svg+xml ⚠️

### 📄 Documents
application/pdf
text/plain
text/csv
text/markdown

### 📦 Archives
application/zip
application/x-zip-compressed
application/x-7z-compressed
application/gzip
application/x-tar

### 🎵 Audio
audio/mpeg
audio/wav
audio/ogg
audio/flac
audio/aac

### 🎥 Video
video/mp4
video/webm
video/ogg

### ☠️ Not Recommended / Dangerous
application/x-msdownload
application/x-sh
application/x-bat
application/java-archive

---
57 changes: 57 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use std::{collections::HashSet, env};

#[derive(Clone)]
pub struct Config {
pub bind_addr: String,
pub token: String,

pub upload_dir: String,
pub max_upload_bytes: usize,
pub body_limit_bytes: usize,
pub allowed_mimes: HashSet<String>,

/// 0 => unlimited (do not delete)
pub message_limit: usize,
}

fn get_env(name: &str, default: &str) -> String {
env::var(name).unwrap_or_else(|_| default.to_string())
}

fn parse_usize_env(name: &str, default: usize) -> usize {
env::var(name)
.ok()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(default)
}

pub fn load_config() -> Config {
let bind_addr = get_env("BIND_ADDR", "127.0.0.1:3000");
let token = get_env("TOKEN", "token");

let upload_dir = get_env("UPLOAD_DIR", "static/uploads");
let max_upload_bytes = parse_usize_env("MAX_UPLOAD_BYTES", 10 * 1024 * 1024);
let body_limit_bytes = parse_usize_env("BODY_LIMIT_BYTES", 20 * 1024 * 1024);

let allowed_raw = get_env(
"ALLOWED_MIMES",
"image/jpeg,image/png,image/webp,image/gif,application/pdf,application/zip",
);
let allowed_mimes: HashSet<String> = allowed_raw
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();

let message_limit = parse_usize_env("MESSAGE_LIMIT", 50);

Config {
bind_addr,
token,
upload_dir,
max_upload_bytes,
body_limit_bytes,
allowed_mimes,
message_limit,
}
}
Loading