feat: Implement course-level asset management and interactive media markers.

This commit is contained in:
2026-01-17 13:55:04 -03:00
parent 0772a88fbe
commit 02909ea85a
15 changed files with 1027 additions and 182 deletions
+50
View File
@@ -2,7 +2,10 @@ use chrono::{DateTime, Utc};
use common::models::{Course, Lesson, Module};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::io::Cursor;
use std::io::Write;
use uuid::Uuid;
use zip::write::FileOptions;
#[derive(Debug, Serialize, Deserialize)]
pub struct CourseExport {
@@ -63,3 +66,50 @@ pub async fn get_course_data(pool: &PgPool, course_id: Uuid) -> anyhow::Result<C
exported_at: Utc::now(),
})
}
pub async fn generate_course_zip(pool: &PgPool, course_id: Uuid) -> anyhow::Result<Vec<u8>> {
let course_data = get_course_data(pool, course_id).await?;
let assets =
sqlx::query_as::<_, common::models::Asset>("SELECT * FROM assets WHERE course_id = $1")
.bind(course_id)
.fetch_all(pool)
.await?;
let mut buf = Vec::new();
let mut zip = zip::ZipWriter::new(Cursor::new(&mut buf));
let options = FileOptions::default()
.compression_method(zip::CompressionMethod::Stored)
.unix_permissions(0o755);
// 1. Add course.json
zip.start_file("course.json", options)?;
let json_bytes = serde_json::to_vec_pretty(&course_data)?;
zip.write_all(&json_bytes)?;
// 2. Add Assets
for asset in assets {
// Use the internal storage filename (UUID.ext) to avoid collisions
let storage_filename = asset
.storage_path
.split('/')
.last()
.unwrap_or(&asset.filename);
let zip_path = format!("assets/{}", storage_filename);
if let Ok(file_content) = tokio::fs::read(&asset.storage_path).await {
zip.start_file(zip_path, options)?;
zip.write_all(&file_content)?;
} else {
tracing::warn!(
"Failed to read asset file for export: {}",
asset.storage_path
);
}
}
zip.finish()?;
drop(zip);
Ok(buf)
}