shardline_server/local_backend/
mod.rs1use std::{num::NonZeroUsize, path::PathBuf};
2
3mod files;
4mod objects;
5pub(crate) mod records;
6mod xorbs;
7
8use shardline_index::{
9 FileChunkRecord, LocalIndexStore, LocalRecordStore, ReconstructionStore, RecordTraversal,
10};
11use shardline_storage::{ObjectPrefix, ObjectStore};
12
13use crate::{
14 ServerError, ServerFrontend,
15 config::default_upload_max_in_flight_chunks,
16 local_path::ensure_directory_path_components_are_not_symlinked,
17 model::ServerStatsResponse,
18 object_store::ServerObjectStore,
19 overflow::{checked_add, checked_increment},
20 validation::ensure_directory,
21};
22use records::read_record;
23
24#[derive(Debug, Clone)]
26pub struct LocalBackend {
27 pub(super) public_base_url: String,
28 pub(super) chunk_size: NonZeroUsize,
29 pub(super) upload_max_in_flight_chunks: NonZeroUsize,
30 pub(super) server_frontends: Vec<ServerFrontend>,
31 pub(super) index_store: LocalIndexStore,
32 pub(super) record_store: LocalRecordStore,
33 pub(super) object_store: ServerObjectStore,
34}
35
36impl LocalBackend {
37 pub async fn new(
43 root: PathBuf,
44 public_base_url: String,
45 chunk_size: NonZeroUsize,
46 ) -> Result<Self, ServerError> {
47 let object_store = ServerObjectStore::local(root.join("chunks"))?;
48 Self::new_with_object_store(root, public_base_url, chunk_size, object_store).await
49 }
50
51 pub async fn new_with_upload_parallelism(
57 root: PathBuf,
58 public_base_url: String,
59 chunk_size: NonZeroUsize,
60 upload_max_in_flight_chunks: NonZeroUsize,
61 ) -> Result<Self, ServerError> {
62 let object_store = ServerObjectStore::local(root.join("chunks"))?;
63 Self::new_with_object_store_and_upload_parallelism(
64 root,
65 public_base_url,
66 chunk_size,
67 upload_max_in_flight_chunks,
68 object_store,
69 )
70 .await
71 }
72
73 pub(crate) async fn new_with_object_store(
74 root: PathBuf,
75 public_base_url: String,
76 chunk_size: NonZeroUsize,
77 object_store: ServerObjectStore,
78 ) -> Result<Self, ServerError> {
79 Self::new_with_object_store_and_upload_parallelism_with_frontends(
80 root,
81 public_base_url,
82 chunk_size,
83 default_upload_max_in_flight_chunks(),
84 object_store,
85 &[ServerFrontend::Xet],
86 )
87 .await
88 }
89
90 pub(crate) async fn new_with_object_store_and_upload_parallelism(
91 root: PathBuf,
92 public_base_url: String,
93 chunk_size: NonZeroUsize,
94 upload_max_in_flight_chunks: NonZeroUsize,
95 object_store: ServerObjectStore,
96 ) -> Result<Self, ServerError> {
97 Self::new_with_object_store_and_upload_parallelism_with_frontends(
98 root,
99 public_base_url,
100 chunk_size,
101 upload_max_in_flight_chunks,
102 object_store,
103 &[ServerFrontend::Xet],
104 )
105 .await
106 }
107
108 pub(crate) async fn new_with_object_store_and_upload_parallelism_with_frontends(
109 root: PathBuf,
110 public_base_url: String,
111 chunk_size: NonZeroUsize,
112 upload_max_in_flight_chunks: NonZeroUsize,
113 object_store: ServerObjectStore,
114 server_frontends: &[ServerFrontend],
115 ) -> Result<Self, ServerError> {
116 ensure_directory_path_components_are_not_symlinked(&root)?;
117 let backend = Self {
118 index_store: LocalIndexStore::open(root.clone()),
119 record_store: LocalRecordStore::open(root),
120 public_base_url,
121 chunk_size,
122 upload_max_in_flight_chunks,
123 server_frontends: server_frontends.to_vec(),
124 object_store,
125 };
126 Ok(backend)
127 }
128
129 #[must_use]
131 pub fn public_base_url(&self) -> &str {
132 &self.public_base_url
133 }
134
135 pub(crate) const fn object_backend_name(&self) -> &'static str {
136 self.object_store.backend_name()
137 }
138
139 pub async fn ready(&self) -> Result<(), ServerError> {
146 let object_store = self.object_store();
147 if let Some(local_root) = object_store.local_root() {
148 ensure_directory(local_root).await?;
149 } else {
150 let probe_key = shardline_storage::ObjectKey::parse("health/probe")
151 .map_err(|_error| ServerError::InvalidContentHash)?;
152 let _object_store_reachable = object_store.metadata(&probe_key)?;
153 }
154 let _latest = RecordTraversal::list_latest_record_locators(&self.record_store).await?;
155 let _reconstructions =
156 ReconstructionStore::list_reconstruction_file_ids(&self.index_store)?;
157 Ok(())
158 }
159
160 pub async fn stats(&self) -> Result<ServerStatsResponse, ServerError> {
166 let object_store = self.object_store();
167 let prefix = ObjectPrefix::parse("").map_err(|_error| ServerError::InvalidContentHash)?;
168 let mut chunks = 0_u64;
169 let mut chunk_bytes = 0_u64;
170 crate::object_store::visit_object_prefix(&object_store, &prefix, |metadata| {
171 let is_chunk =
172 crate::chunk_store::chunk_hash_from_chunk_object_key_if_present(metadata.key())?
173 .is_some();
174 if is_chunk {
175 chunks = checked_increment(chunks)?;
176 chunk_bytes = checked_add(chunk_bytes, metadata.length())?;
177 }
178
179 Ok(())
180 })?;
181 let files = u64::try_from(
182 RecordTraversal::list_latest_record_locators(&self.record_store)
183 .await?
184 .len(),
185 )?;
186
187 Ok(ServerStatsResponse {
188 chunks,
189 chunk_bytes,
190 files,
191 })
192 }
193
194 pub(crate) fn object_store(&self) -> ServerObjectStore {
195 self.object_store.clone()
196 }
197
198 pub(super) async fn read_record(
199 &self,
200 file_id: &str,
201 content_hash: Option<&str>,
202 repository_scope: Option<&shardline_protocol::RepositoryScope>,
203 ) -> Result<shardline_index::FileRecord, ServerError> {
204 read_record(&self.record_store, file_id, content_hash, repository_scope).await
205 }
206}
207
208#[must_use]
209pub fn chunk_hash(bytes: &[u8]) -> shardline_protocol::ShardlineHash {
210 let digest = blake3::hash(bytes);
211 shardline_protocol::ShardlineHash::from_bytes(*digest.as_bytes())
212}
213
214pub(crate) fn content_hash(
215 total_bytes: u64,
216 chunk_size: u64,
217 chunks: &[FileChunkRecord],
218) -> String {
219 let mut hasher = blake3::Hasher::new();
220 hasher.update(&total_bytes.to_le_bytes());
221 hasher.update(&chunk_size.to_le_bytes());
222 for chunk in chunks {
223 hasher.update(chunk.hash.as_bytes());
224 hasher.update(&chunk.offset.to_le_bytes());
225 hasher.update(&chunk.length.to_le_bytes());
226 }
227 hasher.finalize().to_hex().to_string()
228}
229
230#[cfg(test)]
231mod tests;