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    /// Optional: specific version hash to list from
29    #[arg(long)]
30    #[serde(default)]
31    pub at: Option<String>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct LsResponse {
36    pub items: Vec<PathInfo>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct PathInfo {
41    pub path: String,
42    pub name: String,
43    pub link: Link,
44    pub is_dir: bool,
45    pub mime_type: String,
46}
47
48#[axum::debug_handler]
49pub async fn handler(
50    State(state): State<ServiceState>,
51    Json(req): Json<LsRequest>,
52) -> Result<impl IntoResponse, LsError> {
53    let deep = req.deep.unwrap_or(false);
54
55    // Load mount - either from specific link or role-based
56    let mount = if let Some(hash_str) = &req.at {
57        let hash = hash_str
58            .parse::<common::linked_data::Hash>()
59            .map_err(|e| LsError::InvalidHash(format!("Invalid hash format: {}", e)))?;
60        let link = common::linked_data::Link::new(common::linked_data::LD_RAW_CODEC, hash);
61        common::mount::Mount::load(&link, state.peer().secret(), state.peer().blobs()).await?
62    } else {
63        // Load mount based on role (owners see HEAD, mirrors see latest_published)
64        state.peer().mount_for_read(req.bucket_id).await?
65    };
66
67    let path_str = req.path.as_deref().unwrap_or("/");
68    let path_buf = std::path::PathBuf::from(path_str);
69
70    // List items
71    let items = if deep {
72        mount.ls_deep(&path_buf).await?
73    } else {
74        mount.ls(&path_buf).await?
75    };
76
77    // Convert to response format
78    let path_infos = items
79        .into_iter()
80        .map(|(path, node_link)| {
81            // Mount returns relative paths, make them absolute
82            let absolute_path = std::path::Path::new("/").join(&path);
83            let path_str = absolute_path.to_string_lossy().to_string();
84            let name = path
85                .file_name()
86                .map(|n| n.to_string_lossy().to_string())
87                .unwrap_or_else(|| path.to_string_lossy().to_string());
88
89            let mime_type = if node_link.is_dir() {
90                "inode/directory".to_string()
91            } else {
92                node_link
93                    .data()
94                    .and_then(|data| data.mime())
95                    .map(|mime| mime.to_string())
96                    .unwrap_or_else(|| "application/octet-stream".to_string())
97            };
98
99            PathInfo {
100                path: path_str,
101                name,
102                link: node_link.link().clone(),
103                is_dir: node_link.is_dir(),
104                mime_type,
105            }
106        })
107        .collect();
108
109    Ok((http::StatusCode::OK, Json(LsResponse { items: path_infos })).into_response())
110}
111
112#[derive(Debug, thiserror::Error)]
113pub enum LsError {
114    #[error("Invalid hash: {0}")]
115    InvalidHash(String),
116    #[error("Mount error: {0}")]
117    Mount(#[from] MountError),
118}
119
120impl IntoResponse for LsError {
121    fn into_response(self) -> Response {
122        match self {
123            LsError::InvalidHash(msg) => (http::StatusCode::BAD_REQUEST, msg).into_response(),
124            LsError::Mount(_) => (
125                http::StatusCode::INTERNAL_SERVER_ERROR,
126                format!("Error: {}", self),
127            )
128                .into_response(),
129        }
130    }
131}
132
133// Client implementation - builds request for this operation
134impl ApiRequest for LsRequest {
135    type Response = LsResponse;
136
137    fn build_request(self, base_url: &Url, client: &Client) -> RequestBuilder {
138        let full_url = base_url.join("/api/v0/bucket/ls").unwrap();
139        client.post(full_url).json(&self)
140    }
141}