Skip to main content

shardline_server/local_backend/
files.rs

1use axum::body::Bytes;
2#[cfg(test)]
3use shardline_index::FileRecord;
4use shardline_protocol::{ByteRange, RepositoryScope};
5use shardline_storage::ObjectStore;
6use tokio::task;
7
8use super::LocalBackend;
9use crate::{
10    ServerError, ShardMetadataLimits,
11    model::UploadFileResponse,
12    object_store::{read_full_object, reconstruct_file_record_bytes},
13    upload_ingest::RequestBodyReader,
14    validation::validate_identifier,
15    xet_adapter::{FileReconstructionResponse, ShardUploadResponse, build_reconstruction_response},
16};
17
18impl LocalBackend {
19    /// Stores a file version as deduplicated content chunks.
20    ///
21    /// # Errors
22    ///
23    /// Returns [`ServerError`] when file identifier validation, chunk persistence, or
24    /// metadata persistence fails.
25    pub async fn upload_file(
26        &self,
27        file_id: &str,
28        body: Bytes,
29        repository_scope: Option<&RepositoryScope>,
30    ) -> Result<UploadFileResponse, ServerError> {
31        self.upload_file_stream(
32            file_id,
33            RequestBodyReader::from_bytes(body),
34            repository_scope,
35            None,
36        )
37        .await
38    }
39
40    /// Stores a streamed file version as deduplicated content chunks.
41    ///
42    /// # Errors
43    ///
44    /// Returns [`ServerError`] when request streaming, file identifier validation,
45    /// chunk persistence, source digest validation, or metadata persistence fails.
46    pub(crate) async fn upload_file_stream(
47        &self,
48        file_id: &str,
49        mut body: RequestBodyReader,
50        repository_scope: Option<&RepositoryScope>,
51        expected_sha256: Option<&str>,
52    ) -> Result<UploadFileResponse, ServerError> {
53        validate_identifier(file_id)?;
54
55        let object_store = self.object_store();
56        let mut ingestor = crate::upload_ingest::FileUploadIngestor::new_with_parallelism(
57            self.chunk_size,
58            expected_sha256.is_some(),
59            self.upload_max_in_flight_chunks,
60        );
61        while let Some(bytes) = body.next_bytes().await? {
62            ingestor.ingest_body_chunk(&object_store, &bytes).await?;
63        }
64
65        let (record, response) = ingestor
66            .finish(&object_store, file_id, repository_scope, expected_sha256)
67            .await?;
68        self.record_store
69            .commit_file_version_metadata(&record)
70            .await?;
71
72        Ok(response)
73    }
74
75    /// Stores a bounded native Xet shard and indexes the contained file reconstructions.
76    ///
77    /// # Errors
78    ///
79    /// Returns [`ServerError`] when request streaming, shard validation, referenced xorb
80    /// validation, or metadata persistence fails.
81    pub(crate) async fn upload_shard_stream(
82        &self,
83        mut body: RequestBodyReader,
84        repository_scope: Option<&RepositoryScope>,
85        shard_metadata_limits: ShardMetadataLimits,
86    ) -> Result<ShardUploadResponse, ServerError> {
87        let uploaded_body = crate::upload_ingest::read_body_to_bytes(&mut body).await?;
88        let record_store = self.record_store.clone();
89        let object_store = self.object_store();
90        crate::xet_adapter::register_uploaded_shard_bytes(
91            &object_store,
92            &uploaded_body,
93            repository_scope,
94            shard_metadata_limits,
95            move |records, mappings| async move {
96                record_store
97                    .commit_native_shard_metadata(&records, &mappings)
98                    .await?;
99                Ok(())
100            },
101        )
102        .await
103        .map_err(ServerError::from)
104    }
105
106    /// Loads reconstruction metadata for a file.
107    ///
108    /// # Errors
109    ///
110    /// Returns [`ServerError`] when the file identifier is invalid or the record is
111    /// missing or unreadable.
112    pub async fn reconstruction(
113        &self,
114        file_id: &str,
115        content_hash: Option<&str>,
116        requested_range: Option<ByteRange>,
117        repository_scope: Option<&RepositoryScope>,
118    ) -> Result<FileReconstructionResponse, ServerError> {
119        let record = self
120            .read_record(file_id, content_hash, repository_scope)
121            .await?;
122        Ok(build_reconstruction_response(
123            self.public_base_url(),
124            &record,
125            requested_range,
126        )?)
127    }
128
129    /// Loads the logical byte length for a file version.
130    ///
131    /// # Errors
132    ///
133    /// Returns [`ServerError`] when the file identifier is invalid or the record is
134    /// missing or unreadable.
135    pub async fn file_total_bytes(
136        &self,
137        file_id: &str,
138        content_hash: Option<&str>,
139        repository_scope: Option<&RepositoryScope>,
140    ) -> Result<u64, ServerError> {
141        let record = self
142            .read_record(file_id, content_hash, repository_scope)
143            .await?;
144        Ok(record.total_bytes)
145    }
146
147    /// Loads the file-version record used by streaming transfer paths.
148    ///
149    /// # Errors
150    ///
151    /// Returns [`ServerError`] when the file identifier is invalid or the record is
152    /// missing or unreadable.
153    #[cfg(test)]
154    pub(crate) async fn file_record(
155        &self,
156        file_id: &str,
157        content_hash: Option<&str>,
158        repository_scope: Option<&RepositoryScope>,
159    ) -> Result<FileRecord, ServerError> {
160        self.read_record(file_id, content_hash, repository_scope)
161            .await
162    }
163
164    /// Reconstructs a file into a contiguous byte vector.
165    ///
166    /// # Errors
167    ///
168    /// Returns [`ServerError`] when metadata or chunk bytes cannot be read.
169    pub async fn download_file(
170        &self,
171        file_id: &str,
172        content_hash: Option<&str>,
173        repository_scope: Option<&RepositoryScope>,
174    ) -> Result<Vec<u8>, ServerError> {
175        let record = self
176            .read_record(file_id, content_hash, repository_scope)
177            .await?;
178        let object_store = self.object_store();
179        let server_frontends = self.server_frontends.clone();
180        task::spawn_blocking(move || {
181            reconstruct_file_record_bytes(&object_store, &server_frontends, &record)
182        })
183        .await
184        .map_err(ServerError::BlockingTask)?
185    }
186
187    /// Reads a stored chunk by hash.
188    ///
189    /// # Errors
190    ///
191    /// Returns [`ServerError`] when the hash is invalid or the chunk is missing.
192    pub async fn read_chunk(&self, hash_hex: &str) -> Result<Vec<u8>, ServerError> {
193        let object_store = self.object_store();
194        let object_key = crate::chunk_store::chunk_object_key(hash_hex)?;
195        let metadata = object_store.metadata(&object_key)?;
196        let Some(metadata) = metadata else {
197            return Err(ServerError::NotFound);
198        };
199
200        task::spawn_blocking(move || {
201            read_full_object(&object_store, &object_key, metadata.length())
202        })
203        .await
204        .map_err(ServerError::BlockingTask)?
205    }
206
207    /// Reads a stored chunk only when it is reachable from a concrete file version.
208    ///
209    /// # Errors
210    ///
211    /// Returns [`ServerError`] when the hash, file identifier, or content hash are
212    /// invalid, when the file version is missing, or when the chunk is not referenced
213    /// by that version.
214    pub async fn read_chunk_for_file_version(
215        &self,
216        hash_hex: &str,
217        file_id: &str,
218        content_hash: &str,
219        repository_scope: Option<&RepositoryScope>,
220    ) -> Result<Vec<u8>, ServerError> {
221        let record = self
222            .read_record(file_id, Some(content_hash), repository_scope)
223            .await?;
224        if !record.chunks.iter().any(|chunk| chunk.hash == hash_hex) {
225            return Err(ServerError::NotFound);
226        }
227
228        self.read_chunk(hash_hex).await
229    }
230
231    /// Loads the stored byte length for a chunk object.
232    ///
233    /// # Errors
234    ///
235    /// Returns [`ServerError`] when the hash is invalid or the chunk is missing.
236    pub async fn chunk_length(&self, hash_hex: &str) -> Result<u64, ServerError> {
237        let object_store = self.object_store();
238        let object_key = crate::chunk_store::chunk_object_key(hash_hex)?;
239        let metadata = object_store.metadata(&object_key)?;
240        let Some(metadata) = metadata else {
241            return Err(ServerError::NotFound);
242        };
243
244        Ok(metadata.length())
245    }
246}