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",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
|
"doctate-common",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"rand 0.8.6",
|
"rand 0.8.6",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ edition.workspace = true
|
|||||||
default-run = "doctate-server"
|
default-run = "doctate-server"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
doctate-common = { path = "../doctate-common" }
|
||||||
axum = { version = "0.8", features = ["multipart"] }
|
axum = { version = "0.8", features = ["multipart"] }
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
|
|||||||
+2
-1
@@ -5,6 +5,7 @@ use std::time::Instant;
|
|||||||
use axum::extract::{FromRef, FromRequestParts};
|
use axum::extract::{FromRef, FromRequestParts};
|
||||||
use axum::http::request::Parts;
|
use axum::http::request::Parts;
|
||||||
use axum_extra::extract::cookie::CookieJar;
|
use axum_extra::extract::cookie::CookieJar;
|
||||||
|
use doctate_common::API_KEY_HEADER;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
@@ -43,7 +44,7 @@ where
|
|||||||
|
|
||||||
let api_key = parts
|
let api_key = parts
|
||||||
.headers
|
.headers
|
||||||
.get("X-API-Key")
|
.get(API_KEY_HEADER)
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
.ok_or(AppError::Unauthorized)?;
|
.ok_or(AppError::Unauthorized)?;
|
||||||
|
|
||||||
|
|||||||
+1
-15
@@ -1,15 +1 @@
|
|||||||
use serde::Serialize;
|
pub use doctate_common::ack::{AckResponse, AckStatus};
|
||||||
|
|
||||||
#[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,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ use time::OffsetDateTime;
|
|||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
use doctate_common::timestamp::{filename_stem_to_recorded_at, now_rfc3339};
|
||||||
|
|
||||||
use crate::analyze::{
|
use crate::analyze::{
|
||||||
AnalysisInput, AnalyzeJob, AnalyzeSender, RecordingInput, ANALYSIS_INPUT_FILE, DOCUMENT_FILE,
|
AnalysisInput, AnalyzeJob, AnalyzeSender, RecordingInput, ANALYSIS_INPUT_FILE, DOCUMENT_FILE,
|
||||||
};
|
};
|
||||||
@@ -229,16 +231,6 @@ pub(crate) async fn write_input_create_new(
|
|||||||
Ok(())
|
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.
|
/// Read `case_dir/document.md`. Returns `None` if no document exists.
|
||||||
pub(crate) async fn read_document(case_dir: &Path) -> Option<String> {
|
pub(crate) async fn read_document(case_dir: &Path) -> Option<String> {
|
||||||
tokio::fs::read_to_string(case_dir.join(DOCUMENT_FILE)).await.ok()
|
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 {
|
let marker = DeleteMarker {
|
||||||
batch: uuid::Uuid::new_v4(),
|
batch: uuid::Uuid::new_v4(),
|
||||||
deleted_at: now_rfc3339()?,
|
deleted_at: now_rfc3339(),
|
||||||
};
|
};
|
||||||
write_delete_marker(&case_dir, &marker).await?;
|
write_delete_marker(&case_dir, &marker).await?;
|
||||||
|
|
||||||
@@ -444,28 +436,3 @@ async fn restore_batch(user_root: &Path, batch: uuid::Uuid) -> usize {
|
|||||||
}
|
}
|
||||||
count
|
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::extract::{Multipart, State};
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
|
use doctate_common::timestamp::recorded_at_to_filename_stem;
|
||||||
use tokio::sync::mpsc::error::TrySendError;
|
use tokio::sync::mpsc::error::TrySendError;
|
||||||
use tracing::{info, warn};
|
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?;
|
let case_dir = resolve_case_dir(&user.data_dir, &case_id).await?;
|
||||||
|
|
||||||
// Build a filesystem-safe filename from the timestamp (colons → hyphens).
|
// 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"));
|
let file_path = case_dir.join(format!("{safe_timestamp}.m4a"));
|
||||||
|
|
||||||
tokio::fs::write(&file_path, &audio).await?;
|
tokio::fs::write(&file_path, &audio).await?;
|
||||||
|
|||||||
Reference in New Issue
Block a user