summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/files.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2025-12-23 02:14:58 +0000
committersoryu <soryu@soryu.co>2025-12-23 14:47:18 +0000
commita32dc56d2e5447ef8988cb98b8686476cc94e70c (patch)
tree61307503c4af82103cea2360fe95d3ea324968d6 /makima/src/server/handlers/files.rs
parent73649d135efccda7e446775db773e21b458de202 (diff)
downloadsoryu-a32dc56d2e5447ef8988cb98b8686476cc94e70c.tar.gz
soryu-a32dc56d2e5447ef8988cb98b8686476cc94e70c.zip
Add Postgres for persistence and File cabinet
Migrations are local only currently, and must be run manually by setting POSTGRES_CONNECTION_URI
Diffstat (limited to 'makima/src/server/handlers/files.rs')
-rw-r--r--makima/src/server/handlers/files.rs230
1 files changed, 230 insertions, 0 deletions
diff --git a/makima/src/server/handlers/files.rs b/makima/src/server/handlers/files.rs
new file mode 100644
index 0000000..746d66b
--- /dev/null
+++ b/makima/src/server/handlers/files.rs
@@ -0,0 +1,230 @@
+//! HTTP handlers for file CRUD operations.
+
+use axum::{
+ extract::{Path, State},
+ http::StatusCode,
+ response::IntoResponse,
+ Json,
+};
+use uuid::Uuid;
+
+use crate::db::models::{CreateFileRequest, FileListResponse, FileSummary, UpdateFileRequest};
+use crate::db::repository;
+use crate::server::messages::ApiError;
+use crate::server::state::SharedState;
+
+/// List all files for the current owner.
+#[utoipa::path(
+ get,
+ path = "/api/v1/files",
+ responses(
+ (status = 200, description = "List of files", body = FileListResponse),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ tag = "Files"
+)]
+pub async fn list_files(State(state): State<SharedState>) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::list_files(pool).await {
+ Ok(files) => {
+ let summaries: Vec<FileSummary> = files.into_iter().map(FileSummary::from).collect();
+ let total = summaries.len() as i64;
+ Json(FileListResponse {
+ files: summaries,
+ total,
+ })
+ .into_response()
+ }
+ Err(e) => {
+ tracing::error!("Failed to list files: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Get a single file by ID.
+#[utoipa::path(
+ get,
+ path = "/api/v1/files/{id}",
+ params(
+ ("id" = Uuid, Path, description = "File ID")
+ ),
+ responses(
+ (status = 200, description = "File details", body = crate::db::models::File),
+ (status = 404, description = "File not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ tag = "Files"
+)]
+pub async fn get_file(
+ State(state): State<SharedState>,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::get_file(pool, id).await {
+ Ok(Some(file)) => Json(file).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "File not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to get file {}: {}", id, e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Create a new file.
+#[utoipa::path(
+ post,
+ path = "/api/v1/files",
+ request_body = CreateFileRequest,
+ responses(
+ (status = 201, description = "File created", body = crate::db::models::File),
+ (status = 400, description = "Invalid request", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ tag = "Files"
+)]
+pub async fn create_file(
+ State(state): State<SharedState>,
+ Json(req): Json<CreateFileRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::create_file(pool, req).await {
+ Ok(file) => (StatusCode::CREATED, Json(file)).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to create file: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Update an existing file.
+#[utoipa::path(
+ put,
+ path = "/api/v1/files/{id}",
+ params(
+ ("id" = Uuid, Path, description = "File ID")
+ ),
+ request_body = UpdateFileRequest,
+ responses(
+ (status = 200, description = "File updated", body = crate::db::models::File),
+ (status = 404, description = "File not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ tag = "Files"
+)]
+pub async fn update_file(
+ State(state): State<SharedState>,
+ Path(id): Path<Uuid>,
+ Json(req): Json<UpdateFileRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::update_file(pool, id, req).await {
+ Ok(Some(file)) => Json(file).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "File not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to update file {}: {}", id, e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Delete a file.
+#[utoipa::path(
+ delete,
+ path = "/api/v1/files/{id}",
+ params(
+ ("id" = Uuid, Path, description = "File ID")
+ ),
+ responses(
+ (status = 204, description = "File deleted"),
+ (status = 404, description = "File not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ tag = "Files"
+)]
+pub async fn delete_file(
+ State(state): State<SharedState>,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::delete_file(pool, id).await {
+ Ok(true) => StatusCode::NO_CONTENT.into_response(),
+ Ok(false) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "File not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to delete file {}: {}", id, e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}