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:
2026-04-17 15:25:11 +02:00
parent ed5047a3ff
commit a2c1ff49ac
6 changed files with 10 additions and 53 deletions
Generated
+1
View File
@@ -348,6 +348,7 @@ dependencies = [
"axum",
"axum-extra",
"bcrypt",
"doctate-common",
"dotenvy",
"pulldown-cmark",
"rand 0.8.6",
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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};
+3 -36
View File
@@ -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 -1
View File
@@ -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?;