mod.rs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  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/directory insertion and retrieval. There is
  22. //! intentionally no `remove` support. File removal should be handled
  23. //! externally, and then it is only required to run `garbage_collect()` to
  24. //! clean things up.
  25. //!
  26. //! The hash of a file is the BLAKE3 hash of hashed chunks in the correct
  27. //! order.
  28. //! The hash of a directory is the BLAKE3 hash of hashed chunks in the correct
  29. //! order and the ordered list of (file path, file sizes).
  30. //! All hashes (file, directory, chunk) are 32 bytes long, and are encoded in
  31. //! base58 whenever necessary.
  32. //!
  33. //! The filesystem hierarchy stores a `files` directory storing metadata
  34. //! about a full file and a `directories` directory storing metadata about all
  35. //! files in a directory (all subdirectories included).
  36. //! The filename of a file in `files` or `directories` is the hash of the
  37. //! file/directory as defined above.
  38. //! Inside a file in `files` is the ordered list of the chunks making up the
  39. //! full file.
  40. //! Inside a file in `directories` is the ordered list of the chunks making up
  41. //! each full file, and the (relative) file path and hash of all files in the
  42. //! directory.
  43. //!
  44. //! To get the chunks you split the full file into `MAX_CHUNK_SIZE` sized
  45. //! slices, the last chunk is the only one that can be smaller than that.
  46. //!
  47. //! It might look like the following:
  48. //! ```
  49. //! /files/B9fFKaEYphw2oH5PDbeL1TTAcSzL6ax84p8SjBKzuYzX
  50. //! /files/8nA3ndjFFee3n5wMPLZampLpGaMJi3od4MSyaXPDoF91
  51. //! /files/...
  52. //! /directories/FXDduPcEohVzsSxtNVSFU64qtYxEVEHBMkF4k5cBvt3B
  53. //! /directories/AHjU1LizfGqsGnF8VSa9kphSQ5pqS4YjmPqme5RZajsj
  54. //! /directories/...
  55. //! ```
  56. //!
  57. //! Inside a file metadata (file in `files`) is the ordered list of chunk
  58. //! hashes, for example:
  59. //! ```
  60. //! 2bQPxSR8Frz7S7JW3DRAzEtkrHfLXB1CN65V7az77pUp
  61. //! CvjvN6MfWQYK54DgKNR7MPgFSZqsCgpWKF2p8ot66CCP
  62. //! ```
  63. //!
  64. //! Inside a directory metadata (file in `directories`) is, in addition to
  65. //! chunk hashes, the path and size of each file in the directory. For example:
  66. //! ```
  67. //! 8Kb55jeqJsq7WTBN93gvBzh2zmXAXVPh111VqD3Hi42V
  68. //! GLiBqpLPTbpJhSMYfzi3s7WivrTViov7ShX7uso6fG5s
  69. //! picture.jpg 312948
  70. //! ```
  71. //! Chunks of a directory can include multiple files, if multiple files fit
  72. //! into `MAX_CHUNK_SIZE`. The chunks are computed as if all the files were
  73. //! concatenated into a single big file, to minimize the number of chunks.
  74. //!
  75. //! The full file is not copied, and individual chunks are not stored by
  76. //! geode. Additionally it does not keep track of the full files path.
  77. use std::{collections::HashSet, path::PathBuf};
  78. use futures::{AsyncRead, AsyncSeek};
  79. use log::{debug, info, warn};
  80. use smol::{
  81. fs::{self, File},
  82. io::{AsyncBufReadExt, AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufReader, Cursor, SeekFrom},
  83. stream::StreamExt,
  84. };
  85. use std::path::Path;
  86. use crate::{Error, Result};
  87. mod chunked_storage;
  88. pub use chunked_storage::ChunkedStorage;
  89. mod file_sequence;
  90. pub use file_sequence::FileSequence;
  91. mod util;
  92. pub use util::{hash_to_string, read_until_filled};
  93. /// Defined maximum size of a stored chunk (256 KiB)
  94. pub const MAX_CHUNK_SIZE: usize = 262_144;
  95. /// Path prefix where file metadata is stored
  96. const FILES_PATH: &str = "files";
  97. /// Path prefix where directory metadata is stored
  98. const DIRS_PATH: &str = "directories";
  99. /// Chunk-based file storage interface.
  100. pub struct Geode {
  101. /// Path to the filesystem directory where file metadata is stored
  102. pub files_path: PathBuf,
  103. /// Path to the filesystem directory where directory metadata is stored
  104. pub dirs_path: PathBuf,
  105. }
  106. impl Geode {
  107. /// Instantiate a new [`Geode`] object.
  108. /// `base_path` defines the root directory where Geode will store its
  109. /// file metadata and chunks.
  110. pub async fn new(base_path: &PathBuf) -> Result<Self> {
  111. let mut files_path: PathBuf = base_path.into();
  112. files_path.push(FILES_PATH);
  113. let mut dirs_path: PathBuf = base_path.into();
  114. dirs_path.push(DIRS_PATH);
  115. // Create necessary directory structure if needed
  116. fs::create_dir_all(&files_path).await?;
  117. fs::create_dir_all(&dirs_path).await?;
  118. Ok(Self { files_path, dirs_path })
  119. }
  120. /// Attempt to read chunk hashes and files metadata from a given metadata path.
  121. /// This works for both file metadata and directory metadata.
  122. /// Returns (chunk hashes, [(file path, file size)]).
  123. async fn read_metadata(path: &PathBuf) -> Result<(Vec<blake3::Hash>, Vec<(PathBuf, u64)>)> {
  124. debug!(target: "geode::read_dir_metadata()", "Reading chunks from {:?} (dir)", path);
  125. let mut chunk_hashes = vec![];
  126. let mut files = vec![];
  127. let fd = File::open(path).await?;
  128. let mut lines = BufReader::new(fd).lines();
  129. while let Some(line) = lines.next().await {
  130. let line = line?;
  131. let line = line.trim();
  132. if line.is_empty() {
  133. continue; // Skip empty lines
  134. }
  135. let parts: Vec<&str> = line.split_whitespace().collect();
  136. if parts.len() == 2 {
  137. // File
  138. let file_path = PathBuf::from(parts[0]);
  139. if file_path.clone().is_absolute() {
  140. return Err(Error::Custom(format!(
  141. "Path of file {} is absolute, which is not allowed",
  142. parts[0]
  143. )))
  144. }
  145. // Check for `..` in the path components
  146. for component in file_path.clone().components() {
  147. if component == std::path::Component::ParentDir {
  148. return Err(Error::Custom(format!("Path of file {} contains reference to parent dir, which is not allowed", parts[0])))
  149. }
  150. }
  151. let file_size = parts[1].parse::<u64>()?;
  152. files.push((file_path, file_size));
  153. } else if parts.len() == 1 {
  154. // Chunk
  155. let chunk_hash_str = parts[0].trim();
  156. if chunk_hash_str.is_empty() {
  157. break; // Stop reading chunk hashes on empty line
  158. }
  159. let mut hash_buf = [0u8; 32];
  160. bs58::decode(chunk_hash_str).onto(&mut hash_buf)?;
  161. let chunk_hash = blake3::Hash::from_bytes(hash_buf);
  162. chunk_hashes.push(chunk_hash);
  163. } else {
  164. // Invalid format
  165. return Err(Error::Custom("Invalid directory metadata format".to_string()));
  166. }
  167. }
  168. Ok((chunk_hashes, files))
  169. }
  170. /// Perform garbage collection over the filesystem hierarchy.
  171. /// Returns a set representing deleted files.
  172. pub async fn garbage_collect(&self) -> Result<HashSet<blake3::Hash>> {
  173. info!(target: "geode::garbage_collect()", "[Geode] Performing garbage collection");
  174. // We track corrupt files here.
  175. let mut deleted_files = HashSet::new();
  176. // Perform health check over metadata. For now we just ensure they
  177. // have the correct format.
  178. let file_paths = fs::read_dir(&self.files_path).await?;
  179. let dir_paths = fs::read_dir(&self.dirs_path).await?;
  180. let mut paths = file_paths.chain(dir_paths);
  181. while let Some(file) = paths.next().await {
  182. let Ok(entry) = file else { continue };
  183. let path = entry.path();
  184. // Skip if we're not a plain file
  185. if !path.is_file() {
  186. continue
  187. }
  188. // Make sure that the filename is a BLAKE3 hash
  189. let file_name = match path.file_name().and_then(|n| n.to_str()) {
  190. Some(v) => v,
  191. None => continue,
  192. };
  193. let mut hash_buf = [0u8; 32];
  194. let hash = match bs58::decode(file_name).onto(&mut hash_buf) {
  195. Ok(_) => blake3::Hash::from_bytes(hash_buf),
  196. Err(_) => continue,
  197. };
  198. // The filename is a BLAKE3 hash. It should contain a newline-separated
  199. // list of chunks which represent the full file. If that is not the case
  200. // we will consider it a corrupted file and delete it.
  201. if Self::read_metadata(&path).await.is_err() {
  202. if let Err(e) = fs::remove_file(path).await {
  203. warn!(
  204. target: "geode::garbage_collect()",
  205. "[Geode] Garbage collect failed to remove corrupted metadata: {}", e,
  206. );
  207. }
  208. deleted_files.insert(hash);
  209. continue
  210. }
  211. }
  212. info!(target: "geode::garbage_collect()", "[Geode] Garbage collection finished");
  213. Ok(deleted_files)
  214. }
  215. /// Chunk a stream.
  216. /// Returns a hasher (containing the chunk hashes), and the list of chunk hashes.
  217. pub async fn chunk_stream(
  218. &self,
  219. mut stream: impl AsyncRead + Unpin,
  220. ) -> Result<(blake3::Hasher, Vec<blake3::Hash>)> {
  221. let mut hasher = blake3::Hasher::new();
  222. let mut chunk_hashes = vec![];
  223. loop {
  224. let mut buf = vec![0u8; MAX_CHUNK_SIZE];
  225. let bytes_read = stream.read(&mut buf).await?;
  226. if bytes_read == 0 {
  227. break
  228. }
  229. let chunk_hash = blake3::hash(&buf[..bytes_read]);
  230. hasher.update(chunk_hash.as_bytes());
  231. chunk_hashes.push(chunk_hash);
  232. }
  233. Ok((hasher, chunk_hashes))
  234. }
  235. /// Sorts files by their PathBuf.
  236. pub fn sort_files(&self, files: &mut [(PathBuf, u64)]) {
  237. files.sort_by(|(a, _), (b, _)| a.to_string_lossy().cmp(&b.to_string_lossy()));
  238. }
  239. /// Add chunk hashes to `hasher`.
  240. pub fn hash_chunks_metadata(&self, hasher: &mut blake3::Hasher, chunk_hashes: &[blake3::Hash]) {
  241. for chunk in chunk_hashes {
  242. hasher.update(chunk.as_bytes());
  243. }
  244. }
  245. /// Add files metadata to `hasher`.
  246. /// You must sort the files using `sort_files`.
  247. pub fn hash_files_metadata(
  248. &self,
  249. hasher: &mut blake3::Hasher,
  250. relative_files: &[(PathBuf, u64)],
  251. ) {
  252. for file in relative_files {
  253. hasher.update(file.0.to_string_lossy().to_string().as_bytes());
  254. hasher.update(&file.1.to_le_bytes());
  255. }
  256. }
  257. /// Create and insert file or directory metadata into Geode.
  258. /// Always overwrites any existing file.
  259. /// Verifies that the metadata is valid.
  260. /// The `relative_files` slice is empty for files.
  261. pub async fn insert_metadata(
  262. &self,
  263. hash: &blake3::Hash,
  264. chunk_hashes: &[blake3::Hash],
  265. relative_files: &[(PathBuf, u64)],
  266. ) -> Result<()> {
  267. info!(target: "geode::insert_metadata()", "[Geode] Inserting directory metadata");
  268. // Verify the metadata
  269. if !self.verify_metadata(hash, chunk_hashes, relative_files) {
  270. return Err(Error::GeodeNeedsGc)
  271. }
  272. // Write the metadata file
  273. let mut file_path = match relative_files.is_empty() {
  274. true => self.files_path.clone(),
  275. false => self.dirs_path.clone(),
  276. };
  277. file_path.push(hash_to_string(hash).as_str());
  278. let mut file_fd = File::create(&file_path).await?;
  279. for ch in chunk_hashes {
  280. file_fd.write(format!("{}\n", hash_to_string(ch).as_str()).as_bytes()).await?;
  281. }
  282. for file in relative_files {
  283. file_fd.write(format!("{} {}\n", file.0.to_string_lossy(), file.1).as_bytes()).await?;
  284. }
  285. file_fd.flush().await?;
  286. Ok(())
  287. }
  288. /// Write a single chunk given a stream.
  289. /// The file must be inserted into Geode before calling this method.
  290. /// Always overwrites any existing chunk. Returns the chunk hash once inserted.
  291. pub async fn write_chunk(
  292. &self,
  293. chunked: &mut ChunkedStorage,
  294. stream: impl AsRef<[u8]>,
  295. ) -> Result<blake3::Hash> {
  296. info!(target: "geode::write_chunk()", "[Geode] Writing single chunk");
  297. let mut cursor = Cursor::new(&stream);
  298. let mut chunk = vec![0u8; MAX_CHUNK_SIZE];
  299. // Read the stream to get the chunk content
  300. let chunk_slice = read_until_filled(&mut cursor, &mut chunk).await?;
  301. // Get the chunk hash from the content
  302. let chunk_hash = blake3::hash(chunk_slice);
  303. // Get the chunk index in the file/directory from the chunk hash
  304. let chunk_index = match chunked.iter().position(|(h, _)| *h == chunk_hash) {
  305. Some(index) => index,
  306. None => {
  307. return Err(Error::GeodeNeedsGc);
  308. }
  309. };
  310. // Compute byte position from the chunk index and the chunk size
  311. let position = (chunk_index as u64) * (MAX_CHUNK_SIZE as u64);
  312. // Seek to the correct position
  313. let fileseq = &mut chunked.get_fileseq();
  314. fileseq.seek(SeekFrom::Start(position)).await?;
  315. // This will write the chunk, and truncate files if `chunked` is a directory.
  316. fileseq.write_all(chunk_slice).await?;
  317. // If it's the last chunk of a file (and it's *not* a directory),
  318. // truncate the file to the correct length.
  319. // This is because contrary to directories, for a file shared on fud we
  320. // do not know the exact file size from its metadata, we only know the
  321. // number of chunks. Therefore we only know the exact size once we know
  322. // the size of the last chunk.
  323. if !chunked.is_dir() && chunk_index == chunked.len() - 1 {
  324. let exact_file_size =
  325. chunked.len() * MAX_CHUNK_SIZE - (MAX_CHUNK_SIZE - chunk_slice.len());
  326. if let Some(file) = &chunked.get_fileseq().get_current_file() {
  327. let _ = file.set_len(exact_file_size as u64).await;
  328. }
  329. }
  330. Ok(chunk_hash)
  331. }
  332. /// Iterate over chunks and find which chunks are available locally.
  333. pub async fn verify_chunks(&self, chunked_file: &mut ChunkedStorage) -> Result<()> {
  334. let chunks = chunked_file.get_chunks().clone();
  335. let mut available_chunks = vec![];
  336. // Gather all available chunks
  337. for (chunk_index, (chunk_hash, _)) in chunks.iter().enumerate() {
  338. // Read the chunk using the mutable reader
  339. let chunk = self.read_chunk(&mut chunked_file.get_fileseq(), &chunk_index).await?;
  340. // Perform chunk consistency check
  341. if self.verify_chunk(chunk_hash, &chunk) {
  342. available_chunks.push(chunk_index);
  343. }
  344. }
  345. // Update available chunks
  346. for chunk_index in available_chunks {
  347. chunked_file.get_chunk_mut(chunk_index).1 = true;
  348. }
  349. Ok(())
  350. }
  351. /// Fetch file/directory metadata from Geode. Returns [`Chunked`]. Returns an error if
  352. /// the read failed in any way (could also be the file does not exist).
  353. pub async fn get(&self, hash: &blake3::Hash, path: &Path) -> Result<ChunkedStorage> {
  354. let hash_str = hash_to_string(hash);
  355. info!(target: "geode::get()", "[Geode] Getting chunks for {}...", hash_str);
  356. // Try to read the file or dir metadata. If it's corrupt, return an error signalling
  357. // that garbage collection needs to run.
  358. let metadata_paths = [self.files_path.join(&hash_str), self.dirs_path.join(&hash_str)];
  359. for metadata_path in metadata_paths {
  360. match Self::read_metadata(&metadata_path).await {
  361. Ok((chunk_hashes, files)) => {
  362. return self.create_chunked_storage(hash, path, &chunk_hashes, &files).await
  363. }
  364. Err(e) => {
  365. if !matches!(e, Error::Io(std::io::ErrorKind::NotFound)) {
  366. return Err(Error::GeodeNeedsGc)
  367. }
  368. }
  369. };
  370. }
  371. Err(Error::GeodeFileNotFound)
  372. }
  373. /// Create a ChunkedStorage from metadata.
  374. /// `hash` is the hash of the file or directory.
  375. async fn create_chunked_storage(
  376. &self,
  377. hash: &blake3::Hash,
  378. path: &Path,
  379. chunk_hashes: &[blake3::Hash],
  380. relative_files: &[(PathBuf, u64)], // Only used by directories
  381. ) -> Result<ChunkedStorage> {
  382. // Make sure the file or directory is valid
  383. if !self.verify_metadata(hash, chunk_hashes, relative_files) {
  384. return Err(Error::GeodeNeedsGc);
  385. }
  386. let chunked = if relative_files.is_empty() {
  387. // File
  388. let file_size = (chunk_hashes.len() * MAX_CHUNK_SIZE) as u64; // Upper bound, not actual file size
  389. ChunkedStorage::new(chunk_hashes, &[(path.to_path_buf(), file_size)], false)
  390. } else {
  391. // Directory
  392. let files: Vec<_> = relative_files
  393. .iter()
  394. .map(|(file_path, size)| (path.join(file_path), *size))
  395. .collect();
  396. ChunkedStorage::new(chunk_hashes, &files, true)
  397. };
  398. Ok(chunked)
  399. }
  400. /// Fetch a single chunk from Geode. Returns a Vec containing the chunk content
  401. /// if it is found.
  402. pub async fn get_chunk(
  403. &self,
  404. chunked: &mut ChunkedStorage,
  405. chunk_hash: &blake3::Hash,
  406. path: &Path,
  407. ) -> Result<Vec<u8>> {
  408. info!(target: "geode::get_chunk()", "[Geode] Getting chunk {}", hash_to_string(chunk_hash));
  409. if !path.exists() {
  410. return Err(Error::GeodeChunkNotFound)
  411. }
  412. // Get the chunk index in the file from the chunk hash
  413. let chunk_index = match chunked.iter().position(|(h, _)| *h == *chunk_hash) {
  414. Some(index) => index,
  415. None => return Err(Error::GeodeChunkNotFound),
  416. };
  417. // Read the file to get the chunk content
  418. let chunk = self.read_chunk(&mut chunked.get_fileseq(), &chunk_index).await?;
  419. // Perform chunk consistency check
  420. if !self.verify_chunk(chunk_hash, &chunk) {
  421. return Err(Error::GeodeNeedsGc)
  422. }
  423. Ok(chunk)
  424. }
  425. /// Read the file at `file_path` to get its chunk with index `chunk_index`.
  426. /// Returns the chunk content in a Vec.
  427. async fn read_chunk(
  428. &self,
  429. mut stream: impl AsyncRead + Unpin + AsyncSeek,
  430. chunk_index: &usize,
  431. ) -> Result<Vec<u8>> {
  432. let position = (*chunk_index as u64) * (MAX_CHUNK_SIZE as u64);
  433. let mut buf = vec![0u8; MAX_CHUNK_SIZE];
  434. stream.seek(SeekFrom::Start(position)).await?;
  435. let bytes_read = stream.read(&mut buf).await?;
  436. Ok(buf[..bytes_read].to_vec())
  437. }
  438. /// Verifies that the file hash matches the chunk hashes.
  439. pub fn verify_metadata(
  440. &self,
  441. hash: &blake3::Hash,
  442. chunk_hashes: &[blake3::Hash],
  443. files: &[(PathBuf, u64)],
  444. ) -> bool {
  445. info!(target: "geode::verify_metadata()", "[Geode] Verifying metadata for {}", hash_to_string(hash));
  446. let mut hasher = blake3::Hasher::new();
  447. self.hash_chunks_metadata(&mut hasher, chunk_hashes);
  448. self.hash_files_metadata(&mut hasher, files);
  449. *hash == hasher.finalize()
  450. }
  451. /// Verifies that the chunk hash matches the content.
  452. pub fn verify_chunk(&self, chunk_hash: &blake3::Hash, chunk_slice: &[u8]) -> bool {
  453. info!(target: "geode::verify_chunk()", "[Geode] Verifying chunk {}", hash_to_string(chunk_hash));
  454. blake3::hash(chunk_slice) == *chunk_hash
  455. }
  456. }