jax_daemon/http_server/api/v0/bucket/
ls.rs1use 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 #[arg(long)]
16 pub bucket_id: Uuid,
17
18 #[serde(skip_serializing_if = "Option::is_none")]
20 #[arg(long)]
21 pub path: Option<String>,
22
23 #[serde(skip_serializing_if = "Option::is_none")]
25 #[arg(long)]
26 pub deep: Option<bool>,
27
28 #[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 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 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 let items = if deep {
72 mount.ls_deep(&path_buf).await?
73 } else {
74 mount.ls(&path_buf).await?
75 };
76
77 let path_infos = items
79 .into_iter()
80 .map(|(path, node_link)| {
81 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
133impl 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}