rok-cli 0.3.6

Developer CLI for rok-based Axum applications
use super::{write_file, Scaffold, ScaffoldArgs, ScaffoldResult};
use anyhow::Result;

pub struct UploadScaffold;

impl Scaffold for UploadScaffold {
    fn name(&self) -> &'static str {
        "upload"
    }
    fn description(&self) -> &'static str {
        "File upload/download: multipart handling, rok-storage, type validation, thumbnails"
    }

    fn generate(&self, args: &ScaffoldArgs) -> Result<ScaffoldResult> {
        let mut r = ScaffoldResult::default();
        let d = args.dry_run;
        write_file(
            &mut r,
            "src/app/controllers/upload_controller.rs",
            UPLOAD_CONTROLLER,
            d,
        )?;
        write_file(&mut r, "src/app/config/upload.rs", UPLOAD_CONFIG, d)?;
        write_file(&mut r, "migrations/create_uploads_table.sql", MIGRATION, d)?;
        r.warnings
            .push("Add STORAGE_DRIVER to .env (local|s3|r2|gcs)".into());
        r.warnings
            .push("Register POST /uploads and GET /uploads/:id routes".into());
        Ok(r)
    }
}

const UPLOAD_CONTROLLER: &str = r#"use axum::{extract::{Multipart, Path, State}, response::IntoResponse};
use rok_auth::axum::{Ctx, Response};
use rok_storage::Storage;

pub async fn upload(ctx: Ctx, mut multipart: Multipart) -> impl IntoResponse {
    while let Ok(Some(field)) = multipart.next_field().await {
        let filename = field.file_name().unwrap_or("file").to_string();
        let data = field.bytes().await.unwrap_or_default();
        // TODO: validate file type + size, store via rok-storage, save record
        return Response::created(serde_json::json!({ "filename": filename, "size": data.len() }));
    }
    Response::bad_request("No file uploaded")
}

pub async fn download(Path(id): Path<i64>) -> impl IntoResponse {
    // TODO: fetch upload record, stream file from storage with Content-Disposition
    Response::not_found("Upload not found")
}

pub async fn delete(ctx: Ctx, Path(id): Path<i64>) -> impl IntoResponse {
    // TODO: delete from storage + DB
    Response::no_content()
}
"#;

const UPLOAD_CONFIG: &str = r#"pub struct UploadConfig {
    pub max_size_bytes: u64,
    pub allowed_types: Vec<&'static str>,
    pub generate_thumbnail: bool,
}

impl Default for UploadConfig {
    fn default() -> Self {
        Self {
            max_size_bytes: 10 * 1024 * 1024,  // 10 MB
            allowed_types: vec!["image/jpeg", "image/png", "image/webp", "application/pdf"],
            generate_thumbnail: true,
        }
    }
}
"#;

const MIGRATION: &str = r#"CREATE TABLE uploads (
    id           BIGSERIAL PRIMARY KEY,
    filename     TEXT NOT NULL,
    content_type TEXT NOT NULL,
    size_bytes   BIGINT NOT NULL,
    storage_key  TEXT NOT NULL,
    uploader_id  BIGINT,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);
"#;