jax_daemon/http_server/api/v0/bucket/
export.rs1use axum::extract::{Json, State};
2use axum::response::{IntoResponse, Response};
3use serde::{Deserialize, Serialize};
4use std::path::{Path, PathBuf};
5use uuid::Uuid;
6
7use common::bucket_log::BucketLogProvider;
8use common::mount::{MountError, NodeLink};
9use common::prelude::Mount;
10
11use crate::clone_state::PathHashMap;
12use crate::ServiceState;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ExportRequest {
16 pub bucket_id: Uuid,
17 pub target_dir: PathBuf,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ExportResponse {
22 pub bucket_name: String,
23 pub link: common::linked_data::Link,
24 pub height: u64,
25 pub files_exported: usize,
26 pub hash_map: PathHashMap,
27}
28
29pub async fn handler(
30 State(state): State<ServiceState>,
31 Json(req): Json<ExportRequest>,
32) -> Result<impl IntoResponse, ExportError> {
33 tracing::info!(
34 "EXPORT: Exporting bucket {} to {}",
35 req.bucket_id,
36 req.target_dir.display()
37 );
38
39 let logs = state.peer().logs();
41
42 let exists = logs
44 .exists(req.bucket_id)
45 .await
46 .map_err(|e| ExportError::BucketLog(e.to_string()))?;
47
48 if !exists {
49 return Err(ExportError::BucketNotFound(req.bucket_id));
50 }
51
52 let (head_link, height) = logs
53 .head(req.bucket_id, None)
54 .await
55 .map_err(|e| ExportError::BucketLog(e.to_string()))?;
56
57 let blobs = state.node().blobs();
58 let secret_key = state.node().secret();
59
60 let mount = Mount::load(&head_link, secret_key, blobs)
62 .await
63 .map_err(ExportError::Mount)?;
64
65 let mount_inner = mount.inner().await;
67 let bucket_name = mount_inner.manifest().name().to_string();
68
69 std::fs::create_dir_all(&req.target_dir)?;
71
72 let mut hash_map = PathHashMap::new();
74 let files_exported =
75 export_mount_to_filesystem(&mount, &req.target_dir, blobs, &mut hash_map).await?;
76
77 tracing::info!(
78 "EXPORT: Successfully exported {} files from bucket {}",
79 files_exported,
80 req.bucket_id
81 );
82
83 Ok((
84 http::StatusCode::OK,
85 Json(ExportResponse {
86 bucket_name,
87 link: head_link,
88 height,
89 files_exported,
90 hash_map,
91 }),
92 )
93 .into_response())
94}
95
96async fn export_mount_to_filesystem(
98 mount: &Mount,
99 target_dir: &Path,
100 blobs: &common::peer::BlobsStore,
101 hash_map: &mut PathHashMap,
102) -> Result<usize, ExportError> {
103 let mut files_exported = 0;
104
105 let items = mount
107 .ls_deep(&PathBuf::from("/"))
108 .await
109 .map_err(ExportError::Mount)?;
110
111 for (path, node_link) in items {
112 match node_link {
113 NodeLink::Data(link, secret, _) => {
114 let target_path = target_dir.join(&path);
116
117 if let Some(parent) = target_path.parent() {
119 std::fs::create_dir_all(parent)?;
120 }
121
122 let encrypted_data = blobs
124 .get(&link.hash())
125 .await
126 .map_err(|e| ExportError::BlobStore(e.to_string()))?;
127
128 let plaintext_hash = secret
130 .extract_plaintext_hash(&encrypted_data)
131 .map_err(|e| ExportError::Decryption(e.to_string()))?;
132
133 let decrypted_data = secret
135 .decrypt(&encrypted_data)
136 .map_err(|e| ExportError::Decryption(e.to_string()))?;
137
138 std::fs::write(&target_path, decrypted_data)?;
139
140 hash_map.insert(path.clone(), link.hash(), plaintext_hash);
142
143 files_exported += 1;
144
145 tracing::debug!("EXPORT: Exported file {}", path.display());
146 }
147 NodeLink::Dir(_, _) => {
148 let target_path = target_dir.join(&path);
150 std::fs::create_dir_all(&target_path)?;
151
152 tracing::debug!("EXPORT: Created directory {}", path.display());
153 }
154 }
155 }
156
157 Ok(files_exported)
158}
159
160#[derive(Debug, thiserror::Error)]
161pub enum ExportError {
162 #[error("Bucket not found: {0}")]
163 BucketNotFound(Uuid),
164 #[error("Bucket log error: {0}")]
165 BucketLog(String),
166 #[error("Mount error: {0}")]
167 Mount(#[from] MountError),
168 #[error("I/O error: {0}")]
169 Io(#[from] std::io::Error),
170 #[error("Blob store error: {0}")]
171 BlobStore(String),
172 #[error("Decryption error: {0}")]
173 Decryption(String),
174}
175
176impl IntoResponse for ExportError {
177 fn into_response(self) -> Response {
178 tracing::error!("EXPORT ERROR: {:?}", self);
179 match self {
180 ExportError::BucketNotFound(id) => (
181 http::StatusCode::NOT_FOUND,
182 format!("Bucket not found: {}", id),
183 )
184 .into_response(),
185 ExportError::BucketLog(msg) => (
186 http::StatusCode::INTERNAL_SERVER_ERROR,
187 format!("Bucket log error: {}", msg),
188 )
189 .into_response(),
190 ExportError::Mount(e) => (
191 http::StatusCode::INTERNAL_SERVER_ERROR,
192 format!("Mount error: {}", e),
193 )
194 .into_response(),
195 ExportError::Io(e) => (
196 http::StatusCode::INTERNAL_SERVER_ERROR,
197 format!("I/O error: {}", e),
198 )
199 .into_response(),
200 ExportError::BlobStore(msg) => (
201 http::StatusCode::INTERNAL_SERVER_ERROR,
202 format!("Blob store error: {}", msg),
203 )
204 .into_response(),
205 ExportError::Decryption(msg) => (
206 http::StatusCode::INTERNAL_SERVER_ERROR,
207 format!("Decryption error: {}", msg),
208 )
209 .into_response(),
210 }
211 }
212}