Refactor: Use doctate-common for shared logic
Moves shared types and utility functions to a new `doctate-common` crate. This includes: - API key header constant - AckResponse and AckStatus types - Timestamp formatting and parsing utilities This change centralizes common functionality, reducing duplication and improving maintainability. The `server` crate now depends on `doctate-common`.
This commit is contained in:
Generated
+1
@@ -348,6 +348,7 @@ dependencies = [
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"bcrypt",
|
||||
"doctate-common",
|
||||
"dotenvy",
|
||||
"pulldown-cmark",
|
||||
"rand 0.8.6",
|
||||
|
||||
@@ -5,6 +5,7 @@ edition.workspace = true
|
||||
default-run = "doctate-server"
|
||||
|
||||
[dependencies]
|
||||
doctate-common = { path = "../doctate-common" }
|
||||
axum = { version = "0.8", features = ["multipart"] }
|
||||
tokio.workspace = true
|
||||
reqwest.workspace = true
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ use std::time::Instant;
|
||||
use axum::extract::{FromRef, FromRequestParts};
|
||||
use axum::http::request::Parts;
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
use doctate_common::API_KEY_HEADER;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -43,7 +44,7 @@ where
|
||||
|
||||
let api_key = parts
|
||||
.headers
|
||||
.get("X-API-Key")
|
||||
.get(API_KEY_HEADER)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or(AppError::Unauthorized)?;
|
||||
|
||||
|
||||
+1
-15
@@ -1,15 +1 @@
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AckResponse {
|
||||
pub case_id: String,
|
||||
pub recorded_at: String,
|
||||
pub status: AckStatus,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AckStatus {
|
||||
Received,
|
||||
Gone,
|
||||
}
|
||||
pub use doctate_common::ack::{AckResponse, AckStatus};
|
||||
|
||||
@@ -10,6 +10,8 @@ use time::OffsetDateTime;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use doctate_common::timestamp::{filename_stem_to_recorded_at, now_rfc3339};
|
||||
|
||||
use crate::analyze::{
|
||||
AnalysisInput, AnalyzeJob, AnalyzeSender, RecordingInput, ANALYSIS_INPUT_FILE, DOCUMENT_FILE,
|
||||
};
|
||||
@@ -229,16 +231,6 @@ pub(crate) async fn write_input_create_new(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reverse the filesystem-safe timestamp mapping applied in `upload.rs`:
|
||||
/// `:` in the time portion was replaced with `-`. Split at `T`, undo hyphens
|
||||
/// only in the time part to avoid touching date hyphens.
|
||||
fn filename_stem_to_recorded_at(stem: &str) -> String {
|
||||
match stem.split_once('T') {
|
||||
Some((date, time)) => format!("{}T{}", date, time.replace('-', ":")),
|
||||
None => stem.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read `case_dir/document.md`. Returns `None` if no document exists.
|
||||
pub(crate) async fn read_document(case_dir: &Path) -> Option<String> {
|
||||
tokio::fs::read_to_string(case_dir.join(DOCUMENT_FILE)).await.ok()
|
||||
@@ -295,7 +287,7 @@ pub async fn handle_delete_case(
|
||||
|
||||
let marker = DeleteMarker {
|
||||
batch: uuid::Uuid::new_v4(),
|
||||
deleted_at: now_rfc3339()?,
|
||||
deleted_at: now_rfc3339(),
|
||||
};
|
||||
write_delete_marker(&case_dir, &marker).await?;
|
||||
|
||||
@@ -444,28 +436,3 @@ async fn restore_batch(user_root: &Path, batch: uuid::Uuid) -> usize {
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
fn now_rfc3339() -> Result<String, AppError> {
|
||||
OffsetDateTime::now_utc()
|
||||
.format(&Rfc3339)
|
||||
.map_err(|e| AppError::Internal(format!("format timestamp: {e}")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn filename_stem_roundtrip() {
|
||||
assert_eq!(
|
||||
filename_stem_to_recorded_at("2026-04-15T10-32-00Z"),
|
||||
"2026-04-15T10:32:00Z"
|
||||
);
|
||||
// Date-only hyphens are preserved.
|
||||
assert_eq!(
|
||||
filename_stem_to_recorded_at("2026-04-15T10-32-00.123Z"),
|
||||
"2026-04-15T10:32:00.123Z"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use axum::extract::{Multipart, State};
|
||||
use axum::Json;
|
||||
use doctate_common::timestamp::recorded_at_to_filename_stem;
|
||||
use tokio::sync::mpsc::error::TrySendError;
|
||||
use tracing::{info, warn};
|
||||
|
||||
@@ -72,7 +73,7 @@ pub async fn handle_upload(
|
||||
let case_dir = resolve_case_dir(&user.data_dir, &case_id).await?;
|
||||
|
||||
// Build a filesystem-safe filename from the timestamp (colons → hyphens).
|
||||
let safe_timestamp = recorded_at.replace(':', "-");
|
||||
let safe_timestamp = recorded_at_to_filename_stem(&recorded_at);
|
||||
let file_path = case_dir.join(format!("{safe_timestamp}.m4a"));
|
||||
|
||||
tokio::fs::write(&file_path, &audio).await?;
|
||||
|
||||
Reference in New Issue
Block a user