177 lines
5.8 KiB
Rust
177 lines
5.8 KiB
Rust
use actix_web::{HttpRequest, HttpResponse, get, post, web};
|
|
use base64::{Engine, engine::general_purpose};
|
|
|
|
use chrono::Utc;
|
|
use serde::Deserialize;
|
|
use serde_json::json;
|
|
|
|
use crate::{
|
|
LOG_FILE_NAME,
|
|
data_mgt::{AppState, Asset},
|
|
logs::{LogEvent, LogEventType, log_event},
|
|
};
|
|
use crate::{MAX_ASSET_DURATION, MIN_ASSET_DURATION};
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
pub struct UploadRequest {
|
|
duration: u32,
|
|
content_type: String,
|
|
content: String, // text or base64 string
|
|
}
|
|
|
|
#[post("/api/upload")]
|
|
async fn api_upload(
|
|
req: HttpRequest,
|
|
body: web::Json<UploadRequest>,
|
|
app_state: web::Data<AppState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
// Check for rate limiting
|
|
let now = Utc::now().timestamp_millis();
|
|
|
|
let connection_info = req.connection_info();
|
|
|
|
let uploader_ip = connection_info
|
|
.realip_remote_addr()
|
|
.map(|s| s.to_string())
|
|
.or_else(|| connection_info.peer_addr().map(|value| value.to_string()))
|
|
.ok_or_else(|| actix_web::error::ErrorBadRequest("Cannot determine client ip"))?;
|
|
|
|
// Convert to bytes
|
|
let content_bytes = if body.content_type == "text/plain" {
|
|
body.content.as_bytes().to_vec()
|
|
} else {
|
|
match general_purpose::STANDARD.decode(&body.content) {
|
|
Ok(bytes) => bytes,
|
|
Err(_) => return Ok(HttpResponse::BadRequest().json(json!({ "error": "Invalid base64 content" }))),
|
|
}
|
|
};
|
|
|
|
let clamped_duration = body.duration.clamp(MIN_ASSET_DURATION, MAX_ASSET_DURATION);
|
|
let asset_expiration_time = now + (clamped_duration as i64 * 60 * 1000);
|
|
let (allowed, retry_after_ms) = app_state
|
|
.connection_tracker
|
|
.check(&uploader_ip, asset_expiration_time)
|
|
.await;
|
|
if !allowed {
|
|
let retry_after_seconds = retry_after_ms.map(|ms| ((ms + 999) / 1000).max(1));
|
|
let response_body = match retry_after_seconds {
|
|
Some(seconds) => json!({ "error": "Upload limit exceeded", "retry_after_seconds": seconds }),
|
|
None => json!({ "error": "Upload limit exceeded" }),
|
|
};
|
|
// return Ok(HttpResponse::TooManyRequests().body("Upload limit exceeded"));
|
|
return Ok(HttpResponse::TooManyRequests().json(response_body));
|
|
}
|
|
|
|
let asset = crate::data_mgt::Asset::new(
|
|
clamped_duration,
|
|
body.content_type.clone(),
|
|
content_bytes,
|
|
Some(uploader_ip.clone()),
|
|
);
|
|
|
|
let id = asset.id();
|
|
log_event(LogEventType::AssetUploaded(asset.to_value()));
|
|
app_state.assets.add_asset(asset).await;
|
|
let response_body = json!({ "link": format!("/bhs/{}", id) });
|
|
Ok(HttpResponse::Ok().json(response_body))
|
|
}
|
|
|
|
#[get("/api/content/{id}")]
|
|
async fn api_get_asset(
|
|
req: HttpRequest,
|
|
path: web::Path<String>,
|
|
app_state: web::Data<AppState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
log_event(LogEventType::HttpRequest(req.into()));
|
|
|
|
match app_state.assets.get_asset(&path.into_inner()).await {
|
|
None => Ok(HttpResponse::NotFound().body("Asset not found")),
|
|
Some(asset) => Ok(HttpResponse::Ok()
|
|
.content_type(asset.mime())
|
|
.body(asset.content().clone())),
|
|
}
|
|
}
|
|
|
|
#[derive(serde::Serialize)]
|
|
struct StatsResponse {
|
|
active_assets: usize,
|
|
total_uploads: usize,
|
|
total_deleted: usize,
|
|
storage_bytes: u64,
|
|
image_count: usize,
|
|
text_count: usize,
|
|
total_requests: usize,
|
|
recent_activity: Vec<ActivityItem>,
|
|
}
|
|
|
|
#[derive(serde::Serialize)]
|
|
struct ActivityItem {
|
|
action: String,
|
|
mime: String,
|
|
share_duration: u32,
|
|
timestamp: String,
|
|
}
|
|
|
|
#[get("/api/stats")]
|
|
async fn api_stats(app_state: web::Data<AppState>) -> Result<HttpResponse, actix_web::Error> {
|
|
use crate::LOG_DIR;
|
|
use std::fs;
|
|
|
|
let (active_assets, storage_bytes, image_count, text_count) = app_state.assets.stats_summary().await;
|
|
|
|
let mut total_uploads = 0;
|
|
let mut total_deleted = 0;
|
|
let mut recent_activity: Vec<ActivityItem> = Vec::new();
|
|
let mut request_count: usize = 0;
|
|
|
|
let log_path = format!("{}{}", LOG_DIR, LOG_FILE_NAME);
|
|
if let Ok(content) = fs::read_to_string(&log_path) {
|
|
for line in content.lines() {
|
|
if let Ok(entry) = serde_json::from_str::<LogEvent>(line) {
|
|
match entry.event {
|
|
LogEventType::HttpRequest(_req) => {
|
|
request_count += 1;
|
|
}
|
|
LogEventType::AssetUploaded(asset) => {
|
|
let asset = serde_json::from_value::<Asset>(asset).unwrap_or_default();
|
|
total_uploads += 1;
|
|
recent_activity.push(ActivityItem {
|
|
action: "upload".to_string(),
|
|
mime: asset.mime(),
|
|
share_duration: asset.share_duration(),
|
|
timestamp: entry.time,
|
|
});
|
|
}
|
|
LogEventType::AssetDeleted(asset) => {
|
|
let asset = serde_json::from_value::<Asset>(asset).unwrap_or_default();
|
|
total_deleted += 1;
|
|
recent_activity.push(ActivityItem {
|
|
action: "delete".to_string(),
|
|
mime: asset.mime(),
|
|
share_duration: asset.share_duration(),
|
|
timestamp: entry.time,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keep only last 20, most recent first
|
|
recent_activity.reverse();
|
|
recent_activity.truncate(20);
|
|
|
|
let response = StatsResponse {
|
|
active_assets,
|
|
total_uploads,
|
|
total_deleted,
|
|
storage_bytes,
|
|
image_count,
|
|
text_count,
|
|
total_requests: request_count,
|
|
recent_activity,
|
|
};
|
|
|
|
Ok(HttpResponse::Ok().json(response))
|
|
}
|