Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ Cargo.lock
# Per Editor Configuration
.idea/
.vscode/

# Uploads directory
uploads/
9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ path = "src/main.rs"
name = "coduck-backend"

[dependencies]
axum = "0.8.4"
anyhow = "1.0"
axum = { version = "0.8.4", features = ["json", "multipart"] }
chrono = { version = "0.4.38", features = ["serde"] }
reqwest = { version = "0.12.19", features = ["json", "rustls-tls"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.133"
tokio = { version = "1.45.1", features = ["full"] }
uuid = { version = "1.17.0", features = ["v4"] }

[dev-dependencies]
reqwest = { version = "0.12.19", features = ["json", "rustls-tls"] }
18 changes: 18 additions & 0 deletions src/errors/language.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use serde::{Deserialize, Serialize};

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub enum LanguageError {
UnsupportedExtension(String),
InvalidFilename,
}

impl std::fmt::Display for LanguageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LanguageError::UnsupportedExtension(extension) => {
write!(f, "Unsupported file extension: {extension}")
}
LanguageError::InvalidFilename => write!(f, "Invalid filename"),
}
}
}
3 changes: 3 additions & 0 deletions src/errors/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod language;

pub(crate) use language::*;
329 changes: 329 additions & 0 deletions src/file_manager/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
use crate::file_manager::{
FileMetadata, Language, UpdateFileContentRequest, UpdateFilenameRequest,
};

use anyhow::{anyhow, Result};
use axum::{
extract::{Multipart, Path},
http::StatusCode,
response::IntoResponse,
Json,
};
use chrono::Utc;
use std::path::PathBuf;
use tokio::fs;
use uuid::Uuid;

const UPLOAD_DIR: &str = "uploads";

async fn file_exists(file_path: &PathBuf) -> bool {
tokio::fs::metadata(file_path).await.is_ok()
}

pub async fn upload_file(
Path((problem_id, category)): Path<(u32, String)>,
multipart: Multipart,
) -> impl IntoResponse {
let (filename, data) = match extract_multipart_data(multipart).await {
Ok(data) => data,
Err(e) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": e.to_string()
})),
)
.into_response();
}
};

let file_id = Uuid::new_v4().to_string();
let upload_dir = PathBuf::from(UPLOAD_DIR)
.join(problem_id.to_string())
.join(&category);

let file_path = upload_dir.join(&filename);

if file_exists(&file_path).await {
return (
StatusCode::CONFLICT,
Json(serde_json::json!({
"error": format!("File '{filename}' already exists in this category")
})),
)
.into_response();
}

let now = Utc::now()
.timestamp_nanos_opt()
.expect("Failed to get timestamp");
let language = match Language::from_filename(&filename) {
Ok(lang) => lang,
Err(e) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": e.to_string()
})),
)
.into_response();
}
};

if let Err(e) = save_file(&upload_dir, &file_path, &data).await {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": e.to_string()
})),
)
.into_response();
}

let metadata = FileMetadata {
id: file_id,
filename: filename,
language: language,
category: category,
size: data.len() as u64,
created_at: now,
updated_at: now,
};
(StatusCode::CREATED, Json(metadata)).into_response()
}

async fn save_file(
upload_dir: &PathBuf,
file_path: &PathBuf,
data: &axum::body::Bytes,
) -> Result<()> {
fs::create_dir_all(upload_dir)
.await
.map_err(|_| anyhow!("Failed to create directory"))?;

fs::write(file_path, data)
.await
.map_err(|_| anyhow!("Failed to write file"))?;

Ok(())
}

async fn extract_multipart_data(mut multipart: Multipart) -> Result<(String, axum::body::Bytes)> {
let field = multipart
.next_field()
.await?
.ok_or_else(|| anyhow!("Missing multipart field"))?;

let filename = field
.file_name()
.ok_or_else(|| anyhow!("Missing filename in multipart field"))?
.to_string();

let data = field.bytes().await?;

Ok((filename, data))
}

