mod.rs 19 KB

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