Backend:
- POST /api/v1/upload: multipart file upload with caption + hashtags
- Validates file size against DB config limits (image/video separate)
- Checks user ban status and event upload lock
- Saves original to disk under {media_path}/originals/{slug}/
- Tracks user total_upload_bytes for quota enforcement
- Extracts hashtags from caption text and explicit CSV field
- Upserts hashtags and links them to uploads
- PATCH /api/v1/upload/{id}: edit caption and hashtags (owner only)
- DELETE /api/v1/upload/{id}: soft-delete (owner only)
- GET /api/v1/stream: SSE endpoint with 30s keepalive
- Broadcasts new-upload events to all connected clients
- Uses tokio broadcast channel for fan-out
Services:
- CompressionWorker: Tokio semaphore-bounded (concurrency=2) background processor
- Images: resize to 800px wide JPEG preview via image crate
- PNG originals: lossless compression via oxipng
- Videos: ffmpeg thumbnail extraction (1 frame at 1s, scaled to 800px)
- Updates upload record with preview_path/thumbnail_path on completion
Models:
- Upload with full CRUD (create, find, update caption, soft delete, set paths)
- Hashtag with upsert, link/unlink, extract_hashtags() text parser
- UploadDto for API serialization with like/comment counts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
118 lines
3.1 KiB
Rust
118 lines
3.1 KiB
Rust
use chrono::{DateTime, Utc};
|
|
use serde::Serialize;
|
|
use sqlx::PgPool;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, sqlx::FromRow)]
|
|
pub struct Upload {
|
|
pub id: Uuid,
|
|
pub event_id: Uuid,
|
|
pub user_id: Uuid,
|
|
pub original_path: String,
|
|
pub preview_path: Option<String>,
|
|
pub thumbnail_path: Option<String>,
|
|
pub mime_type: String,
|
|
pub original_size_bytes: i64,
|
|
pub caption: Option<String>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub deleted_at: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct UploadDto {
|
|
pub id: Uuid,
|
|
pub user_id: Uuid,
|
|
pub uploader_name: String,
|
|
pub preview_url: Option<String>,
|
|
pub thumbnail_url: Option<String>,
|
|
pub mime_type: String,
|
|
pub caption: Option<String>,
|
|
pub hashtags: Vec<String>,
|
|
pub like_count: i64,
|
|
pub comment_count: i64,
|
|
pub liked_by_me: bool,
|
|
pub created_at: DateTime<Utc>,
|
|
}
|
|
|
|
impl Upload {
|
|
pub async fn create(
|
|
pool: &PgPool,
|
|
event_id: Uuid,
|
|
user_id: Uuid,
|
|
original_path: &str,
|
|
mime_type: &str,
|
|
original_size_bytes: i64,
|
|
caption: Option<&str>,
|
|
) -> Result<Self, sqlx::Error> {
|
|
sqlx::query_as::<_, Self>(
|
|
"INSERT INTO upload (event_id, user_id, original_path, mime_type, original_size_bytes, caption)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
RETURNING *",
|
|
)
|
|
.bind(event_id)
|
|
.bind(user_id)
|
|
.bind(original_path)
|
|
.bind(mime_type)
|
|
.bind(original_size_bytes)
|
|
.bind(caption)
|
|
.fetch_one(pool)
|
|
.await
|
|
}
|
|
|
|
pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Self>, sqlx::Error> {
|
|
sqlx::query_as::<_, Self>(
|
|
"SELECT * FROM upload WHERE id = $1 AND deleted_at IS NULL",
|
|
)
|
|
.bind(id)
|
|
.fetch_optional(pool)
|
|
.await
|
|
}
|
|
|
|
pub async fn set_preview_path(
|
|
pool: &PgPool,
|
|
id: Uuid,
|
|
preview_path: &str,
|
|
) -> Result<(), sqlx::Error> {
|
|
sqlx::query("UPDATE upload SET preview_path = $2 WHERE id = $1")
|
|
.bind(id)
|
|
.bind(preview_path)
|
|
.execute(pool)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn set_thumbnail_path(
|
|
pool: &PgPool,
|
|
id: Uuid,
|
|
thumbnail_path: &str,
|
|
) -> Result<(), sqlx::Error> {
|
|
sqlx::query("UPDATE upload SET thumbnail_path = $2 WHERE id = $1")
|
|
.bind(id)
|
|
.bind(thumbnail_path)
|
|
.execute(pool)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn soft_delete(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
|
|
sqlx::query("UPDATE upload SET deleted_at = NOW() WHERE id = $1")
|
|
.bind(id)
|
|
.execute(pool)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn update_caption(
|
|
pool: &PgPool,
|
|
id: Uuid,
|
|
caption: Option<&str>,
|
|
) -> Result<(), sqlx::Error> {
|
|
sqlx::query("UPDATE upload SET caption = $2 WHERE id = $1")
|
|
.bind(id)
|
|
.bind(caption)
|
|
.execute(pool)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
}
|