pub async fn get_file(
Path((problem_id, category, filename)): Path<(u32, String, String)>,
) -> impl IntoResponse {
let file_path = PathBuf::from(UPLOAD_DIR)
.join(problem_id.to_string())
.join(&category)
.join(&filename);

if !file_exists(&file_path).await {
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": format!("File '{filename}' not found in category '{category}'")
})),
)
.into_response();
}

match fs::read(&file_path).await {
Ok(content) => {
let content_str = String::from_utf8_lossy(&content);

(
StatusCode::OK,
[("Content-Type", "text/html; charset=UTF-8")],
content_str.to_string(),
)
.into_response()
}
Err(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "Failed to read file content"
})),
)
.into_response(),
}
}

pub async fn get_files_by_category(
Path((problem_id, category)): Path<(u32, String)>,
) -> impl IntoResponse {
let category_dir = PathBuf::from(UPLOAD_DIR)
.join(problem_id.to_string())
.join(&category);

if !file_exists(&category_dir).await {
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": format!("Category '{category}' not found for problem {problem_id}")
})),
)
.into_response();
}

let mut files = Vec::new();

match fs::read_dir(&category_dir).await {
Ok(mut entries) => {
while let Ok(Some(entry)) = entries.next_entry().await {
let path = entry.path();
if path.is_file() {
if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
files.push(filename.to_string());
}
}
}
}
Err(_) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "Failed to read directory"
})),
)
.into_response();
}
}

(StatusCode::OK, Json(files)).into_response()
}

pub async fn delete_file(
Path((problem_id, category, filename)): Path<(u32, String, String)>,
) -> impl IntoResponse {
let file_path = PathBuf::from(UPLOAD_DIR)
.join(problem_id.to_string())
.join(&category)
.join(&filename);

if !file_exists(&file_path).await {
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": format!("File '{filename}' not found in category '{category}'")
})),
)
.into_response();
}

match fs::remove_file(&file_path).await {
Ok(_) => (
StatusCode::OK,
Json(serde_json::json!({
"message": format!("File '{filename}' deleted successfully")
})),
)
.into_response(),
Err(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "Failed to delete file"
})),
)
.into_response(),
}
}

pub async fn update_file_content(
Path((problem_id, category, filename)): Path<(u32, String, String)>,
Json(update_request): Json<UpdateFileContentRequest>,
) -> impl IntoResponse {
let file_path = PathBuf::from(UPLOAD_DIR)
.join(problem_id.to_string())
.join(&category)
.join(&filename);

if !file_exists(&file_path).await {
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": format!("File '{filename}' not found in category '{category}'")
})),
)
.into_response();
}

match fs::write(&file_path, &update_request.content).await {
Ok(_) => (
StatusCode::OK,
Json(serde_json::json!({
"message": format!("File '{filename}' updated successfully")
})),
)
.into_response(),
Err(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "Failed to update file"
})),
)
.into_response(),
}
}

pub async fn update_filename(
Path((problem_id, category)): Path<(u32, String)>,
Json(update_request): Json<UpdateFilenameRequest>,
) -> impl IntoResponse {
let file_path = PathBuf::from(UPLOAD_DIR)
.join(problem_id.to_string())
.join(&category)
.join(&update_request.old_filename);

if !file_exists(&file_path).await {
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": format!(
"File '{}' not found in category '{category}'",
update_request.old_filename
)
})),
)
.into_response();
}

match fs::rename(
&file_path,
&file_path.with_file_name(&update_request.new_filename),
)
.await
{
Ok(_) => (
StatusCode::OK,
Json(serde_json::json!({
"message": format!(
"File '{}' updated successfully to '{}'",
update_request.old_filename, update_request.new_filename
)
})),
)
.into_response(),
Err(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "Failed to update filename"
})),
)
.into_response(),
}
}
5 changes: 5 additions & 0 deletions src/file_manager/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod handlers;
mod models;

pub(crate) use handlers::*;
pub use models::*;
Loading
Loading