Skip to main content

jax_daemon/http_server/api/v0/bucket/
ls.rs

1use axum::extract::{Json, State};
2use axum::response::{IntoResponse, Response};
3use reqwest::{Client, RequestBuilder, Url};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use common::prelude::{Link, MountError};
8
9use crate::http_server::api::client::ApiRequest;
10use crate::ServiceState;
11
12#[derive(Debug, Clone, Serialize, Deserialize, clap::Args)]
13pub struct LsRequest {
14    /// Bucket ID to list
15    #[arg(long)]
16    pub bucket_id: Uuid,
17
18    /// Path in bucket to list (defaults to root)
19    #[serde(skip_serializing_if = "Option::is_none")]
20    #[arg(long)]
21    pub path: Option<String>,
22
23    /// List recursively
24    #[serde(skip_serializing_if = "Option::is_none")]
25    #[arg(long)]
26    pub deep: Option<bool>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct LsResponse {
31    pub items: Vec<PathInfo>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct PathInfo {
36    pub path: String,
37    pub name: String,
38    pub link: Link,
39    pub is_dir: bool,
40    pub mime_type: String,
41}
42
43#[axum::debug_handler]
44pub async fn handler(
45    State(state): State<ServiceState>,
46    Json(req): Json<LsRequest>,
47) -> Result<impl IntoResponse, LsError> {
48    let deep = req.deep.unwrap_or(false);
49
50    // Load mount based on role (owners see HEAD, mirrors see latest_published)
51    let mount = state.peer().mount_for_read(req.bucket_id).await?;
52
53    let path_str = req.path.as_deref().unwrap_or("/");
54    let path_buf = std::path::PathBuf::from(path_str);
55
56    // List items
57    let items = if deep {
58        mount.ls_deep(&path_buf).await?
59    } else {
60        mount.ls(&path_buf).await?
61    };
62
63    // Convert to response format
64    let path_infos = items
65        .into_iter()
66        .map(|(path, node_link)| {
67            // Mount returns relative paths, make them absolute
68            let absolute_path = std::path::Path::new("/").join(&path);
69            let path_str = absolute_path.to_string_lossy().to_string();
70            let name = path
71                .file_name()
72                .map(|n| n.to_string_lossy().to_string())
73                .unwrap_or_else(|| path.to_string_lossy().to_string());
74
75            let mime_type = if node_link.is_dir() {
76                "inode/directory".to_string()
77            } else {
78                node_link
79                    .data()
80                    .and_then(|data| data.mime())
81                    .map(|mime| mime.to_string())
82                    .unwrap_or_else(|| "application/octet-stream".to_string())
83            };
84
85            PathInfo {
86                path: path_str,
87                name,
88                link: node_link.link().clone(),
89                is_dir: node_link.is_dir(),
90                mime_type,
91            }
92        })
93        .collect();
94
95    Ok((http::StatusCode::OK, Json(LsResponse { items: path_infos })).into_response())
96}
97
98#[derive(Debug, thiserror::Error)]
99pub enum LsError {
100    #[error("Mount error: {0}")]
101    Mount(#[from] MountError),
102}
103
104impl IntoResponse for LsError {
105    fn into_response(self) -> Response {
106        (
107            http::StatusCode::INTERNAL_SERVER_ERROR,
108            format!("Error: {}", self),
109        )
110            .into_response()
111    }
112}
113
114// Client implementation - builds request for this operation
115impl ApiRequest for LsRequest {
116    type Response = LsResponse;
117
118    fn build_request(self, base_url: &Url, client: &Client) -> RequestBuilder {
119        let full_url = base_url.join("/api/v0/bucket/ls").unwrap();
120        client.post(full_url).json(&self)
121    }
122}