Skip to main content

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

1use 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    // Load the bucket from logs
40    let logs = state.peer().logs();
41
42    // Check if bucket exists first
43    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    // Load the mount
61    let mount = Mount::load(&head_link, secret_key, blobs)
62        .await
63        .map_err(ExportError::Mount)?;
64
65    // Get the bucket name from the mount's manifest
66    let mount_inner = mount.inner().await;
67    let bucket_name = mount_inner.manifest().name().to_string();
68
69    // Create target directory if it doesn't exist
70    std::fs::create_dir_all(&req.target_dir)?;
71
72    // Export all files from the mount to the filesystem
73    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
96/// Export the entire mount to a filesystem directory
97async 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    // Get all items recursively
106    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                // This is a file - export it
115                let target_path = target_dir.join(&path);
116
117                // Create parent directories if needed
118                if let Some(parent) = target_path.parent() {
119                    std::fs::create_dir_all(parent)?;
120                }
121
122                // Get encrypted blob
123                let encrypted_data = blobs
124                    .get(&link.hash())
125                    .await
126                    .map_err(|e| ExportError::BlobStore(e.to_string()))?;
127
128                // Extract plaintext hash without full decryption (for hash map)
129                let plaintext_hash = secret
130                    .extract_plaintext_hash(&encrypted_data)
131                    .map_err(|e| ExportError::Decryption(e.to_string()))?;
132
133                // Decrypt and write file
134                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                // Store mapping: path -> (blob_hash, plaintext_hash)
141                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                // This is a directory - create it
149                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}