use axum::{
extract::{Path, Query},
http::StatusCode,
response::IntoResponse,
routing::{delete, get, patch, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use crate::{config, core, shared};
#[derive(Serialize)]
struct ApiResponse<T: Serialize> {
ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
}
impl<T: Serialize> ApiResponse<T> {
fn success(data: T) -> Json<Self> {
Json(Self {
ok: true,
data: Some(data),
error: None,
})
}
}
fn api_error(status: StatusCode, msg: impl ToString) -> impl IntoResponse {
(
status,
Json(ApiResponse::<()> {
ok: false,
data: None,
error: Some(msg.to_string()),
}),
)
}
pub fn router() -> Router {
Router::new()
.route("/api/v1/sessions", get(list_sessions))
.route("/api/v1/sessions/{provider}/{session_id}", get(get_session))
.route(
"/api/v1/sessions/{provider}/{session_id}",
delete(delete_session),
)
.route(
"/api/v1/sessions/{provider}/{session_id}",
patch(rename_session),
)
.route("/api/v1/export", post(export_session))
.route("/api/v1/import", post(import_session))
.route("/api/v1/switch", post(switch_session))
.route("/api/v1/find", get(find_sessions))
.route("/api/v1/workspaces", get(list_workspaces))
.route("/api/v1/share", get(list_shared_sessions))
.route("/api/v1/share", post(create_shared_session))
.route("/api/v1/share/status", get(shared_status))
.route("/api/v1/share/sync", post(sync_shared_sessions))
.route("/api/v1/share/bind", post(bind_shared_session))
.route(
"/api/v1/share/holdings/{group_id}/{holding_id}",
delete(unbind_shared_session),
)
.route("/api/v1/share/{group_id}", delete(remove_shared_session))
.route("/api/v1/share/{group_id}", patch(rename_shared_session))
}
#[derive(Deserialize)]
struct ListQuery {
all: Option<bool>,
provider: Option<String>,
dir: Option<String>,
workspace: Option<String>,
}
async fn list_sessions(Query(q): Query<ListQuery>) -> impl IntoResponse {
let providers = q
.provider
.map(|p| p.split(',').map(|s| s.trim().to_string()).collect())
.unwrap_or_default();
let cwd = q.workspace.or(q.dir).or_else(|| {
std::env::current_dir()
.ok()
.map(|p| p.to_string_lossy().to_string())
});
let params = core::SessionListParams {
all: q.all.unwrap_or(false),
providers,
cwd,
};
match core::list_sessions(¶ms) {
Ok(groups) => ApiResponse::success(groups).into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
async fn list_workspaces() -> impl IntoResponse {
match config::known_workspaces() {
Ok(workspaces) => ApiResponse::success(workspaces).into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
async fn get_session(Path((provider, session_id)): Path<(String, String)>) -> impl IntoResponse {
match core::get_session(&provider, &session_id) {
Ok(session) => ApiResponse::success(session).into_response(),
Err(e) => api_error(StatusCode::NOT_FOUND, e).into_response(),
}
}
async fn delete_session(Path((provider, session_id)): Path<(String, String)>) -> impl IntoResponse {
match core::delete_session(&provider, &session_id) {
Ok(()) => ApiResponse::success("deleted").into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
#[derive(Deserialize)]
struct RenameBody {
title: String,
}
async fn rename_session(
Path((provider, session_id)): Path<(String, String)>,
Json(body): Json<RenameBody>,
) -> impl IntoResponse {
match core::rename_session(&provider, &session_id, &body.title) {
Ok(()) => ApiResponse::success("renamed").into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
#[derive(Deserialize)]
struct ExportBody {
provider: String,
session_id: String,
output_prefix: Option<String>,
#[serde(default = "default_format")]
format: String,
}
fn default_format() -> String {
"both".to_string()
}
async fn export_session(Json(body): Json<ExportBody>) -> impl IntoResponse {
let params = core::ExportParams {
provider: body.provider,
session_id: body.session_id,
output_prefix: body.output_prefix,
format: body.format,
};
match core::export_session(¶ms) {
Ok(result) => ApiResponse::success(result).into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
#[derive(Deserialize)]
struct ImportBody {
provider: String,
file_or_id: String,
to_dir: Option<String>,
}
async fn import_session(Json(body): Json<ImportBody>) -> impl IntoResponse {
let params = core::ImportParams {
provider: body.provider,
file_or_id: body.file_or_id,
to_dir: body.to_dir,
};
match core::import_session(¶ms) {
Ok(result) => ApiResponse::success(result).into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
#[derive(Deserialize)]
struct SwitchBody {
from: String,
to: String,
session_id: Option<String>,
to_dir: Option<String>,
}
async fn switch_session(Json(body): Json<SwitchBody>) -> impl IntoResponse {
let params = core::SwitchParams {
from: body.from,
to: body.to,
session_id: body.session_id,
to_dir: body.to_dir,
};
match core::switch_session(¶ms) {
Ok(result) => ApiResponse::success(result).into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
#[derive(Deserialize)]
struct FindQuery {
dir: Option<String>,
session: Option<String>,
provider: Option<String>,
}
async fn find_sessions(Query(q): Query<FindQuery>) -> impl IntoResponse {
if q.dir.is_none() && q.session.is_none() && q.provider.is_none() {
return api_error(
StatusCode::BAD_REQUEST,
"At least one filter required: dir, session, or provider",
)
.into_response();
}
let providers = q
.provider
.map(|p| p.split(',').map(|s| s.trim().to_string()).collect())
.unwrap_or_default();
let params = core::FindParams {
dir: q.dir,
session: q.session,
providers,
};
match core::find_sessions(¶ms) {
Ok(groups) => ApiResponse::success(groups).into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
async fn list_shared_sessions() -> impl IntoResponse {
match shared::list_groups() {
Ok(items) => ApiResponse::success(items).into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
#[derive(Deserialize)]
struct ShareCreateBody {
provider: String,
session_id: String,
#[serde(default)]
targets: Vec<String>,
to_dir: Option<String>,
title: Option<String>,
}
async fn create_shared_session(Json(body): Json<ShareCreateBody>) -> impl IntoResponse {
let params = shared::ShareCreateParams {
provider: body.provider,
session_id: body.session_id,
targets: body.targets,
to_dir: body.to_dir,
title: body.title,
};
match shared::create_group(¶ms) {
Ok(result) => ApiResponse::success(result).into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
#[derive(Deserialize)]
struct ShareBindBody {
group_id: String,
provider: String,
session_id: Option<String>,
to_dir: Option<String>,
}
async fn bind_shared_session(Json(body): Json<ShareBindBody>) -> impl IntoResponse {
let params = shared::AddHoldingParams {
group_id: body.group_id,
provider: body.provider,
session_id: body.session_id,
to_dir: body.to_dir,
};
match shared::add_holding(¶ms) {
Ok(result) => ApiResponse::success(result).into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
async fn unbind_shared_session(
Path((group_id, holding_id)): Path<(String, String)>,
) -> impl IntoResponse {
match shared::remove_holding(&group_id, &holding_id) {
Ok(()) => ApiResponse::success("unbound").into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
#[derive(Deserialize)]
struct ShareStatusQuery {
group_id: Option<String>,
}
async fn shared_status(Query(q): Query<ShareStatusQuery>) -> impl IntoResponse {
match q.group_id {
Some(id) => match shared::load_group(&id) {
Ok(mut group) => {
let _ = shared::refresh_active_times(&mut group);
ApiResponse::success(group).into_response()
}
Err(e) => api_error(StatusCode::NOT_FOUND, e).into_response(),
},
None => match shared::list_groups() {
Ok(mut groups) => {
for group in &mut groups {
let _ = shared::refresh_active_times(group);
}
ApiResponse::success(groups).into_response()
}
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
},
}
}
#[derive(Deserialize)]
struct ShareSyncBody {
group_id: String,
source_holding_id: Option<String>,
}
async fn sync_shared_sessions(Json(body): Json<ShareSyncBody>) -> impl IntoResponse {
let result = if let Some(source_id) = body.source_holding_id {
shared::push_sync(&body.group_id, &source_id)
} else {
shared::sync_to_latest(&body.group_id)
};
match result {
Ok(report) => ApiResponse::success(report).into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
#[derive(Deserialize)]
struct ShareRemoveQuery {
delete_provider_sessions: Option<bool>,
}
async fn remove_shared_session(
Path(group_id): Path<String>,
Query(q): Query<ShareRemoveQuery>,
) -> impl IntoResponse {
match shared::delete_group(&group_id, q.delete_provider_sessions.unwrap_or(false)) {
Ok(()) => ApiResponse::success("removed").into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
#[derive(Deserialize)]
struct ShareRenameBody {
title: String,
}
async fn rename_shared_session(
Path(group_id): Path<String>,
Json(body): Json<ShareRenameBody>,
) -> impl IntoResponse {
match shared::rename_group(&group_id, &body.title) {
Ok(()) => ApiResponse::success("renamed").into_response(),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}