mod.rs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2023 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/7d4c0d5539057c8f9b60d32b423964beb38ecd8ea1ab203c0207990cbf0cad22
  35. //! /files/...
  36. //! /chunks/9d7abc2efa52b8be63ff82b756edb6822e09aa40fc587aba977185a5bb449c19
  37. //! /chunks/fc432e087d16d8788e87640511e627be34a4a50533f1e5ed3e1370645a0266b8
  38. //! /chunks/...
  39. //! ```
  40. //!
  41. //! In the above example, contents of `7d4c0d5539057c8f9b60d32b423964beb38ecd8ea1ab203c0207990cbf0cad22`
  42. //! may be:
  43. //! ```
  44. //! 9d7abc2efa52b8be63ff82b756edb6822e09aa40fc587aba977185a5bb449c19
  45. //! fc432e087d16d8788e87640511e627be34a4a50533f1e5ed3e1370645a0266b8
  46. //! ```
  47. //!
  48. //! This means, in order to retrieve `7d4c0d5539057c8f9b60d32b423964beb38ecd8ea1ab203c0207990cbf0cad22`,
  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;
  58. use async_std::{
  59. fs::{self, File, OpenOptions},
  60. io::{prelude::*, BufReader, Cursor, SeekFrom},
  61. path::PathBuf,
  62. stream::StreamExt,
  63. };
  64. use futures::AsyncRead;
  65. use log::{debug, info, warn};
  66. use crate::{Error, Result};
  67. /// Defined maximum size of a stored chunk (256 KiB)
  68. pub const MAX_CHUNK_SIZE: usize = 262_144;
  69. /// Path prefix where file metadata is stored
  70. const FILES_PATH: &str = "files";
  71. /// Path prefix where file chunks are stored
  72. const CHUNKS_PATH: &str = "chunks";
  73. /// `ChunkedFile` is a representation of a file we're trying to
  74. /// retrieve from `Geode`. The tuple contains `blake3::Hash` of
  75. /// the file's chunks and an optional `PathBuf` which points to
  76. /// the filesystem where the chunk can be found. If `None`, it
  77. /// is to be assumed that the chunk is not available locally.
  78. #[derive(Clone)]
  79. pub struct ChunkedFile(Vec<(blake3::Hash, Option<PathBuf>)>);
  80. impl ChunkedFile {
  81. fn new(hashes: &[blake3::Hash]) -> Self {
  82. Self(hashes.iter().map(|x| (*x, None)).collect())
  83. }
  84. /// Check whether we have all the chunks available locally.
  85. pub fn is_complete(&self) -> bool {
  86. !self.0.iter().any(|(_, p)| p.is_none())
  87. }
  88. /// Return an iterator over the chunks and their paths.
  89. pub fn iter(&self) -> core::slice::Iter<'_, (blake3::Hash, Option<PathBuf>)> {
  90. self.0.iter()
  91. }
  92. }
  93. /// Chunk-based file storage interface.
  94. pub struct Geode {
  95. /// Path to the filesystem directory where file metadata is stored
  96. files_path: PathBuf,
  97. /// Path to the filesystem directory where file chunks are stored
  98. chunks_path: PathBuf,
  99. }
  100. impl Geode {
  101. /// Instantiate a new [`Geode`] object.
  102. /// `base_path` defines the root directory where Geode will store its
  103. /// file metadata and chunks.
  104. pub async fn new(base_path: &PathBuf) -> Result<Self> {
  105. let mut files_path: PathBuf = base_path.into();
  106. let mut chunks_path: PathBuf = base_path.into();
  107. files_path.push(FILES_PATH);
  108. chunks_path.push(CHUNKS_PATH);
  109. // Create necessary directory structure if needed
  110. fs::create_dir_all(&files_path).await?;
  111. fs::create_dir_all(&chunks_path).await?;
  112. Ok(Self { files_path, chunks_path })
  113. }
  114. /// Attempt to read chunk hashes from a given file path and return
  115. /// a `Vec` containing the hashes in order.
  116. async fn read_metadata(path: &PathBuf) -> Result<Vec<blake3::Hash>> {
  117. debug!(target: "geode::read_metadata()", "Reading chunks from {:?}", path);
  118. let fd = File::open(path).await?;
  119. let mut read_chunks = vec![];
  120. let mut lines = BufReader::new(fd).lines();
  121. while let Some(line) = lines.next().await {
  122. let line = line?;
  123. let chunk_hash = blake3::Hash::from_hex(line)?;
  124. read_chunks.push(chunk_hash);
  125. }
  126. Ok(read_chunks)
  127. }
  128. /// Perform garbage collection over the filesystem hierarchy.
  129. /// Returns sets representing deleted files and deleted chunks, respectively.
  130. pub async fn garbage_collect(&self) -> Result<(HashSet<blake3::Hash>, HashSet<blake3::Hash>)> {
  131. info!(target: "geode::garbage_collect()", "[Geode] Performing garbage collection");
  132. // We track corrupt files and chunks here.
  133. let mut deleted_files = HashSet::new();
  134. let mut deleted_chunks = HashSet::new();
  135. let mut deleted_chunk_paths = HashSet::new();
  136. // Scan through available chunks and check them for consistency.
  137. let mut chunk_paths = fs::read_dir(&self.chunks_path).await?;
  138. let mut buf = [0u8; MAX_CHUNK_SIZE];
  139. while let Some(chunk) = chunk_paths.next().await {
  140. let Ok(entry) = chunk else { continue };
  141. let chunk_path = entry.path();
  142. // Skip if we're not a plain file
  143. if !chunk_path.is_file().await {
  144. continue
  145. }
  146. // Make sure that the filename is a BLAKE3 hash
  147. let file_name = match chunk_path.file_name().and_then(|n| n.to_str()) {
  148. Some(v) => v,
  149. None => continue,
  150. };
  151. let chunk_hash = match blake3::Hash::from_hex(file_name) {
  152. Ok(v) => v,
  153. Err(_) => continue,
  154. };
  155. // If there is a problem with opening the file, remove it.
  156. let Ok(mut chunk_fd) = File::open(&chunk_path).await else {
  157. deleted_chunk_paths.insert(chunk_path);
  158. deleted_chunks.insert(chunk_hash);
  159. continue
  160. };
  161. // Perform consistency check
  162. let Ok(bytes_read) = chunk_fd.read(&mut buf).await else {
  163. deleted_chunk_paths.insert(chunk_path);
  164. deleted_chunks.insert(chunk_hash);
  165. buf = [0u8; MAX_CHUNK_SIZE];
  166. continue
  167. };
  168. let chunk_slice = &buf[..bytes_read];
  169. let hashed_chunk = blake3::hash(chunk_slice);
  170. // If the hash doesn't match the filename, remove it.
  171. if chunk_hash != hashed_chunk {
  172. deleted_chunk_paths.insert(chunk_path);
  173. deleted_chunks.insert(chunk_hash);
  174. buf = [0u8; MAX_CHUNK_SIZE];
  175. continue
  176. }
  177. // Seems legit.
  178. buf = [0u8; MAX_CHUNK_SIZE];
  179. }
  180. for chunk_path in &deleted_chunk_paths {
  181. if let Err(e) = fs::remove_file(chunk_path).await {
  182. warn!(
  183. target: "geode::garbage_collect()",
  184. "[Geode] Garbage collect failed to remove corrupted chunk: {}", e,
  185. );
  186. }
  187. }
  188. // Perform health check over file metadata. For now we just ensure they
  189. // have the correct format.
  190. let mut file_paths = fs::read_dir(&self.files_path).await?;
  191. while let Some(file) = file_paths.next().await {
  192. let Ok(entry) = file else { continue };
  193. let path = entry.path();
  194. // Skip if we're not a plain file
  195. if !path.is_file().await {
  196. continue
  197. }
  198. // Make sure that the filename is a BLAKE3 hash
  199. let file_name = match path.file_name().and_then(|n| n.to_str()) {
  200. Some(v) => v,
  201. None => continue,
  202. };
  203. let file_hash = match blake3::Hash::from_hex(file_name) {
  204. Ok(v) => v,
  205. Err(_) => continue,
  206. };
  207. // The filename is a BLAKE3 hash. It should contain a newline-separated
  208. // list of chunks which represent the full file. If that is not the case
  209. // we will consider it a corrupted file and delete it.
  210. if Self::read_metadata(&path).await.is_err() {
  211. if let Err(e) = fs::remove_file(path).await {
  212. warn!(
  213. target: "geode::garbage_collect()",
  214. "[Geode] Garbage collect failed to remove corrupted file: {}", e,
  215. );
  216. }
  217. deleted_files.insert(file_hash);
  218. continue
  219. }
  220. }
  221. info!(target: "geode::garbage_collect()", "[Geode] Garbage collection finished");
  222. Ok((deleted_files, deleted_chunks))
  223. }
  224. /// Insert a file into Geode. The function expects any kind of byte stream, which
  225. /// can either be another file on the filesystem, a buffer, etc.
  226. /// Returns a tuple of `(blake3::Hash, Vec<blake3::Hash>)` which represents the
  227. /// file name, and the file's chunks, respectively.
  228. pub async fn insert(
  229. &self,
  230. mut stream: impl AsyncRead + Unpin,
  231. ) -> Result<(blake3::Hash, Vec<blake3::Hash>)> {
  232. info!(target: "geode::insert()", "[Geode] Inserting file...");
  233. let mut file_hasher = blake3::Hasher::new();
  234. let mut chunk_hashes = vec![];
  235. let mut buf = [0u8; MAX_CHUNK_SIZE];
  236. while let Ok(bytes_read) = stream.read(&mut buf).await {
  237. if bytes_read == 0 {
  238. break
  239. }
  240. let chunk_slice = &buf[..bytes_read];
  241. let chunk_hash = blake3::hash(chunk_slice);
  242. file_hasher.update(chunk_slice);
  243. chunk_hashes.push(chunk_hash);
  244. // Write the chunk to a file, if necessary. We first perform
  245. // a consistency check and if things are fine, we don't have
  246. // to perform a write, which is usually more expensive than
  247. // reading from disk.
  248. let mut chunk_path = self.chunks_path.clone();
  249. chunk_path.push(chunk_hash.to_hex().as_str());
  250. let mut chunk_fd =
  251. OpenOptions::new().read(true).write(true).create(true).open(&chunk_path).await?;
  252. let mut fs_buf = [0u8; MAX_CHUNK_SIZE];
  253. let fs_bytes_read = chunk_fd.read(&mut fs_buf).await?;
  254. let fs_chunk_slice = &fs_buf[..fs_bytes_read];
  255. let fs_chunk_hash = blake3::hash(fs_chunk_slice);
  256. if fs_chunk_hash != chunk_hash {
  257. debug!(
  258. target: "geode::insert()",
  259. "Existing chunk inconsistent or unavailable. Writing chunk to {:?}",
  260. chunk_path,
  261. );
  262. // Here the chunk is broken, so we'll truncate and write the new one.
  263. chunk_fd.set_len(0).await?;
  264. chunk_fd.seek(SeekFrom::Start(0)).await?;
  265. chunk_fd.write_all(chunk_slice).await?;
  266. } else {
  267. debug!(
  268. target: "geode::insert()",
  269. "Existing chunk consistent. Skipping write to {:?}",
  270. chunk_path,
  271. );
  272. }
  273. buf = [0u8; MAX_CHUNK_SIZE];
  274. }
  275. // This hash is the file's chunks hashed in order.
  276. let file_hash = file_hasher.finalize();
  277. let mut file_path = self.files_path.clone();
  278. file_path.push(file_hash.to_hex().as_str());
  279. // We always overwrite the metadata.
  280. let mut file_fd = File::create(&file_path).await?;
  281. for ch in &chunk_hashes {
  282. file_fd.write(format!("{}\n", ch.to_hex().as_str()).as_bytes()).await?;
  283. }
  284. Ok((file_hash, chunk_hashes))
  285. }
  286. /// Create and insert a single chunk into Geode given a stream.
  287. /// Always overwrites any existing chunk. Returns the chunk hash once inserted.
  288. pub async fn insert_chunk(&mut self, stream: impl AsRef<[u8]>) -> Result<blake3::Hash> {
  289. info!(target: "geode::insert_chunk()", "[Geode] Inserting single chunk");
  290. let mut cursor = Cursor::new(&stream);
  291. let mut chunk = [0u8; MAX_CHUNK_SIZE];
  292. let bytes_read = cursor.read(&mut chunk).await?;
  293. let chunk_slice = &chunk[..bytes_read];
  294. let chunk_hash = blake3::hash(chunk_slice);
  295. let mut chunk_path = self.chunks_path.clone();
  296. chunk_path.push(chunk_hash.to_hex().as_str());
  297. let mut chunk_fd = File::create(&chunk_path).await?;
  298. chunk_fd.write_all(chunk_slice).await?;
  299. Ok(chunk_hash)
  300. }
  301. /// Fetch file metadata from Geode. Returns [`ChunkedFile`] which gives a list
  302. /// of chunks and optionally file paths to the said chunks. Returns an error if
  303. /// the read failed in any way (could also be the file does not exist).
  304. pub async fn get(&self, file_hash: &blake3::Hash) -> Result<ChunkedFile> {
  305. info!(target: "geode::get()", "[Geode] Getting file chunks for {}...", file_hash);
  306. let mut file_path = self.files_path.clone();
  307. file_path.push(file_hash.to_hex().as_str());
  308. // Try to read the file metadata. If it's corrupt, return an error signalling
  309. // that garbage collection needs to run.
  310. let chunk_hashes = match Self::read_metadata(&file_path).await {
  311. Ok(v) => v,
  312. Err(e) => match e {
  313. // If the file is not found, return according error.
  314. Error::Io(err) if err == std::io::ErrorKind::NotFound => return Err(e),
  315. // Anything else should tell the client to do garbage collection
  316. _ => return Err(Error::GeodeNeedsGc),
  317. },
  318. };
  319. let mut chunked_file = ChunkedFile::new(&chunk_hashes);
  320. // Iterate over chunks and find which chunks we have available locally.
  321. let mut buf = [0u8; MAX_CHUNK_SIZE];
  322. for (chunk_hash, chunk_path) in chunked_file.0.iter_mut() {
  323. let mut c_path = self.chunks_path.clone();
  324. c_path.push(chunk_hash.to_hex().as_str());
  325. if !c_path.exists().await || !c_path.is_file().await {
  326. // TODO: We should be aggressive here and remove the non-file.
  327. continue
  328. }
  329. // Perform chunk consistency check
  330. let mut chunk_fd = File::open(&c_path).await?;
  331. let bytes_read = chunk_fd.read(&mut buf).await?;
  332. let chunk_slice = &buf[..bytes_read];
  333. let hashed_chunk = blake3::hash(chunk_slice);
  334. if &hashed_chunk != chunk_hash {
  335. // The chunk is corrupted/inconsistent. Garbage collection should run.
  336. buf = [0u8; MAX_CHUNK_SIZE];
  337. continue
  338. }
  339. *chunk_path = Some(c_path);
  340. buf = [0u8; MAX_CHUNK_SIZE];
  341. }
  342. Ok(chunked_file)
  343. }
  344. /// Fetch a single chunk from Geode. Returns a `PathBuf` pointing to the chunk
  345. /// if it is found.
  346. pub async fn get_chunk(&self, chunk_hash: &blake3::Hash) -> Result<PathBuf> {
  347. info!(target: "geode::get_chunk()", "[Geode] Getting chunk {}", chunk_hash);
  348. let mut chunk_path = self.chunks_path.clone();
  349. chunk_path.push(chunk_hash.to_hex().as_str());
  350. if !chunk_path.exists().await || !chunk_path.is_file().await {
  351. // TODO: We should be aggressive here and remove the non-file.
  352. return Err(Error::GeodeChunkNotFound)
  353. }
  354. // Perform chunk consistency check
  355. let mut buf = [0u8; MAX_CHUNK_SIZE];
  356. let mut chunk_fd = File::open(&chunk_path).await?;
  357. let bytes_read = chunk_fd.read(&mut buf).await?;
  358. let chunk_slice = &buf[..bytes_read];
  359. let hashed_chunk = blake3::hash(chunk_slice);
  360. if &hashed_chunk != chunk_hash {
  361. // The chunk is corrupted
  362. return Err(Error::GeodeNeedsGc)
  363. }
  364. Ok(chunk_path)
  365. }
  366. }