mod.rs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2025 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. //! Chunk-based file storage implementation.
  19. //! This is a building block for a DHT or something similar.
  20. //!
  21. //! The API supports file insertion and retrieval. There is intentionally no
  22. //! `remove` support. File removal should be handled externally, and then it
  23. //! is only required to run `garbage_collect()` to clean things up.
  24. //!
  25. //! The filesystem hierarchy stores two directories: `files` and `chunks`.
  26. //! `chunks` store [`MAX_CHUNK_SIZE`] files, where the filename is a BLAKE3
  27. //! hash of the chunk's contents.
  28. //! `files` store metadata about a full file, which can be retrieved by
  29. //! concatenating the chunks in order. The filename of a file in `files`
  30. //! is the BLAKE3 hash of hashed chunks in the correct order.
  31. //!
  32. //! It might look like the following:
  33. //! ```
  34. //! /files/B9fFKaEYphw2oH5PDbeL1TTAcSzL6ax84p8SjBKzuYzX
  35. //! /files/...
  36. //! /chunks/2bQPxSR8Frz7S7JW3DRAzEtkrHfLXB1CN65V7az77pUp
  37. //! /chunks/CvjvN6MfWQYK54DgKNR7MPgFSZqsCgpWKF2p8ot66CCP
  38. //! /chunks/...
  39. //! ```
  40. //!
  41. //! In the above example, contents of `B9fFKaEYphw2oH5PDbeL1TTAcSzL6ax84p8SjBKzuYzX`
  42. //! may be:
  43. //! ```
  44. //! 2bQPxSR8Frz7S7JW3DRAzEtkrHfLXB1CN65V7az77pUp
  45. //! CvjvN6MfWQYK54DgKNR7MPgFSZqsCgpWKF2p8ot66CCP
  46. //! ```
  47. //!
  48. //! This means, in order to retrieve `B9fFKaEYphw2oH5PDbeL1TTAcSzL6ax84p8SjBKzuYzX`,
  49. //! we need to concatenate the files under `/chunks` whose filenames are the
  50. //! hashes found above. The contents of the files in `/chunks` are arbitrary
  51. //! data, and by concatenating them we can retrieve the original file.
  52. //!
  53. //! It is important to note that multiple files can use the same chunks.
  54. //! This is some kind of naive deduplication, so we actually don't consider
  55. //! chunks to be specific to a single file and therefore when we do garbage
  56. //! collection, we keep chunks and files independent of each other.
  57. use std::{collections::HashSet, path::PathBuf};
  58. use futures::AsyncRead;
  59. use log::{debug, info, warn};
  60. use smol::{
  61. fs::{self, File, OpenOptions},
  62. io::{
  63. self, AsyncBufReadExt, AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufReader, Cursor,
  64. SeekFrom,
  65. },
  66. stream::StreamExt,
  67. };
  68. use crate::{Error, Result};
  69. /// Defined maximum size of a stored chunk (256 KiB)
  70. pub const MAX_CHUNK_SIZE: usize = 262_144;
  71. /// Path prefix where file metadata is stored
  72. const FILES_PATH: &str = "files";
  73. /// Path prefix where file chunks are stored
  74. const CHUNKS_PATH: &str = "chunks";
  75. pub fn hash_to_string(hash: &blake3::Hash) -> String {
  76. bs58::encode(hash.as_bytes()).into_string()
  77. }
  78. /// `ChunkedFile` is a representation of a file we're trying to
  79. /// retrieve from `Geode`.
  80. ///
  81. /// The tuple contains `blake3::Hash` of
  82. /// the file's chunks and an optional `PathBuf` which points to
  83. /// the filesystem where the chunk can be found. If `None`, it
  84. /// is to be assumed that the chunk is not available locally.
  85. #[derive(Clone)]
  86. pub struct ChunkedFile(Vec<(blake3::Hash, Option<PathBuf>)>);
  87. impl ChunkedFile {
  88. fn new(hashes: &[blake3::Hash]) -> Self {
  89. Self(hashes.iter().map(|x| (*x, None)).collect())
  90. }
  91. /// Check whether we have all the chunks available locally.
  92. pub fn is_complete(&self) -> bool {
  93. !self.0.iter().any(|(_, p)| p.is_none())
  94. }
  95. /// Return an iterator over the chunks and their paths.
  96. pub fn iter(&self) -> core::slice::Iter<'_, (blake3::Hash, Option<PathBuf>)> {
  97. self.0.iter()
  98. }
  99. /// Return the number of chunks.
  100. pub fn len(&self) -> usize {
  101. self.0.len()
  102. }
  103. /// Return `true` if the chunked file contains no chunk.
  104. pub fn is_empty(&self) -> bool {
  105. self.0.is_empty()
  106. }
  107. /// Return the number of chunks available locally.
  108. pub fn local_chunks(&self) -> usize {
  109. self.0.iter().filter(|(_, p)| p.is_some()).count()
  110. }
  111. }
  112. /// Chunk-based file storage interface.
  113. pub struct Geode {
  114. /// Path to the filesystem directory where file metadata is stored
  115. files_path: PathBuf,
  116. /// Path to the filesystem directory where file chunks are stored
  117. chunks_path: PathBuf,
  118. }
  119. /// smol::fs::File::read does not guarantee that the buffer will be filled, even if the buffer is
  120. /// smaller than the file. This is a workaround.
  121. /// This reads the stream until the buffer is full or until we reached the end of the stream.
  122. pub async fn read_until_filled(
  123. mut stream: impl AsyncRead + Unpin,
  124. buffer: &mut [u8],
  125. ) -> io::Result<usize> {
  126. let mut total_bytes_read = 0;
  127. while total_bytes_read < buffer.len() {
  128. let bytes_read = stream.read(&mut buffer[total_bytes_read..]).await?;
  129. if bytes_read == 0 {
  130. break; // EOF reached
  131. }
  132. total_bytes_read += bytes_read;
  133. }
  134. Ok(total_bytes_read)
  135. }
  136. impl Geode {
  137. /// Instantiate a new [`Geode`] object.
  138. /// `base_path` defines the root directory where Geode will store its
  139. /// file metadata and chunks.
  140. pub async fn new(base_path: &PathBuf) -> Result<Self> {
  141. let mut files_path: PathBuf = base_path.into();
  142. let mut chunks_path: PathBuf = base_path.into();
  143. files_path.push(FILES_PATH);
  144. chunks_path.push(CHUNKS_PATH);
  145. // Create necessary directory structure if needed
  146. fs::create_dir_all(&files_path).await?;
  147. fs::create_dir_all(&chunks_path).await?;
  148. Ok(Self { files_path, chunks_path })
  149. }
  150. /// Attempt to read chunk hashes from a given file path and return
  151. /// a `Vec` containing the hashes in order.
  152. async fn read_metadata(path: &PathBuf) -> Result<Vec<blake3::Hash>> {
  153. debug!(target: "geode::read_metadata()", "Reading chunks from {:?}", path);
  154. let fd = File::open(path).await?;
  155. let mut read_chunks = vec![];
  156. let mut lines = BufReader::new(fd).lines();
  157. while let Some(line) = lines.next().await {
  158. let line = line?;
  159. let mut hash_buf = [0u8; 32];
  160. bs58::decode(line).onto(&mut hash_buf)?;
  161. let chunk_hash = blake3::Hash::from_bytes(hash_buf);
  162. read_chunks.push(chunk_hash);
  163. }
  164. Ok(read_chunks)
  165. }
  166. /// Perform garbage collection over the filesystem hierarchy.
  167. /// Returns sets representing deleted files and deleted chunks, respectively.
  168. pub async fn garbage_collect(&self) -> Result<(HashSet<blake3::Hash>, HashSet<blake3::Hash>)> {
  169. info!(target: "geode::garbage_collect()", "[Geode] Performing garbage collection");
  170. // We track corrupt files and chunks here.
  171. let mut deleted_files = HashSet::new();
  172. let mut deleted_chunks = HashSet::new();
  173. let mut deleted_chunk_paths = HashSet::new();
  174. // Scan through available chunks and check them for consistency.
  175. let mut chunk_paths = fs::read_dir(&self.chunks_path).await?;
  176. let mut buf = [0u8; MAX_CHUNK_SIZE];
  177. while let Some(chunk) = chunk_paths.next().await {
  178. let Ok(entry) = chunk else { continue };
  179. let chunk_path = entry.path();
  180. // Skip if we're not a plain file
  181. if !chunk_path.is_file() {
  182. continue
  183. }
  184. // Make sure that the filename is a BLAKE3 hash
  185. let file_name = match chunk_path.file_name().and_then(|n| n.to_str()) {
  186. Some(v) => v,
  187. None => continue,
  188. };
  189. let mut hash_buf = [0u8; 32];
  190. let chunk_hash = match bs58::decode(file_name).onto(&mut hash_buf) {
  191. Ok(_) => blake3::Hash::from_bytes(hash_buf),
  192. Err(_) => continue,
  193. };
  194. // If there is a problem with opening the file, remove it.
  195. let Ok(mut chunk_fd) = File::open(&chunk_path).await else {
  196. deleted_chunk_paths.insert(chunk_path);
  197. deleted_chunks.insert(chunk_hash);
  198. continue
  199. };
  200. // Perform consistency check
  201. let Ok(bytes_read) = read_until_filled(&mut chunk_fd, &mut buf).await else {
  202. deleted_chunk_paths.insert(chunk_path);
  203. deleted_chunks.insert(chunk_hash);
  204. buf = [0u8; MAX_CHUNK_SIZE];
  205. continue
  206. };
  207. let chunk_slice = &buf[..bytes_read];
  208. let hashed_chunk = blake3::hash(chunk_slice);
  209. // If the hash doesn't match the filename, remove it.
  210. if chunk_hash != hashed_chunk {
  211. deleted_chunk_paths.insert(chunk_path);
  212. deleted_chunks.insert(chunk_hash);
  213. buf = [0u8; MAX_CHUNK_SIZE];
  214. continue
  215. }
  216. // Seems legit.
  217. buf = [0u8; MAX_CHUNK_SIZE];
  218. }
  219. for chunk_path in &deleted_chunk_paths {
  220. if let Err(e) = fs::remove_file(chunk_path).await {
  221. warn!(
  222. target: "geode::garbage_collect()",
  223. "[Geode] Garbage collect failed to remove corrupted chunk: {}", e,
  224. );
  225. }
  226. }
  227. // Perform health check over file metadata. For now we just ensure they
  228. // have the correct format.
  229. let mut file_paths = fs::read_dir(&self.files_path).await?;
  230. while let Some(file) = file_paths.next().await {
  231. let Ok(entry) = file else { continue };
  232. let path = entry.path();
  233. // Skip if we're not a plain file
  234. if !path.is_file() {
  235. continue
  236. }
  237. // Make sure that the filename is a BLAKE3 hash
  238. let file_name = match path.file_name().and_then(|n| n.to_str()) {
  239. Some(v) => v,
  240. None => continue,
  241. };
  242. let mut hash_buf = [0u8; 32];
  243. let file_hash = match bs58::decode(file_name).onto(&mut hash_buf) {
  244. Ok(_) => blake3::Hash::from_bytes(hash_buf),
  245. Err(_) => continue,
  246. };
  247. // The filename is a BLAKE3 hash. It should contain a newline-separated
  248. // list of chunks which represent the full file. If that is not the case
  249. // we will consider it a corrupted file and delete it.
  250. if Self::read_metadata(&path).await.is_err() {
  251. if let Err(e) = fs::remove_file(path).await {
  252. warn!(
  253. target: "geode::garbage_collect()",
  254. "[Geode] Garbage collect failed to remove corrupted file: {}", e,
  255. );
  256. }
  257. deleted_files.insert(file_hash);
  258. continue
  259. }
  260. }
  261. info!(target: "geode::garbage_collect()", "[Geode] Garbage collection finished");
  262. Ok((deleted_files, deleted_chunks))
  263. }
  264. /// Insert a file into Geode. The function expects any kind of byte stream, which
  265. /// can either be another file on the filesystem, a buffer, etc.
  266. /// Returns a tuple of `(blake3::Hash, Vec<blake3::Hash>)` which represents the
  267. /// file hash, and the file's chunks, respectively.
  268. pub async fn insert(
  269. &self,
  270. mut stream: impl AsyncRead + Unpin,
  271. ) -> Result<(blake3::Hash, Vec<blake3::Hash>)> {
  272. info!(target: "geode::insert()", "[Geode] Inserting file...");
  273. let mut file_hasher = blake3::Hasher::new();
  274. let mut chunk_hashes = vec![];
  275. let mut buf = [0u8; MAX_CHUNK_SIZE];
  276. loop {
  277. let bytes_read = read_until_filled(&mut stream, &mut buf).await?;
  278. if bytes_read == 0 {
  279. break
  280. }
  281. let chunk_slice = &buf[..bytes_read];
  282. let chunk_hash = blake3::hash(chunk_slice);
  283. file_hasher.update(chunk_hash.as_bytes());
  284. chunk_hashes.push(chunk_hash);
  285. // Write the chunk to a file, if necessary. We first perform
  286. // a consistency check and if things are fine, we don't have
  287. // to perform a write, which is usually more expensive than
  288. // reading from disk.
  289. let mut chunk_path = self.chunks_path.clone();
  290. chunk_path.push(hash_to_string(&chunk_hash).as_str());
  291. let chunk_fd =
  292. OpenOptions::new().read(true).write(true).create(true).open(&chunk_path).await?;
  293. let mut fs_buf = [0u8; MAX_CHUNK_SIZE];
  294. let fs_bytes_read = read_until_filled(chunk_fd, &mut fs_buf).await?;
  295. let fs_chunk_slice = &fs_buf[..fs_bytes_read];
  296. let fs_chunk_hash = blake3::hash(fs_chunk_slice);
  297. if fs_chunk_hash != chunk_hash {
  298. debug!(
  299. target: "geode::insert()",
  300. "Existing chunk inconsistent or unavailable. Writing chunk to {:?}",
  301. chunk_path,
  302. );
  303. // Here the chunk is broken, so we'll truncate and write the new one.
  304. let mut chunk_fd = OpenOptions::new()
  305. .read(true)
  306. .write(true)
  307. .create(true)
  308. .open(&chunk_path)
  309. .await?;
  310. chunk_fd.set_len(0).await?;
  311. chunk_fd.seek(SeekFrom::Start(0)).await?;
  312. chunk_fd.write_all(chunk_slice).await?;
  313. chunk_fd.flush().await?;
  314. } else {
  315. debug!(
  316. target: "geode::insert()",
  317. "Existing chunk consistent. Skipping write to {:?}",
  318. chunk_path,
  319. );
  320. }
  321. buf = [0u8; MAX_CHUNK_SIZE];
  322. }
  323. // This hash is the file's chunks hashes hashed in order.
  324. let file_hash = file_hasher.finalize();
  325. let mut file_path = self.files_path.clone();
  326. file_path.push(hash_to_string(&file_hash).as_str());
  327. // We always overwrite the metadata.
  328. let mut file_fd = File::create(&file_path).await?;
  329. for ch in &chunk_hashes {
  330. file_fd.write(format!("{}\n", hash_to_string(ch).as_str()).as_bytes()).await?;
  331. }
  332. file_fd.flush().await?;
  333. Ok((file_hash, chunk_hashes))
  334. }
  335. /// Create and insert file metadata into Geode given a list of hashes.
  336. /// Always overwrites any existing file.
  337. /// Verifies that the file hash matches the chunk hashes
  338. pub async fn insert_file(
  339. &self,
  340. file_hash: &blake3::Hash,
  341. chunk_hashes: &[blake3::Hash],
  342. ) -> Result<()> {
  343. info!(target: "geode::insert_file()", "[Geode] Inserting file metadata");
  344. if !self.verify_file(file_hash, chunk_hashes) {
  345. // The chunk list or file hash is wrong
  346. return Err(Error::GeodeNeedsGc)
  347. }
  348. let mut file_path = self.files_path.clone();
  349. file_path.push(hash_to_string(file_hash).as_str());
  350. let mut file_fd = File::create(&file_path).await?;
  351. for ch in chunk_hashes {
  352. file_fd.write(format!("{}\n", hash_to_string(ch).as_str()).as_bytes()).await?;
  353. }
  354. file_fd.flush().await?;
  355. Ok(())
  356. }
  357. /// Create and insert a single chunk into Geode given a stream.
  358. /// Always overwrites any existing chunk. Returns the chunk hash once inserted.
  359. pub async fn insert_chunk(&self, stream: impl AsRef<[u8]>) -> Result<blake3::Hash> {
  360. info!(target: "geode::insert_chunk()", "[Geode] Inserting single chunk");
  361. let mut cursor = Cursor::new(&stream);
  362. let mut chunk = [0u8; MAX_CHUNK_SIZE];
  363. let bytes_read = read_until_filled(&mut cursor, &mut chunk).await?;
  364. let chunk_slice = &chunk[..bytes_read];
  365. let chunk_hash = blake3::hash(chunk_slice);
  366. let mut chunk_path = self.chunks_path.clone();
  367. chunk_path.push(hash_to_string(&chunk_hash).as_str());
  368. let mut chunk_fd = File::create(&chunk_path).await?;
  369. chunk_fd.write_all(chunk_slice).await?;
  370. chunk_fd.flush().await?;
  371. Ok(chunk_hash)
  372. }
  373. /// Fetch file metadata from Geode. Returns [`ChunkedFile`] which gives a list
  374. /// of chunks and optionally file paths to the said chunks. Returns an error if
  375. /// the read failed in any way (could also be the file does not exist).
  376. pub async fn get(&self, file_hash: &blake3::Hash) -> Result<ChunkedFile> {
  377. let file_hash_str = hash_to_string(file_hash);
  378. info!(target: "geode::get()", "[Geode] Getting file chunks for {}...", file_hash_str);
  379. let mut file_path = self.files_path.clone();
  380. file_path.push(file_hash_str);
  381. // Try to read the file metadata. If it's corrupt, return an error signalling
  382. // that garbage collection needs to run.
  383. let chunk_hashes = match Self::read_metadata(&file_path).await {
  384. Ok(v) => v,
  385. Err(e) => {
  386. return match e {
  387. // If the file is not found, return according error.
  388. Error::Io(std::io::ErrorKind::NotFound) => Err(Error::GeodeFileNotFound),
  389. // Anything else should tell the client to do garbage collection
  390. _ => Err(Error::GeodeNeedsGc),
  391. }
  392. }
  393. };
  394. // Make sure the chunk hashes match with the file hash
  395. if !self.verify_file(file_hash, &chunk_hashes) {
  396. return Err(Error::GeodeNeedsGc);
  397. }
  398. let mut chunked_file = ChunkedFile::new(&chunk_hashes);
  399. // Iterate over chunks and find which chunks we have available locally.
  400. let mut buf = vec![];
  401. for (chunk_hash, chunk_path) in chunked_file.0.iter_mut() {
  402. let mut c_path = self.chunks_path.clone();
  403. c_path.push(hash_to_string(chunk_hash).as_str());
  404. if !c_path.exists() || !c_path.is_file() {
  405. // TODO: We should be aggressive here and remove the non-file.
  406. continue
  407. }
  408. // Perform chunk consistency check
  409. let mut chunk_fd = File::open(&c_path).await?;
  410. let bytes_read = chunk_fd.read_to_end(&mut buf).await?;
  411. let chunk_slice = &buf[..bytes_read];
  412. let hashed_chunk = blake3::hash(chunk_slice);
  413. if &hashed_chunk != chunk_hash {
  414. // The chunk is corrupted/inconsistent. Garbage collection should run.
  415. buf = vec![];
  416. continue
  417. }
  418. *chunk_path = Some(c_path);
  419. buf = vec![];
  420. }
  421. Ok(chunked_file)
  422. }
  423. /// Fetch a single chunk from Geode. Returns a `PathBuf` pointing to the chunk
  424. /// if it is found.
  425. pub async fn get_chunk(&self, chunk_hash: &blake3::Hash) -> Result<PathBuf> {
  426. let chunk_hash_str = hash_to_string(chunk_hash);
  427. info!(target: "geode::get_chunk()", "[Geode] Getting chunk {}", chunk_hash_str);
  428. let mut chunk_path = self.chunks_path.clone();
  429. chunk_path.push(chunk_hash_str);
  430. if !chunk_path.exists() || !chunk_path.is_file() {
  431. // TODO: We should be aggressive here and remove the non-file.
  432. return Err(Error::GeodeChunkNotFound)
  433. }
  434. // Perform chunk consistency check
  435. let mut buf = vec![];
  436. let mut chunk_fd = File::open(&chunk_path).await?;
  437. let bytes_read = chunk_fd.read_to_end(&mut buf).await?;
  438. if !self.verify_chunk(chunk_hash, &buf[..bytes_read]) {
  439. // The chunk is corrupted
  440. return Err(Error::GeodeNeedsGc)
  441. }
  442. Ok(chunk_path)
  443. }
  444. /// Verifies that the file hash matches the chunk hashes.
  445. pub fn verify_file(&self, file_hash: &blake3::Hash, chunk_hashes: &[blake3::Hash]) -> bool {
  446. info!(target: "geode::verify_file()", "[Geode] Verifying file metadata");
  447. let mut file_hasher = blake3::Hasher::new();
  448. for chunk_hash in chunk_hashes {
  449. file_hasher.update(chunk_hash.as_bytes());
  450. }
  451. *file_hash == file_hasher.finalize()
  452. }
  453. /// Verifies that the chunk hash matches the content.
  454. pub fn verify_chunk(&self, chunk_hash: &blake3::Hash, chunk_slice: &[u8]) -> bool {
  455. blake3::hash(chunk_slice) == *chunk_hash
  456. }
  457. /// Assemble chunks to create a file.
  458. /// This method does NOT perform a consistency check.
  459. pub async fn assemble_file(
  460. &self,
  461. file_hash: &blake3::Hash,
  462. chunked_file: &ChunkedFile,
  463. file_path: &PathBuf,
  464. ) -> Result<()> {
  465. let file_hash_str = hash_to_string(file_hash);
  466. info!(target: "geode::assemble_file()", "[Geode] Assembling file {}", file_hash_str);
  467. if file_path.exists() && file_path.is_dir() {
  468. return Err(Error::Custom("File path is an existing directory".to_string())) // TODO
  469. }
  470. let mut file_fd = File::create(&file_path).await?;
  471. for (_, chunk_path) in chunked_file.iter() {
  472. let mut buf = vec![];
  473. let mut chunk_fd = File::open(chunk_path.clone().unwrap()).await?;
  474. let bytes_read = chunk_fd.read_to_end(&mut buf).await?;
  475. let chunk_slice = &buf[..bytes_read];
  476. file_fd.write(chunk_slice).await?;
  477. file_fd.flush().await?;
  478. }
  479. Ok(())
  480. }
  481. /// List file hashes.
  482. pub async fn list_files(&self) -> Result<Vec<blake3::Hash>> {
  483. info!(target: "geode::list_files()", "[Geode] Listing files");
  484. let mut dir = fs::read_dir(&self.files_path).await?;
  485. let mut file_hashes = vec![];
  486. while let Some(file) = dir.try_next().await? {
  487. let os_file_name = file.file_name();
  488. let file_name = os_file_name.to_string_lossy().to_string();
  489. let mut hash_buf = [0u8; 32];
  490. let file_hash = match bs58::decode(file_name).onto(&mut hash_buf) {
  491. Ok(_) => blake3::Hash::from_bytes(hash_buf),
  492. Err(_) => continue,
  493. };
  494. file_hashes.push(file_hash);
  495. }
  496. Ok(file_hashes)
  497. }
  498. }