codex-sync 0.2.3

Sync and merge Codex conversations across computers, LAN, SSH, and offline storage
use crate::{config::Config, discovery, sync};
use anyhow::Result;
use axum::{
    Router,
    body::Body,
    extract::{Path, State},
    http::{HeaderMap, StatusCode},
    response::{Html, IntoResponse},
    routing::{get, post},
};
use std::{net::SocketAddr, sync::Arc};

type AppState = Arc<Config>;

pub async fn serve(cfg: Config) -> Result<()> {
    let addr: SocketAddr = cfg.listen.parse()?;
    let discovery_cfg = cfg.clone();
    tokio::spawn(async move {
        if let Err(e) = discovery::responder(discovery_cfg).await {
            eprintln!("发现服务停止:{e}");
        }
    });
    let state = Arc::new(cfg);
    let app = Router::new()
        .route("/", get(index))
        .route("/api/status", get(status))
        .route("/api/peers", get(peers))
        .route("/api/manifest", get(manifest))
        .route("/api/file/{*path}", get(file))
        .route("/api/pull", post(pull))
        .with_state(state);
    println!("Codex Sync 已启动:http://{addr}");
    axum::serve(tokio::net::TcpListener::bind(addr).await?, app).await?;
    Ok(())
}

fn authorized(headers: &HeaderMap, cfg: &Config) -> bool {
    headers.get("authorization").and_then(|h| h.to_str().ok())
        == Some(&format!("Bearer {}", cfg.shared_key))
}

async fn index() -> Html<&'static str> {
    Html(include_str!("web.html"))
}

async fn status(State(cfg): State<AppState>) -> impl IntoResponse {
    match sync::manifest(&cfg) {
        Ok(m) => axum::Json(m).into_response(),
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

async fn peers(State(cfg): State<AppState>) -> impl IntoResponse {
    match discovery::discover(&cfg, 2).await {
        Ok(p) => axum::Json(p).into_response(),
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

async fn manifest(State(cfg): State<AppState>, headers: HeaderMap) -> impl IntoResponse {
    if !authorized(&headers, &cfg) {
        return StatusCode::UNAUTHORIZED.into_response();
    }
    status(State(cfg)).await.into_response()
}

async fn file(
    State(cfg): State<AppState>,
    headers: HeaderMap,
    Path(path): Path<String>,
) -> impl IntoResponse {
    if !authorized(&headers, &cfg) {
        return StatusCode::UNAUTHORIZED.into_response();
    }
    match sync::safe_path(&cfg.sync_dir, &path).and_then(|p| std::fs::read(p).map_err(Into::into)) {
        Ok(bytes) => Body::from(bytes).into_response(),
        Err(_) => StatusCode::NOT_FOUND.into_response(),
    }
}

#[derive(serde::Deserialize)]
struct PullRequest {
    peer: String,
    overwrite: Option<bool>,
}
async fn pull(
    State(cfg): State<AppState>,
    axum::Json(req): axum::Json<PullRequest>,
) -> impl IntoResponse {
    match sync::pull(&cfg, &req.peer, req.overwrite.unwrap_or(false)).await {
        Ok(r) => axum::Json(r).into_response(),
        Err(e) => (StatusCode::BAD_GATEWAY, e.to_string()).into_response(),
    }
}