From d15338f79f07917c35af381eceebbc48b03de3f7 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Fri, 4 Apr 2025 18:25:06 +0300 Subject: [PATCH 01/60] create FuseFS, implement basic methods Signed-off-by: Gleb Nasretdinov --- Cargo.lock | 80 +++++ Cargo.toml | 2 + src/lib.rs | 1 + src/system/fuse_filesystem.rs | 549 ++++++++++++++++++++++++++++++++++ src/system/mod.rs | 1 + 5 files changed, 633 insertions(+) create mode 100644 src/system/fuse_filesystem.rs diff --git a/Cargo.lock b/Cargo.lock index a02e7695..4e877a1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,6 +156,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.40" @@ -182,7 +188,9 @@ dependencies = [ "criterion", "csv", "fastcdc", + "fuser", "itertools 0.14.0", + "libc", "rand 0.9.0", "serde", "serde_json", @@ -487,6 +495,22 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "fuser" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53274f494609e77794b627b1a3cddfe45d675a6b2e9ba9c0fdc8d8eee2184369" +dependencies = [ + "libc", + "log", + "memchr", + "nix", + "page_size", + "pkg-config", + "smallvec", + "zerocopy", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -702,6 +726,18 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -730,6 +766,22 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "plotters" version = "0.3.7" @@ -1048,6 +1100,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + [[package]] name = "strsim" version = "0.11.1" @@ -1280,6 +1338,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.9" @@ -1289,6 +1363,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 00350d2e..0aaf0a90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,8 @@ csv = { version = "1", optional = true } serde = { version = "1", optional = true, features = ["derive"] } serde_with = { version = "3", optional = true } chrono = { version = "0.4", optional = true, features = ["serde"] } +libc = "0.2.171" +fuser = "0.15.1" [features] chunkers = ["cdc-chunkers", "fastcdc"] diff --git a/src/lib.rs b/src/lib.rs index 8d702bf6..7637a33f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; pub use system::database::{Database, IterableDatabase}; +pub use system::fuse_filesystem::FuseFS; pub use system::scrub::{CopyScrubber, Scrub, ScrubMeasurements}; pub use system::storage::{Data, DataContainer}; pub use system::{create_cdc_filesystem, FileSystem}; diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs new file mode 100644 index 00000000..3ec90085 --- /dev/null +++ b/src/system/fuse_filesystem.rs @@ -0,0 +1,549 @@ +use crate::system::file_layer::FileHandle; +use crate::{ + create_cdc_filesystem, ChunkHash, ChunkerRef, DataContainer, Database, FileSystem, Hasher, +}; +use fuser::FileType::RegularFile; +use fuser::TimeOrNow::Now; +use fuser::{ + FileAttr, FileType, Filesystem, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, + ReplyEntry, ReplyOpen, ReplyWrite, Request, TimeOrNow, +}; +use libc::{ + EACCES, EBADF, EEXIST, EINVAL, EIO, ENOENT, EPERM, ESTALE, O_ACCMODE, O_RDONLY, O_RDWR, + O_WRONLY, R_OK, W_OK, X_OK, +}; +use std::cmp::min; +use std::collections::HashMap; +use std::ffi::OsStr; +use std::time::{Duration, SystemTime}; + +type Inode = u64; +type Fh = u64; + +const FMODE_EXEC: i32 = 0x20; + +#[derive(Clone)] +struct FuseFile { + attr: FileAttr, + name: String, + generation: u64, + handles: u64, +} + +struct FuseFileHandle { + underlying_file_handle: FileHandle, + read: bool, + write: bool, + inode: u64, +} + +pub struct FuseFS +where + B: Database>, + Hash: ChunkHash, +{ + underlying_fs: FileSystem>>, + files: HashMap, + inodes: HashMap, + next_fh: u64, + file_handles: HashMap, + chunker: ChunkerRef, +} + +impl FuseFS +where + B: Database>, + Hash: ChunkHash, +{ + pub fn new(base: B, hasher: H, chunker: C) -> Self + where + H: Into + 'static>>, + C: Into, + { + let underlying_fs = create_cdc_filesystem(base, hasher); + + let uid = unsafe { libc::getuid() }; + let gid = unsafe { libc::getgid() }; + let now = SystemTime::now(); + let root_attr = FileAttr { + ino: 1, + size: 0, + blocks: 0, + atime: now, + mtime: now, + ctime: now, + crtime: now, + kind: FileType::Directory, + perm: 0o755, + nlink: 2, + uid, + gid, + rdev: 0, + flags: 0, + blksize: 512, + }; + let root_dir = FuseFile { + attr: root_attr, + name: ".".to_string(), + generation: 0, + handles: 0, + }; + let mut parent_dir = root_dir.clone(); + parent_dir.name = "..".to_string(); + parent_dir.attr.ino = 0; + let files = HashMap::from([(0, parent_dir), (1, root_dir)]); + + let inodes = HashMap::from([("..".to_string(), 0), (".".to_string(), 1)]); + Self { + underlying_fs, + files, + inodes, + file_handles: HashMap::default(), + next_fh: 0, + chunker: chunker.into(), + } + } + + fn get_new_inode(&self) -> Inode { + self.inodes.len() as Inode + } + + fn get_new_fh(&mut self) -> Fh { + let next_fh = self.next_fh; + self.next_fh += 1; + next_fh + } +} + +fn check_access(file_attr: &FileAttr, req: &Request, access_mask: i32) -> bool { + let file_uid = file_attr.uid; + let file_gid = file_attr.gid; + let file_mode = file_attr.perm; + let uid = req.uid(); + let gid = req.gid(); + + let mut access_mask = access_mask; + // F_OK tests for existence of file + if access_mask == libc::F_OK { + return true; + } + let file_mode = i32::from(file_mode); + + // root is allowed to read & write anything + if uid == 0 { + // root only allowed to exec if one of the Exec bits is set + access_mask &= X_OK; + access_mask -= access_mask & (file_mode >> 6); + access_mask -= access_mask & (file_mode >> 3); + access_mask -= access_mask & file_mode; + return access_mask == 0; + } + + if uid == file_uid { + access_mask -= access_mask & (file_mode >> 6); + } else if gid == file_gid { + access_mask -= access_mask & (file_mode >> 3); + } else { + access_mask -= access_mask & file_mode; + } + + access_mask == 0 +} + +impl Filesystem for FuseFS +where + B: Database>, + Hash: ChunkHash, +{ + fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) { + let name = name.to_str().unwrap().to_owned(); + if parent != 1 { + reply.error(EINVAL); + return; + } + + let Some(inode) = self.inodes.get::(&name) else { + reply.error(ENOENT); + return; + }; + let file = self.files.get(inode).unwrap(); + reply.entry(&Duration::new(0, 0), &file.attr, file.generation) + } + + fn getattr(&mut self, _req: &Request<'_>, ino: u64, _fh: Option, reply: ReplyAttr) { + match self.files.get(&ino) { + Some(file) => reply.attr(&Duration::new(0, 0), &file.attr), + None => reply.error(ENOENT), + } + } + + fn setattr( + &mut self, + req: &Request<'_>, + ino: u64, + mode: Option, + _uid: Option, + _gid: Option, + _size: Option, + atime: Option, + mtime: Option, + _ctime: Option, + _fh: Option, + _crtime: Option, + _chgtime: Option, + _bkuptime: Option, + _flags: Option, + reply: ReplyAttr, + ) { + let Some(file) = self.files.get_mut(&ino) else { + reply.error(ENOENT); + return; + }; + + let now = SystemTime::now(); + let attr = &mut file.attr; + if let Some(mode) = mode { + if req.uid() != 0 && req.uid() != attr.uid { + reply.error(EPERM); + return; + } else { + attr.perm = mode as u16; + } + attr.ctime = now; + reply.attr(&Duration::new(0, 0), &file.attr); + return; + } + + if let Some(atime) = atime { + if attr.uid != req.uid() && req.uid() != 0 && atime != Now { + reply.error(EPERM); + return; + } + + if attr.uid != req.uid() && !check_access(&attr, req, W_OK) { + reply.error(EACCES); + return; + } + + attr.atime = match atime { + TimeOrNow::SpecificTime(time) => time, + Now => now, + }; + attr.ctime = now; + } + + if let Some(mtime) = mtime { + if attr.uid != req.uid() && req.uid() != 0 && mtime != Now { + reply.error(EPERM); + return; + } + + if attr.uid != req.uid() && !check_access(&attr, req, W_OK) { + reply.error(EACCES); + return; + } + + attr.mtime = match mtime { + TimeOrNow::SpecificTime(time) => time, + Now => now, + }; + attr.ctime = now; + } + + reply.attr(&Duration::new(0, 0), &attr); + return; + } + + fn open(&mut self, req: &Request<'_>, ino: u64, flags: i32, reply: ReplyOpen) { + let Some(file) = self.files.get_mut(&ino) else { + reply.error(ENOENT); + return; + }; + + let (access_mask, read, write) = match flags & O_ACCMODE { + O_RDONLY => { + if flags & libc::O_TRUNC != 0 { + reply.error(EACCES); + return; + } + if flags & FMODE_EXEC != 0 { + // Open is from internal exec syscall + (X_OK, true, false) + } else { + (R_OK, true, false) + } + } + O_WRONLY => (W_OK, false, true), + O_RDWR => (R_OK | W_OK, true, true), + // Exactly one access mode flag must be specified + _ => { + reply.error(EINVAL); + return; + } + }; + + if !check_access(&file.attr, req, access_mask) { + reply.error(EACCES); + return; + } + + let underlying_file_handle = if write { + self.underlying_fs + .open_file(&file.name, self.chunker.clone()) + } else { + self.underlying_fs.open_file_readonly(&file.name) + } + .unwrap(); + + let file_handle = FuseFileHandle { + underlying_file_handle, + inode: ino, + read, + write, + }; + file.handles += 1; + let fh = self.get_new_fh(); + self.file_handles.insert(fh, file_handle); + + reply.opened(fh, flags as u32) + } + + fn read( + &mut self, + req: &Request<'_>, + ino: u64, + fh: u64, + offset: i64, + size: u32, + _flags: i32, + _lock_owner: Option, + reply: ReplyData, + ) { + let Some(file_handle) = self.file_handles.get_mut(&fh) else { + reply.error(EBADF); + return; + }; + if file_handle.inode != ino { + reply.error(ESTALE); + return; + } + let Some(file) = self.files.get(&ino) else { + reply.error(ENOENT); + return; + }; + if offset < 0 { + reply.error(EINVAL); + return; + } + + if !check_access(&file.attr, req, R_OK) || !file_handle.read { + reply.error(EACCES); + return; + } + + let file_size = file.attr.size; + let _read_size = min(size, file_size as u32); + // file_handle.underlying_file_handle.offset = offset; + let Ok(_data) = self + .underlying_fs + .read_from_file(&mut file_handle.underlying_file_handle) + else { + reply.error(EIO); + return; + }; + + todo!() + } + + fn write( + &mut self, + req: &Request<'_>, + ino: u64, + fh: u64, + offset: i64, + data: &[u8], + _write_flags: u32, + _flags: i32, + _lock_owner: Option, + reply: ReplyWrite, + ) { + let Some(file_handle) = self.file_handles.get_mut(&fh) else { + reply.error(EBADF); + return; + }; + if file_handle.inode != ino { + reply.error(ESTALE); + return; + } + let Some(file) = self.files.get_mut(&ino) else { + reply.error(ENOENT); + return; + }; + if offset < 0 || offset as u64 != file.attr.size { + reply.error(EINVAL); + return; + } + + if !check_access(&file.attr, req, R_OK) || !file_handle.write { + reply.error(EACCES); + return; + } + + if !self + .underlying_fs + .write_to_file(&mut file_handle.underlying_file_handle, data) + .is_ok() + { + reply.error(EIO); + return; + } + + let now = SystemTime::now(); + file.attr.ctime = now; + file.attr.mtime = now; + file.attr.size += data.len() as u64; + file.generation += 1; + + reply.written(data.len() as u32); + } + + fn release( + &mut self, + _req: &Request<'_>, + ino: u64, + _fh: u64, + _flags: i32, + _lock_owner: Option, + _flush: bool, + reply: ReplyEmpty, + ) { + let Some(file) = self.files.get_mut(&ino) else { + reply.error(EINVAL); + return; + }; + if file.handles <= 0 { + reply.error(EINVAL); + return; + } + let Some(file_handle) = self.file_handles.remove(&ino) else { + reply.error(EINVAL); + return; + }; + file_handle.underlying_file_handle.close(); + file.handles -= 1; + reply.ok() + } + + fn readdir( + &mut self, + req: &Request<'_>, + ino: u64, + _fh: u64, + offset: i64, + mut reply: ReplyDirectory, + ) { + if ino != 1 { + reply.error(EINVAL); + return; + } + let dir = self.files.get(&ino).unwrap(); + if !check_access(&dir.attr, req, R_OK) { + reply.error(EACCES); + return; + } + + let entries = self + .files + .iter() + .map(|(inode, file)| (inode, file.attr.kind, &file.name)); + for (i, entry) in entries.enumerate().skip(offset as usize) { + let (inode, kind, name) = entry; + if reply.add(inode.clone(), offset + i as i64 + 1, kind, name) { + break; + } + } + + reply.ok() + } + + fn create( + &mut self, + req: &Request<'_>, + parent: u64, + name: &OsStr, + mode: u32, + umask: u32, + flags: i32, + reply: ReplyCreate, + ) { + let name = name.to_str().unwrap().to_owned(); + if parent != 1 { + reply.error(EINVAL); + return; + } + let fh = self.get_new_fh(); + let Ok(underlying_file_handle) = self + .underlying_fs + .create_file(name.clone(), (&self.chunker).clone()) + else { + reply.error(EEXIST); + return; + }; + + let ino = self.get_new_inode(); + let now = SystemTime::now(); + let attr = FileAttr { + ino, + size: 0, + blocks: 0, + atime: now, + mtime: now, + ctime: now, + crtime: now, + kind: RegularFile, + perm: (mode & !umask) as u16, + nlink: 1, + uid: req.uid(), + gid: req.gid(), + rdev: 000, + blksize: 000, + flags: flags as u32, + }; + + let (read, write) = match flags & O_ACCMODE { + O_RDONLY => (true, false), + O_WRONLY => (false, true), + O_RDWR => (true, true), + // Exactly one access mode flag must be specified + _ => { + reply.error(EINVAL); + return; + } + }; + + let file_handle = FuseFileHandle { + underlying_file_handle, + inode: ino, + read, + write, + }; + let file = FuseFile { + attr, + name: name.clone(), + generation: 0, + handles: 1, + }; + + reply.created( + &Duration::new(0, 0), + &file.attr, + 0, + fh.clone(), + flags as u32, + ); + + self.files.insert(ino, file); + self.inodes.insert(name, ino); + self.file_handles.insert(fh, file_handle); + } +} diff --git a/src/system/mod.rs b/src/system/mod.rs index c37f09d3..723e106a 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -12,6 +12,7 @@ use super::{ChunkHash, ChunkerRef, Hasher, WriteMeasurements}; pub mod database; pub mod file_layer; +pub mod fuse_filesystem; pub mod scrub; pub mod storage; From 92ab57fcd059ed3af0bbbd24f2c1ae4836a7a0f5 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Fri, 4 Apr 2025 19:35:16 +0300 Subject: [PATCH 02/60] add Send trait bound for Chunker, Hasher, Scrub Signed-off-by: Gleb Nasretdinov --- src/lib.rs | 4 ++-- src/system/scrub.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7637a33f..7ffc0201 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,7 +72,7 @@ impl Chunk { /// Chunks that are found are returned by [`chunk_data`][Chunker::chunk_data] method. /// If some contents were cut because the end of `data` and not the end of the chunk was reached, /// it must be returned with [`rest`][Chunker::rest] instead of storing it in the [`chunk_data`][Chunker::chunk_data]'s output. -pub trait Chunker: Debug { +pub trait Chunker: Debug + Send { /// Goes through whole `data` and finds chunks. If last chunk is not actually a chunk but a leftover, /// it is returned via [`rest`][Chunker::rest] method and is not contained in the vector. /// @@ -126,7 +126,7 @@ impl Debug for ChunkerRef { } /// Functionality for an object that hashes the input. -pub trait Hasher { +pub trait Hasher: Send { /// Hash type that would be returned by the hasher. type Hash: ChunkHash; diff --git a/src/system/scrub.rs b/src/system/scrub.rs index 3957022b..0ee4860e 100644 --- a/src/system/scrub.rs +++ b/src/system/scrub.rs @@ -26,9 +26,9 @@ use super::storage::DataContainer; /// [DataContainer::extract] or [DataContainer::extract_mut] should be used. /// /// 2. A target map, which contains `Key`-`Vec` pairs, where `Key` is a generic value determined by the implementation. -/// The way data is stored is determined by the target map implementation, the only information known to the scrubber is that -/// the target map implements [Database] trait. It should only be used for storage purposes and not contain any algorithm logic. -pub trait Scrub +/// The way data is stored is determined by the target map implementation, the only information known to the scrubber is that +/// the target map implements [Database] trait. It should only be used for storage purposes and not contain any algorithm logic. +pub trait Scrub: Send where Hash: ChunkHash, B: IterableDatabase>, From 70713410ce3204248e77514617432524b6dbb3a6 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 5 Apr 2025 01:40:34 +0300 Subject: [PATCH 03/60] implement read with exact size, FileHandle::set_offset() Signed-off-by: Gleb Nasretdinov --- src/bench/mod.rs | 2 +- src/system/database.rs | 2 +- src/system/file_layer.rs | 76 +++++++++++++++++++++++------------ src/system/fuse_filesystem.rs | 2 +- src/system/mod.rs | 46 +++++++++++++++++---- src/system/storage.rs | 4 +- tests/filesystem.rs | 39 ++++++++++++++++-- 7 files changed, 130 insertions(+), 41 deletions(-) diff --git a/src/bench/mod.rs b/src/bench/mod.rs index d1df843f..fcc96951 100644 --- a/src/bench/mod.rs +++ b/src/bench/mod.rs @@ -257,7 +257,7 @@ where let mut buffer = Vec::with_capacity(MB); loop { - let read = self.fs.read_from_file(&mut fs_file)?; + let read = self.fs.read_1mb_from_file(&mut fs_file)?; if read.is_empty() { break; } diff --git a/src/system/database.rs b/src/system/database.rs index 29957ada..32ecd21d 100644 --- a/src/system/database.rs +++ b/src/system/database.rs @@ -27,7 +27,7 @@ pub trait Database { } /// Retrieves a multitude of values, corresponding to the keys, in the correct order. - fn get_multi(&self, keys: &[K]) -> io::Result> { + fn get_multi(&self, keys: Vec<&K>) -> io::Result> { keys.iter().map(|key| self.get(key)).collect() } diff --git a/src/system/file_layer.rs b/src/system/file_layer.rs index 2fc0462d..91fc27d3 100644 --- a/src/system/file_layer.rs +++ b/src/system/file_layer.rs @@ -1,10 +1,12 @@ +use std::cmp::min; use std::collections::HashMap; +use std::hash::Hash; use std::io; use std::io::ErrorKind; use crate::system::storage::SpansInfo; +use crate::WriteMeasurements; use crate::{ChunkHash, ChunkerRef}; -use crate::{WriteMeasurements, SEG_SIZE}; /// Hashed span, starting at `offset`. #[derive(Debug, PartialEq, Eq, Default, Clone, Hash)] @@ -14,6 +16,20 @@ pub struct FileSpan { len: usize, } +impl FileSpan { + pub fn hash(&self) -> &Hash { + &self.hash + } + + pub fn offset(&self) -> &usize { + &self.offset + } + + pub fn len(&self) -> &usize { + &self.len + } +} + /// A named file, doesn't store actual contents, /// but rather hashes for them. #[derive(Clone)] @@ -68,6 +84,22 @@ impl FileHandle { } } + pub fn offset(&self) -> &usize { + &self.offset + } + + /// Sets the offset for the read-only file handle. + pub fn set_offset(&mut self, offset: usize) -> io::Result<()> { + if self.chunker.is_some() { + return Err(io::Error::new( + ErrorKind::PermissionDenied, + "offset can only be set with read-only file handles", + )); + } + self.offset = offset; + Ok(()) + } + /// Returns name of the file. pub fn name(&self) -> &str { &self.file_name @@ -124,12 +156,9 @@ impl FileLayer { } /// Reads all hashes of the file, from beginning to end. - pub fn read_complete(&self, handle: &FileHandle) -> Vec { + pub fn read_complete(&self, handle: &FileHandle) -> Vec<&Hash> { let file = self.find_file(handle); - file.spans - .iter() - .map(|span| span.hash.clone()) // cloning hashes, takes a lot of time - .collect() + file.spans.iter().map(|span| span.hash()).collect() } /// Writes spans to the end of the file. @@ -147,31 +176,28 @@ impl FileLayer { handle.measurements += info.measurements; } - /// Reads 1 MB of data from the open file and returns received hashes, - /// starting point is based on the `FileHandle`'s offset. - pub fn read(&self, handle: &mut FileHandle) -> Vec { + /// Reads the specified amount of data from the open file and + /// returns FileSpans in which the necessary data is stored (the side FileSpans may contain it partially). + /// Starting point is based on the `FileHandle`'s offset. + /// + /// If `size` + file handle offset is greater than file size, then returns FileSpans up to the end of the file. + pub fn read(&self, handle: &mut FileHandle, size: usize) -> Vec<&FileSpan> { let file = self.find_file(handle); - let mut bytes_read = 0; - let hashes = file + let spans: Vec<_> = file .spans .iter() - .skip_while(|span| span.offset < handle.offset) // find current span in the file - .take_while(|span| { - bytes_read += span.len; - if bytes_read > SEG_SIZE { - bytes_read -= span.len; - false - } else { - true - } - }) // take 1 MB of spans after current one - .map(|span| span.hash.clone()) // take their hashes + .skip_while(|span| span.offset + span.len < handle.offset) // find the first span that contains required data + .take_while(|span| span.offset < handle.offset + size) .collect(); - handle.offset += bytes_read; - - hashes + if spans.is_empty() { + return spans; + } + let last_span = spans.last().unwrap(); + let read_size_possible = last_span.offset + last_span.len - handle.offset; + handle.offset += min(read_size_possible, size); + spans } /// Checks if the file with the given name exists. diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 3ec90085..a9b5653e 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -346,7 +346,7 @@ where // file_handle.underlying_file_handle.offset = offset; let Ok(_data) = self .underlying_fs - .read_from_file(&mut file_handle.underlying_file_handle) + .read_1mb_from_file(&mut file_handle.underlying_file_handle) else { reply.error(EIO); return; diff --git a/src/system/mod.rs b/src/system/mod.rs index 723e106a..2510a489 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -1,3 +1,4 @@ +use std::cmp::min; use std::collections::HashMap; use std::io; use std::io::Write; @@ -8,7 +9,7 @@ use file_layer::{FileHandle, FileLayer}; use scrub::{Scrub, ScrubMeasurements}; use storage::{ChunkStorage, DataContainer}; -use super::{ChunkHash, ChunkerRef, Hasher, WriteMeasurements}; +use super::{ChunkHash, ChunkerRef, Hasher, WriteMeasurements, SEG_SIZE}; pub mod database; pub mod file_layer; @@ -149,15 +150,46 @@ where /// Reads all contents of the file from beginning to end and returns them. pub fn read_file_complete(&self, handle: &FileHandle) -> io::Result> { let hashes = self.file_layer.read_complete(handle); - Ok(self.storage.retrieve(&hashes)?.concat()) // it assumes that all retrieved data segments are in correct order + Ok(self.storage.retrieve(hashes)?.concat()) // it assumes that all retrieved data segments are in correct order } - /// Reads at most 1 MB of data from a file and returns it. + /// Reads 1 MB of data from a file and returns it. + /// If file handle offset + 1 MB is greater than file size, then returns data starting from the offset to the end of the file /// /// **Careful:** it modifies internal `FileHandle` data. After using this `write_to_file` should not be used on the same FileHandle. - pub fn read_from_file(&self, handle: &mut FileHandle) -> io::Result> { - let hashes = self.file_layer.read(handle); - Ok(self.storage.retrieve(&hashes)?.concat()) + pub fn read_1mb_from_file(&self, handle: &mut FileHandle) -> io::Result> { + self.read(handle, SEG_SIZE) + } + + /// Reads the specified amount of data from the open file. + /// Starting point is based on the `FileHandle`'s offset. + /// + /// If `size` + file handle offset is greater than file size, then returns data starting from the offset to the end of the file + /// **Careful:** it modifies internal `FileHandle` data. After using this `write_to_file` should not be used on the same FileHandle. + pub fn read(&self, handle: &mut FileHandle, size: usize) -> io::Result> { + let original_offset = handle.offset().clone(); + let spans = self.file_layer.read(handle, size); + if spans.is_empty() { + return Ok(vec![]); + } + let mut data_vectors = self + .storage + .retrieve(spans.iter().map(|span| span.hash()).collect())?; + + let first_span = spans.first().unwrap(); + let last_span = spans.last().unwrap(); + let data_extra_start = original_offset - first_span.offset(); // amount of data from start to be removed + data_vectors.first_mut().unwrap().drain(0..data_extra_start); + + let read_size_possible = last_span.offset() + last_span.len() - original_offset; + let read_size_actual = min(size, read_size_possible); + let data_extra_end = read_size_possible - read_size_actual; // amount of data from end to be removed + data_vectors + .last_mut() + .unwrap() + .truncate(last_span.len() - data_extra_end); + + Ok(data_vectors.concat()) } /// Gives out a distribution of the chunks with the same hash for the given file. @@ -188,7 +220,7 @@ where .open(path)?; loop { - let data = self.read_from_file(&mut handle)?; + let data = self.read_1mb_from_file(&mut handle)?; if data.is_empty() { break; diff --git a/src/system/storage.rs b/src/system/storage.rs index 1010557a..7f59f270 100644 --- a/src/system/storage.rs +++ b/src/system/storage.rs @@ -138,7 +138,7 @@ where /// Retrieves the data from the storage based on hashes of the data [`segments`][Segment], /// or Error(NotFound) if some of the hashes were not present in the base. - pub fn retrieve(&self, request: &[Hash]) -> io::Result>> { + pub fn retrieve(&self, request: Vec<&Hash>) -> io::Result>> { let retrieved = self.database.get_multi(request)?; retrieved @@ -147,7 +147,7 @@ where Data::Chunk(chunk) => Ok(chunk.clone()), Data::TargetChunk(keys) => Ok(self .target_map - .get_multi(keys)? + .get_multi(keys.iter().collect())? .into_iter() .flatten() .collect()), diff --git a/tests/filesystem.rs b/tests/filesystem.rs index 383dda18..4122bc1b 100644 --- a/tests/filesystem.rs +++ b/tests/filesystem.rs @@ -11,6 +11,37 @@ use chunkfs::hashers::{Sha256Hasher, SimpleHasher}; use chunkfs::{create_cdc_filesystem, ChunkerRef, DataContainer, Database, WriteMeasurements}; const MB: usize = 1024 * 1024; +const KB: usize = 1024; + +#[test] +fn write_read_with_strange_size() { + let mut fs = create_cdc_filesystem(HashMap::default(), SimpleHasher); + + let mut handle = fs.create_file("file", FSChunker::new(4096)).unwrap(); + + let mut data1 = vec![0; 433 * KB]; + let data2 = vec![1; 2 * MB + 3]; + let data3 = vec![2; 2 * MB]; + fs.write_to_file(&mut handle, &data1).unwrap(); + fs.write_to_file(&mut handle, &data2).unwrap(); + fs.write_to_file(&mut handle, &data3).unwrap(); + fs.close_file(handle).unwrap(); + + let mut handle = fs.open_file_readonly("file").unwrap(); + + data1.append(&mut vec![1; MB]); + let actual = fs.read(&mut handle, 433 * KB + MB).unwrap(); + assert_eq!(actual, data1); + + let mut ones_w_twos = vec![1; MB + 3]; + ones_w_twos.append(&mut vec![2; MB]); + let actual = fs.read(&mut handle, 2 * MB + 3).unwrap(); + assert_eq!(actual, ones_w_twos); + + handle.set_offset(433 * KB + 2 * MB + 3 + MB / 2).unwrap(); + let actual = fs.read(&mut handle, 10 * MB).unwrap(); + assert_eq!(actual, vec![2; MB + MB / 2]); +} #[test] fn write_read_complete_test() { @@ -56,12 +87,12 @@ fn write_read_blocks_test() { let mut handle = fs.open_file("file", LeapChunker::default()).unwrap(); let mut buffer = Vec::with_capacity(MB * 3 + 50); for _ in 0..4 { - let buf = fs.read_from_file(&mut handle).unwrap(); + let buf = fs.read_1mb_from_file(&mut handle).unwrap(); buffer.extend_from_slice(&buf); } assert_eq!(buffer.len(), MB * 3 + 50); assert!(complete == buffer); - assert_eq!(fs.read_from_file(&mut handle).unwrap(), []); + assert_eq!(fs.read_1mb_from_file(&mut handle).unwrap(), []); } #[test] @@ -76,7 +107,7 @@ fn read_file_with_size_less_than_1mb() { println!("{:?}", measurements); let mut handle = fs.open_file_readonly("file").unwrap(); - assert_eq!(fs.read_from_file(&mut handle).unwrap(), ones); + assert_eq!(fs.read_1mb_from_file(&mut handle).unwrap(), ones); } #[test] @@ -206,7 +237,7 @@ fn readonly_file_handle_cannot_write_can_read() { assert_eq!(read.len(), MB); assert_eq!(read, [1; MB]); - let _ = fs.read_from_file(&mut ro_fh).unwrap(); + let _ = fs.read_1mb_from_file(&mut ro_fh).unwrap(); // can close let measurements = fs.close_file(ro_fh).unwrap(); From 014c678b1dbca1e443152d731c04a751dc90f2d4 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 5 Apr 2025 02:32:54 +0300 Subject: [PATCH 04/60] finalized write and read in fuse_filesystem Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 23 +++++++++++-------- tests/fuse_filesystem.rs | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 tests/fuse_filesystem.rs diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index a9b5653e..3dd2a39d 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -12,7 +12,6 @@ use libc::{ EACCES, EBADF, EEXIST, EINVAL, EIO, ENOENT, EPERM, ESTALE, O_ACCMODE, O_RDONLY, O_RDWR, O_WRONLY, R_OK, W_OK, X_OK, }; -use std::cmp::min; use std::collections::HashMap; use std::ffi::OsStr; use std::time::{Duration, SystemTime}; @@ -340,19 +339,25 @@ where reply.error(EACCES); return; } + if file_handle + .underlying_file_handle + .set_offset(offset as usize) + .is_err() + { + reply.error(EACCES); + return; + }; - let file_size = file.attr.size; - let _read_size = min(size, file_size as u32); - // file_handle.underlying_file_handle.offset = offset; - let Ok(_data) = self + if let Ok(data) = self .underlying_fs - .read_1mb_from_file(&mut file_handle.underlying_file_handle) - else { + .read(&mut file_handle.underlying_file_handle, size as usize) + { + reply.data(&data); + return; + } else { reply.error(EIO); return; }; - - todo!() } fn write( diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs new file mode 100644 index 00000000..3c066587 --- /dev/null +++ b/tests/fuse_filesystem.rs @@ -0,0 +1,42 @@ +use chunkfs::chunkers::SuperChunker; +use chunkfs::hashers::SimpleHasher; +use chunkfs::FuseFS; +use std::collections::HashMap; +use std::fs; +use std::fs::OpenOptions; +use std::io::{Read, Write}; +use std::os::unix::fs::FileExt; + +const MOUNT_POINT: &str = "./mount_point"; + +#[test] +fn write_fuse_fs() { + let db = HashMap::default(); + let fuse_fs = FuseFS::new(db, SimpleHasher, SuperChunker::default()); + + fs::create_dir_all(MOUNT_POINT).unwrap(); + + let session = fuser::spawn_mount2(fuse_fs, MOUNT_POINT, &vec![]).unwrap(); + + let file_path = format!("{}/{}", MOUNT_POINT, "file"); + let mut file = OpenOptions::new() + .write(true) + .create(true) + .open(&file_path) + .unwrap(); + + let mut data1 = vec![1u8; 2000]; + let mut data2 = vec![2u8; 5000]; + file.write(&data1).unwrap(); + file.write_at(&data2, data1.len() as u64).unwrap(); + + let mut file = OpenOptions::new().read(true).open(&file_path).unwrap(); + data1.append(&mut data2); + let mut actual = Vec::new(); + file.read_to_end(&mut actual).unwrap(); + assert_eq!(actual, data1); + + drop(session); + + fs::remove_dir_all(MOUNT_POINT).unwrap(); +} From 9bf46ff7f5140831e7af0047a4059bd5744a47a4 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 5 Apr 2025 02:54:54 +0300 Subject: [PATCH 05/60] add some docs to fuse_filesystem Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 3dd2a39d..78ad5d86 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -36,6 +36,9 @@ struct FuseFileHandle { inode: u64, } +/// Wrap around [`FileSystem`] for implementing [`fuser::Filesystem`] trait. +/// +/// After creation, it should be passed to [`mount2`][fuser::mount2] or [`spawn_mount2`][fuser::spawn_mount2]. pub struct FuseFS where B: Database>, @@ -44,6 +47,7 @@ where underlying_fs: FileSystem>>, files: HashMap, inodes: HashMap, + /// Number for the next created file handle. next_fh: u64, file_handles: HashMap, chunker: ChunkerRef, @@ -54,6 +58,9 @@ where B: Database>, Hash: ChunkHash, { + /// Creates a file system with the given [`hasher`][Hasher], [`database`][Database] and [`chunker`][ChunkerRef]. + /// + /// After creation, it should be passed to [`mount2`][fuser::mount2] or [`spawn_mount2`][fuser::spawn_mount2]. pub fn new(base: B, hasher: H, chunker: C) -> Self where H: Into + 'static>>, @@ -114,6 +121,7 @@ where } } +/// Checks the request rights for the file with the specified access mask (flags). fn check_access(file_attr: &FileAttr, req: &Request, access_mask: i32) -> bool { let file_uid = file_attr.uid; let file_gid = file_attr.gid; From 5581bdfa7a8130e5a2f0b4ca292efb80f8944f13 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 5 Apr 2025 03:35:34 +0300 Subject: [PATCH 06/60] add fuse fs test Signed-off-by: Gleb Nasretdinov --- tests/fuse_filesystem.rs | 62 +++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 3c066587..6572e32e 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -1,24 +1,28 @@ use chunkfs::chunkers::SuperChunker; use chunkfs::hashers::SimpleHasher; -use chunkfs::FuseFS; +use chunkfs::{FuseFS, MB}; use std::collections::HashMap; use std::fs; use std::fs::OpenOptions; use std::io::{Read, Write}; use std::os::unix::fs::FileExt; +use uuid::Uuid; -const MOUNT_POINT: &str = "./mount_point"; +fn generate_unique_mount_point() -> String { + Uuid::new_v4().to_string() +} #[test] fn write_fuse_fs() { let db = HashMap::default(); let fuse_fs = FuseFS::new(db, SimpleHasher, SuperChunker::default()); + let mount_point = generate_unique_mount_point(); - fs::create_dir_all(MOUNT_POINT).unwrap(); + fs::create_dir_all(&mount_point).unwrap(); - let session = fuser::spawn_mount2(fuse_fs, MOUNT_POINT, &vec![]).unwrap(); + let session = fuser::spawn_mount2(fuse_fs, &mount_point, &vec![]).unwrap(); - let file_path = format!("{}/{}", MOUNT_POINT, "file"); + let file_path = format!("{}/{}", &mount_point, "file"); let mut file = OpenOptions::new() .write(true) .create(true) @@ -27,7 +31,7 @@ fn write_fuse_fs() { let mut data1 = vec![1u8; 2000]; let mut data2 = vec![2u8; 5000]; - file.write(&data1).unwrap(); + file.write_all(&data1).unwrap(); file.write_at(&data2, data1.len() as u64).unwrap(); let mut file = OpenOptions::new().read(true).open(&file_path).unwrap(); @@ -37,6 +41,50 @@ fn write_fuse_fs() { assert_eq!(actual, data1); drop(session); + fs::remove_dir_all(&mount_point).unwrap(); +} + +#[test] +fn different_data_writes() { + let db = HashMap::default(); + let fuse_fs = FuseFS::new(db, SimpleHasher, SuperChunker::default()); + let mount_point = generate_unique_mount_point(); + + fs::create_dir_all(&mount_point).unwrap(); + + let session = fuser::spawn_mount2(fuse_fs, &mount_point, &vec![]).unwrap(); + + let file_path = format!("{}/{}", &mount_point, "file"); + let mut file = OpenOptions::new() + .write(true) + .create(true) + .open(&file_path) + .unwrap(); - fs::remove_dir_all(MOUNT_POINT).unwrap(); + let mut data1 = vec![1u8; 500]; + let mut data2 = vec![2u8; 700]; + let data3 = vec![3u8; 3 * MB]; + let mut data4 = vec![4u8; 10 * MB]; + file.write_all(&data1).unwrap(); + file.write_all(&data2).unwrap(); + file.write_all(&data3).unwrap(); + file.write_all(&data4).unwrap(); + + let mut file = OpenOptions::new().read(true).open(&file_path).unwrap(); + data1.append(&mut data2); + data1.append(&mut vec![3u8; MB + 11]); + let mut actual = vec![0u8; 500 + 700 + MB + 11]; + file.read_exact(&mut actual).unwrap(); + assert_eq!(actual, data1); + let first_read_len = actual.len(); + + let mut expected = vec![3u8; MB - 11]; + expected.append(&mut data4); + let mut actual = vec![0u8; expected.len()]; + file.read_exact_at(&mut actual, (first_read_len + MB) as u64) + .unwrap(); + assert_eq!(actual, expected); + + drop(session); + fs::remove_dir_all(&mount_point).unwrap(); } From 0397aff568395458f230b385f089fcfcb84c457a Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 5 Apr 2025 15:59:12 +0300 Subject: [PATCH 07/60] change FileHandle::offset return type from &usize to usize Signed-off-by: Gleb Nasretdinov --- src/system/file_layer.rs | 4 ++-- src/system/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/system/file_layer.rs b/src/system/file_layer.rs index 91fc27d3..e445e7ee 100644 --- a/src/system/file_layer.rs +++ b/src/system/file_layer.rs @@ -84,8 +84,8 @@ impl FileHandle { } } - pub fn offset(&self) -> &usize { - &self.offset + pub fn offset(&self) -> usize { + self.offset.clone() } /// Sets the offset for the read-only file handle. diff --git a/src/system/mod.rs b/src/system/mod.rs index 2510a489..709defb6 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -167,7 +167,7 @@ where /// If `size` + file handle offset is greater than file size, then returns data starting from the offset to the end of the file /// **Careful:** it modifies internal `FileHandle` data. After using this `write_to_file` should not be used on the same FileHandle. pub fn read(&self, handle: &mut FileHandle, size: usize) -> io::Result> { - let original_offset = handle.offset().clone(); + let original_offset = handle.offset(); let spans = self.file_layer.read(handle, size); if spans.is_empty() { return Ok(vec![]); From 9b1d09470c464472feee1c7b8c14b9dd4bea8daa Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 5 Apr 2025 17:54:40 +0300 Subject: [PATCH 08/60] add ability to change offset of any file handle Signed-off-by: Gleb Nasretdinov --- src/system/file_layer.rs | 11 ++--------- src/system/fuse_filesystem.rs | 15 +++------------ tests/filesystem.rs | 2 +- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/system/file_layer.rs b/src/system/file_layer.rs index e445e7ee..89d4823a 100644 --- a/src/system/file_layer.rs +++ b/src/system/file_layer.rs @@ -88,16 +88,9 @@ impl FileHandle { self.offset.clone() } - /// Sets the offset for the read-only file handle. - pub fn set_offset(&mut self, offset: usize) -> io::Result<()> { - if self.chunker.is_some() { - return Err(io::Error::new( - ErrorKind::PermissionDenied, - "offset can only be set with read-only file handles", - )); - } + /// Sets the offset for the file handle. + pub fn set_offset(&mut self, offset: usize) { self.offset = offset; - Ok(()) } /// Returns name of the file. diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 78ad5d86..22c4017f 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -347,19 +347,10 @@ where reply.error(EACCES); return; } - if file_handle - .underlying_file_handle - .set_offset(offset as usize) - .is_err() - { - reply.error(EACCES); - return; - }; + let underlying_fh = &mut file_handle.underlying_file_handle; + underlying_fh.set_offset(offset as usize); - if let Ok(data) = self - .underlying_fs - .read(&mut file_handle.underlying_file_handle, size as usize) - { + if let Ok(data) = self.underlying_fs.read(underlying_fh, size as usize) { reply.data(&data); return; } else { diff --git a/tests/filesystem.rs b/tests/filesystem.rs index 4122bc1b..4728d4b0 100644 --- a/tests/filesystem.rs +++ b/tests/filesystem.rs @@ -38,7 +38,7 @@ fn write_read_with_strange_size() { let actual = fs.read(&mut handle, 2 * MB + 3).unwrap(); assert_eq!(actual, ones_w_twos); - handle.set_offset(433 * KB + 2 * MB + 3 + MB / 2).unwrap(); + handle.set_offset(433 * KB + 2 * MB + 3 + MB / 2); let actual = fs.read(&mut handle, 10 * MB).unwrap(); assert_eq!(actual, vec![2; MB + MB / 2]); } From bac565aa17f76ec72e4e5ff65f5f3f6641ab2ac7 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 5 Apr 2025 17:57:41 +0300 Subject: [PATCH 09/60] add example of FS mounting Signed-off-by: Gleb Nasretdinov --- examples/mount.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 examples/mount.rs diff --git a/examples/mount.rs b/examples/mount.rs new file mode 100644 index 00000000..07e68fa0 --- /dev/null +++ b/examples/mount.rs @@ -0,0 +1,46 @@ +use chunkfs::chunkers::SuperChunker; +use chunkfs::hashers::SimpleHasher; +use chunkfs::{FuseFS, MB}; +use std::collections::HashMap; +use std::fs; +use std::fs::OpenOptions; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::os::unix::fs::FileExt; + +const MOUNT_POINT: &str = "./mount_point"; + +fn main() { + let db = HashMap::default(); + let fuse_fs = FuseFS::new(db, SimpleHasher, SuperChunker::default()); + + fs::create_dir_all(MOUNT_POINT).unwrap(); + + let session = fuser::spawn_mount2(fuse_fs, MOUNT_POINT, &vec![]).unwrap(); + + let file_path = format!("{}/{}", MOUNT_POINT, "file"); + // careful: writing is sequential only + let mut file = OpenOptions::new() + .write(true) + .read(true) + .create(true) + .open(&file_path) + .unwrap(); + + let data1 = vec![1u8; 2 * MB]; + let data2 = vec![2u8; 5 * MB]; + file.write_all(&data1).unwrap(); + file.write_all(&data2).unwrap(); + + let expected: Vec = vec![1, 1, 1, 1, 2, 2, 2]; + let mut actual = vec![0u8; expected.len()]; + file.read_exact_at(&mut actual, 2 * MB as u64 - 4).unwrap(); + assert_eq!(expected, actual); + + file.seek(SeekFrom::Start(0)).unwrap(); + let mut file_data = vec![0u8; 7 * MB]; + let read_size = file.read_to_end(&mut file_data).unwrap(); + assert_eq!(read_size, 7 * MB); + + drop(session); + fs::remove_dir_all(MOUNT_POINT).unwrap(); +} From 5b854acb65fba0b0ab770a4866372851b7101697 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 5 Apr 2025 20:02:56 +0300 Subject: [PATCH 10/60] change FileSpan::{offset, len} return type from &usize to usize Signed-off-by: Gleb Nasretdinov --- src/system/file_layer.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/system/file_layer.rs b/src/system/file_layer.rs index 89d4823a..2777d1a6 100644 --- a/src/system/file_layer.rs +++ b/src/system/file_layer.rs @@ -21,12 +21,12 @@ impl FileSpan { &self.hash } - pub fn offset(&self) -> &usize { - &self.offset + pub fn offset(&self) -> usize { + self.offset.clone() } - pub fn len(&self) -> &usize { - &self.len + pub fn len(&self) -> usize { + self.len.clone() } } From 5b735238850c0411a8ce9652ee62aee177c54470 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 5 Apr 2025 20:22:11 +0300 Subject: [PATCH 11/60] add more comments in FileSystem::read method Signed-off-by: Gleb Nasretdinov --- src/system/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/system/mod.rs b/src/system/mod.rs index 709defb6..f79340d5 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -176,11 +176,14 @@ where .storage .retrieve(spans.iter().map(|span| span.hash()).collect())?; + // Since we read by offset, which may be somewhere in the middle of the FileSpan offset, + // we need to remove the extra at the beginning of the first span let first_span = spans.first().unwrap(); let last_span = spans.last().unwrap(); let data_extra_start = original_offset - first_span.offset(); // amount of data from start to be removed data_vectors.first_mut().unwrap().drain(0..data_extra_start); + // Same, we need to remove the extra at the end of the last span. let read_size_possible = last_span.offset() + last_span.len() - original_offset; let read_size_actual = min(size, read_size_possible); let data_extra_end = read_size_possible - read_size_actual; // amount of data from end to be removed From b1e54caf9f77fafe0b6aeda60bf65a496010fff7 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 5 Apr 2025 21:28:46 +0300 Subject: [PATCH 12/60] change keys type from Vec<&K> to &[&K] in Database::get_multi Signed-off-by: Gleb Nasretdinov --- src/system/database.rs | 2 +- src/system/mod.rs | 7 +++---- src/system/storage.rs | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/system/database.rs b/src/system/database.rs index 32ecd21d..c77a6a24 100644 --- a/src/system/database.rs +++ b/src/system/database.rs @@ -27,7 +27,7 @@ pub trait Database { } /// Retrieves a multitude of values, corresponding to the keys, in the correct order. - fn get_multi(&self, keys: Vec<&K>) -> io::Result> { + fn get_multi(&self, keys: &[&K]) -> io::Result> { keys.iter().map(|key| self.get(key)).collect() } diff --git a/src/system/mod.rs b/src/system/mod.rs index f79340d5..e027f516 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -150,7 +150,7 @@ where /// Reads all contents of the file from beginning to end and returns them. pub fn read_file_complete(&self, handle: &FileHandle) -> io::Result> { let hashes = self.file_layer.read_complete(handle); - Ok(self.storage.retrieve(hashes)?.concat()) // it assumes that all retrieved data segments are in correct order + Ok(self.storage.retrieve(&hashes)?.concat()) // it assumes that all retrieved data segments are in correct order } /// Reads 1 MB of data from a file and returns it. @@ -172,9 +172,8 @@ where if spans.is_empty() { return Ok(vec![]); } - let mut data_vectors = self - .storage - .retrieve(spans.iter().map(|span| span.hash()).collect())?; + let hashes: Vec<_> = spans.iter().map(|span| span.hash()).collect(); + let mut data_vectors = self.storage.retrieve(&hashes)?; // Since we read by offset, which may be somewhere in the middle of the FileSpan offset, // we need to remove the extra at the beginning of the first span diff --git a/src/system/storage.rs b/src/system/storage.rs index 7f59f270..1faa98a7 100644 --- a/src/system/storage.rs +++ b/src/system/storage.rs @@ -138,7 +138,7 @@ where /// Retrieves the data from the storage based on hashes of the data [`segments`][Segment], /// or Error(NotFound) if some of the hashes were not present in the base. - pub fn retrieve(&self, request: Vec<&Hash>) -> io::Result>> { + pub fn retrieve(&self, request: &[&Hash]) -> io::Result>> { let retrieved = self.database.get_multi(request)?; retrieved @@ -147,7 +147,7 @@ where Data::Chunk(chunk) => Ok(chunk.clone()), Data::TargetChunk(keys) => Ok(self .target_map - .get_multi(keys.iter().collect())? + .get_multi(&keys.into_iter().collect::>())? .into_iter() .flatten() .collect()), From e5e068d612e9dd8065108761f25265e60edf4873 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 6 Apr 2025 15:53:50 +0300 Subject: [PATCH 13/60] refactor: libc flags import, remove usize.clone() Signed-off-by: Gleb Nasretdinov --- src/system/file_layer.rs | 6 +- src/system/fuse_filesystem.rs | 105 +++++++++++++++++----------------- 2 files changed, 54 insertions(+), 57 deletions(-) diff --git a/src/system/file_layer.rs b/src/system/file_layer.rs index 2777d1a6..25c7e484 100644 --- a/src/system/file_layer.rs +++ b/src/system/file_layer.rs @@ -22,11 +22,11 @@ impl FileSpan { } pub fn offset(&self) -> usize { - self.offset.clone() + self.offset } pub fn len(&self) -> usize { - self.len.clone() + self.len } } @@ -85,7 +85,7 @@ impl FileHandle { } pub fn offset(&self) -> usize { - self.offset.clone() + self.offset } /// Sets the offset for the file handle. diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 22c4017f..a35f472c 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -8,10 +8,6 @@ use fuser::{ FileAttr, FileType, Filesystem, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyOpen, ReplyWrite, Request, TimeOrNow, }; -use libc::{ - EACCES, EBADF, EEXIST, EINVAL, EIO, ENOENT, EPERM, ESTALE, O_ACCMODE, O_RDONLY, O_RDWR, - O_WRONLY, R_OK, W_OK, X_OK, -}; use std::collections::HashMap; use std::ffi::OsStr; use std::time::{Duration, SystemTime}; @@ -19,6 +15,7 @@ use std::time::{Duration, SystemTime}; type Inode = u64; type Fh = u64; +/// File is opened for execution. const FMODE_EXEC: i32 = 0x20; #[derive(Clone)] @@ -36,7 +33,7 @@ struct FuseFileHandle { inode: u64, } -/// Wrap around [`FileSystem`] for implementing [`fuser::Filesystem`] trait. +/// Wrap around [`FileSystem`] for implementing [`Filesystem`] trait. /// /// After creation, it should be passed to [`mount2`][fuser::mount2] or [`spawn_mount2`][fuser::spawn_mount2]. pub struct FuseFS @@ -139,7 +136,7 @@ fn check_access(file_attr: &FileAttr, req: &Request, access_mask: i32) -> bool { // root is allowed to read & write anything if uid == 0 { // root only allowed to exec if one of the Exec bits is set - access_mask &= X_OK; + access_mask &= libc::X_OK; access_mask -= access_mask & (file_mode >> 6); access_mask -= access_mask & (file_mode >> 3); access_mask -= access_mask & file_mode; @@ -165,12 +162,12 @@ where fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) { let name = name.to_str().unwrap().to_owned(); if parent != 1 { - reply.error(EINVAL); + reply.error(libc::EINVAL); return; } let Some(inode) = self.inodes.get::(&name) else { - reply.error(ENOENT); + reply.error(libc::ENOENT); return; }; let file = self.files.get(inode).unwrap(); @@ -180,7 +177,7 @@ where fn getattr(&mut self, _req: &Request<'_>, ino: u64, _fh: Option, reply: ReplyAttr) { match self.files.get(&ino) { Some(file) => reply.attr(&Duration::new(0, 0), &file.attr), - None => reply.error(ENOENT), + None => reply.error(libc::ENOENT), } } @@ -203,7 +200,7 @@ where reply: ReplyAttr, ) { let Some(file) = self.files.get_mut(&ino) else { - reply.error(ENOENT); + reply.error(libc::ENOENT); return; }; @@ -211,7 +208,7 @@ where let attr = &mut file.attr; if let Some(mode) = mode { if req.uid() != 0 && req.uid() != attr.uid { - reply.error(EPERM); + reply.error(libc::EPERM); return; } else { attr.perm = mode as u16; @@ -223,12 +220,12 @@ where if let Some(atime) = atime { if attr.uid != req.uid() && req.uid() != 0 && atime != Now { - reply.error(EPERM); + reply.error(libc::EPERM); return; } - if attr.uid != req.uid() && !check_access(&attr, req, W_OK) { - reply.error(EACCES); + if attr.uid != req.uid() && !check_access(&attr, req, libc::W_OK) { + reply.error(libc::EACCES); return; } @@ -241,12 +238,12 @@ where if let Some(mtime) = mtime { if attr.uid != req.uid() && req.uid() != 0 && mtime != Now { - reply.error(EPERM); + reply.error(libc::EPERM); return; } - if attr.uid != req.uid() && !check_access(&attr, req, W_OK) { - reply.error(EACCES); + if attr.uid != req.uid() && !check_access(&attr, req, libc::W_OK) { + reply.error(libc::EACCES); return; } @@ -263,34 +260,34 @@ where fn open(&mut self, req: &Request<'_>, ino: u64, flags: i32, reply: ReplyOpen) { let Some(file) = self.files.get_mut(&ino) else { - reply.error(ENOENT); + reply.error(libc::ENOENT); return; }; - let (access_mask, read, write) = match flags & O_ACCMODE { - O_RDONLY => { + let (access_mask, read, write) = match flags & libc::O_ACCMODE { + libc::O_RDONLY => { if flags & libc::O_TRUNC != 0 { - reply.error(EACCES); + reply.error(libc::EACCES); return; } if flags & FMODE_EXEC != 0 { // Open is from internal exec syscall - (X_OK, true, false) + (libc::X_OK, true, false) } else { - (R_OK, true, false) + (libc::R_OK, true, false) } } - O_WRONLY => (W_OK, false, true), - O_RDWR => (R_OK | W_OK, true, true), + libc::O_WRONLY => (libc::W_OK, false, true), + libc::O_RDWR => (libc::R_OK | libc::W_OK, true, true), // Exactly one access mode flag must be specified _ => { - reply.error(EINVAL); + reply.error(libc::EINVAL); return; } }; if !check_access(&file.attr, req, access_mask) { - reply.error(EACCES); + reply.error(libc::EACCES); return; } @@ -327,24 +324,24 @@ where reply: ReplyData, ) { let Some(file_handle) = self.file_handles.get_mut(&fh) else { - reply.error(EBADF); + reply.error(libc::EBADF); return; }; if file_handle.inode != ino { - reply.error(ESTALE); + reply.error(libc::ESTALE); return; } let Some(file) = self.files.get(&ino) else { - reply.error(ENOENT); + reply.error(libc::ENOENT); return; }; if offset < 0 { - reply.error(EINVAL); + reply.error(libc::EINVAL); return; } - if !check_access(&file.attr, req, R_OK) || !file_handle.read { - reply.error(EACCES); + if !check_access(&file.attr, req, libc::R_OK) || !file_handle.read { + reply.error(libc::EACCES); return; } let underlying_fh = &mut file_handle.underlying_file_handle; @@ -354,7 +351,7 @@ where reply.data(&data); return; } else { - reply.error(EIO); + reply.error(libc::EIO); return; }; } @@ -372,24 +369,24 @@ where reply: ReplyWrite, ) { let Some(file_handle) = self.file_handles.get_mut(&fh) else { - reply.error(EBADF); + reply.error(libc::EBADF); return; }; if file_handle.inode != ino { - reply.error(ESTALE); + reply.error(libc::ESTALE); return; } let Some(file) = self.files.get_mut(&ino) else { - reply.error(ENOENT); + reply.error(libc::ENOENT); return; }; if offset < 0 || offset as u64 != file.attr.size { - reply.error(EINVAL); + reply.error(libc::EINVAL); return; } - if !check_access(&file.attr, req, R_OK) || !file_handle.write { - reply.error(EACCES); + if !check_access(&file.attr, req, libc::R_OK) || !file_handle.write { + reply.error(libc::EACCES); return; } @@ -398,7 +395,7 @@ where .write_to_file(&mut file_handle.underlying_file_handle, data) .is_ok() { - reply.error(EIO); + reply.error(libc::EIO); return; } @@ -422,15 +419,15 @@ where reply: ReplyEmpty, ) { let Some(file) = self.files.get_mut(&ino) else { - reply.error(EINVAL); + reply.error(libc::EINVAL); return; }; if file.handles <= 0 { - reply.error(EINVAL); + reply.error(libc::EINVAL); return; } let Some(file_handle) = self.file_handles.remove(&ino) else { - reply.error(EINVAL); + reply.error(libc::EINVAL); return; }; file_handle.underlying_file_handle.close(); @@ -447,12 +444,12 @@ where mut reply: ReplyDirectory, ) { if ino != 1 { - reply.error(EINVAL); + reply.error(libc::EINVAL); return; } let dir = self.files.get(&ino).unwrap(); - if !check_access(&dir.attr, req, R_OK) { - reply.error(EACCES); + if !check_access(&dir.attr, req, libc::R_OK) { + reply.error(libc::EACCES); return; } @@ -482,7 +479,7 @@ where ) { let name = name.to_str().unwrap().to_owned(); if parent != 1 { - reply.error(EINVAL); + reply.error(libc::EINVAL); return; } let fh = self.get_new_fh(); @@ -490,7 +487,7 @@ where .underlying_fs .create_file(name.clone(), (&self.chunker).clone()) else { - reply.error(EEXIST); + reply.error(libc::EEXIST); return; }; @@ -514,13 +511,13 @@ where flags: flags as u32, }; - let (read, write) = match flags & O_ACCMODE { - O_RDONLY => (true, false), - O_WRONLY => (false, true), - O_RDWR => (true, true), + let (read, write) = match flags & libc::O_ACCMODE { + libc::O_RDONLY => (true, false), + libc::O_WRONLY => (false, true), + libc::O_RDWR => (true, true), // Exactly one access mode flag must be specified _ => { - reply.error(EINVAL); + reply.error(libc::EINVAL); return; } }; From 117bf92246f9921a2a467e085e1133e87344b306 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 19 Apr 2025 20:49:57 +0300 Subject: [PATCH 14/60] some refactor Signed-off-by: Gleb Nasretdinov --- examples/mount.rs | 12 ++++++------ src/bench/mod.rs | 2 +- src/system/file_layer.rs | 1 + src/system/mod.rs | 6 +++--- tests/filesystem.rs | 8 ++++---- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/examples/mount.rs b/examples/mount.rs index 07e68fa0..23d25c59 100644 --- a/examples/mount.rs +++ b/examples/mount.rs @@ -6,18 +6,18 @@ use std::fs; use std::fs::OpenOptions; use std::io::{Read, Seek, SeekFrom, Write}; use std::os::unix::fs::FileExt; - -const MOUNT_POINT: &str = "./mount_point"; +use std::path::Path; fn main() { + let mount_point = Path::new("./mount_point"); let db = HashMap::default(); let fuse_fs = FuseFS::new(db, SimpleHasher, SuperChunker::default()); - fs::create_dir_all(MOUNT_POINT).unwrap(); + fs::create_dir_all(mount_point).unwrap(); - let session = fuser::spawn_mount2(fuse_fs, MOUNT_POINT, &vec![]).unwrap(); + let session = fuser::spawn_mount2(fuse_fs, mount_point, &vec![]).unwrap(); - let file_path = format!("{}/{}", MOUNT_POINT, "file"); + let file_path = mount_point.join("file"); // careful: writing is sequential only let mut file = OpenOptions::new() .write(true) @@ -42,5 +42,5 @@ fn main() { assert_eq!(read_size, 7 * MB); drop(session); - fs::remove_dir_all(MOUNT_POINT).unwrap(); + fs::remove_dir_all(mount_point).unwrap(); } diff --git a/src/bench/mod.rs b/src/bench/mod.rs index fcc96951..d1df843f 100644 --- a/src/bench/mod.rs +++ b/src/bench/mod.rs @@ -257,7 +257,7 @@ where let mut buffer = Vec::with_capacity(MB); loop { - let read = self.fs.read_1mb_from_file(&mut fs_file)?; + let read = self.fs.read_from_file(&mut fs_file)?; if read.is_empty() { break; } diff --git a/src/system/file_layer.rs b/src/system/file_layer.rs index 25c7e484..9a2ba98f 100644 --- a/src/system/file_layer.rs +++ b/src/system/file_layer.rs @@ -187,6 +187,7 @@ impl FileLayer { if spans.is_empty() { return spans; } + let last_span = spans.last().unwrap(); let read_size_possible = last_span.offset + last_span.len - handle.offset; handle.offset += min(read_size_possible, size); diff --git a/src/system/mod.rs b/src/system/mod.rs index e027f516..cf6dbd3d 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -157,7 +157,7 @@ where /// If file handle offset + 1 MB is greater than file size, then returns data starting from the offset to the end of the file /// /// **Careful:** it modifies internal `FileHandle` data. After using this `write_to_file` should not be used on the same FileHandle. - pub fn read_1mb_from_file(&self, handle: &mut FileHandle) -> io::Result> { + pub fn read_from_file(&self, handle: &mut FileHandle) -> io::Result> { self.read(handle, SEG_SIZE) } @@ -178,11 +178,11 @@ where // Since we read by offset, which may be somewhere in the middle of the FileSpan offset, // we need to remove the extra at the beginning of the first span let first_span = spans.first().unwrap(); - let last_span = spans.last().unwrap(); let data_extra_start = original_offset - first_span.offset(); // amount of data from start to be removed data_vectors.first_mut().unwrap().drain(0..data_extra_start); // Same, we need to remove the extra at the end of the last span. + let last_span = spans.last().unwrap(); let read_size_possible = last_span.offset() + last_span.len() - original_offset; let read_size_actual = min(size, read_size_possible); let data_extra_end = read_size_possible - read_size_actual; // amount of data from end to be removed @@ -222,7 +222,7 @@ where .open(path)?; loop { - let data = self.read_1mb_from_file(&mut handle)?; + let data = self.read_from_file(&mut handle)?; if data.is_empty() { break; diff --git a/tests/filesystem.rs b/tests/filesystem.rs index 4728d4b0..3a659984 100644 --- a/tests/filesystem.rs +++ b/tests/filesystem.rs @@ -87,12 +87,12 @@ fn write_read_blocks_test() { let mut handle = fs.open_file("file", LeapChunker::default()).unwrap(); let mut buffer = Vec::with_capacity(MB * 3 + 50); for _ in 0..4 { - let buf = fs.read_1mb_from_file(&mut handle).unwrap(); + let buf = fs.read_from_file(&mut handle).unwrap(); buffer.extend_from_slice(&buf); } assert_eq!(buffer.len(), MB * 3 + 50); assert!(complete == buffer); - assert_eq!(fs.read_1mb_from_file(&mut handle).unwrap(), []); + assert_eq!(fs.read_from_file(&mut handle).unwrap(), []); } #[test] @@ -107,7 +107,7 @@ fn read_file_with_size_less_than_1mb() { println!("{:?}", measurements); let mut handle = fs.open_file_readonly("file").unwrap(); - assert_eq!(fs.read_1mb_from_file(&mut handle).unwrap(), ones); + assert_eq!(fs.read_from_file(&mut handle).unwrap(), ones); } #[test] @@ -237,7 +237,7 @@ fn readonly_file_handle_cannot_write_can_read() { assert_eq!(read.len(), MB); assert_eq!(read, [1; MB]); - let _ = fs.read_1mb_from_file(&mut ro_fh).unwrap(); + let _ = fs.read_from_file(&mut ro_fh).unwrap(); // can close let measurements = fs.close_file(ro_fh).unwrap(); From 5f22426aff99f76b7f9b4333b2b80c37ad813d3d Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 20 Apr 2025 15:42:26 +0300 Subject: [PATCH 15/60] fix FuseFS::write() - was checking permission for read instead of write Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index a35f472c..0b53a177 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -385,7 +385,7 @@ where return; } - if !check_access(&file.attr, req, libc::R_OK) || !file_handle.write { + if !check_access(&file.attr, req, libc::W_OK) || !file_handle.write { reply.error(libc::EACCES); return; } From bf74f041fdf8e35f2cd9b5b352c946d21c325d54 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 20 Apr 2025 15:42:46 +0300 Subject: [PATCH 16/60] add permissions tests for FuseFS Signed-off-by: Gleb Nasretdinov --- tests/fuse_filesystem.rs | 112 +++++++++++++++++++++++++++++++-------- 1 file changed, 89 insertions(+), 23 deletions(-) diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 6572e32e..88af13ec 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -1,28 +1,105 @@ use chunkfs::chunkers::SuperChunker; use chunkfs::hashers::SimpleHasher; use chunkfs::{FuseFS, MB}; +use fuser::BackgroundSession; use std::collections::HashMap; use std::fs; -use std::fs::OpenOptions; +use std::fs::{File, OpenOptions, Permissions}; use std::io::{Read, Write}; -use std::os::unix::fs::FileExt; +use std::os::unix::fs::{FileExt, PermissionsExt}; +use std::path::Path; use uuid::Uuid; fn generate_unique_mount_point() -> String { Uuid::new_v4().to_string() } +struct FuseFixture { + mount_point: String, + fuse_session: Option, +} + +impl FuseFixture { + fn default() -> Self { + let mount_point = generate_unique_mount_point(); + let db = HashMap::default(); + let fuse_fs = FuseFS::new(db, SimpleHasher, SuperChunker::default()); + fs::create_dir_all(&mount_point).unwrap(); + + let fuse_session = fuser::spawn_mount2(fuse_fs, &mount_point, &vec![]).unwrap(); + + Self { + mount_point, + fuse_session: Some(fuse_session), + } + } +} + +impl Drop for FuseFixture { + fn drop(&mut self) { + if let Some(session) = self.fuse_session.take() { + drop(session) + } + fs::remove_dir(&self.mount_point).unwrap(); + } +} + #[test] -fn write_fuse_fs() { - let db = HashMap::default(); - let fuse_fs = FuseFS::new(db, SimpleHasher, SuperChunker::default()); - let mount_point = generate_unique_mount_point(); +fn permissions() { + let fuse_fixture = FuseFixture::default(); + let mount_point = Path::new(&fuse_fixture.mount_point); - fs::create_dir_all(&mount_point).unwrap(); + let file_path = mount_point.join("file"); + OpenOptions::new() + .write(true) + .create(true) + .open(&file_path) + .unwrap(); - let session = fuser::spawn_mount2(fuse_fs, &mount_point, &vec![]).unwrap(); + let perms: Vec<_> = (0o000..=0o777).map(|m| Permissions::from_mode(m)).collect(); + + let file_size = |file: &File| file.metadata().unwrap().len(); + let read_ok = || { + let mut file = OpenOptions::new().read(true).open(&file_path).unwrap(); + let mut buf = vec![]; + file.read_to_end(&mut buf).unwrap(); + assert_eq!(file_size(&file), buf.len() as u64); + }; + let read_denied = || { + let res = OpenOptions::new().read(true).open(&file_path); + assert!(res.is_err()); + }; + let write_ok = || { + let file = OpenOptions::new().write(true).open(&file_path).unwrap(); + let write_len = file.write_at(&mut vec![0; 512], file_size(&file)).unwrap(); + assert_eq!(write_len, 512); + }; + let write_denied = || { + let res = OpenOptions::new().write(true).open(&file_path); + assert!(res.is_err()); + }; + for perm in perms { + fs::set_permissions(&file_path, perm.clone()).unwrap(); + if perm.mode() & 0o400 != 0 { + read_ok(); + } else { + read_denied(); + } + + if perm.mode() & 0o200 != 0 { + write_ok(); + } else { + write_denied(); + } + } +} + +#[test] +fn write_fuse_fs() { + let fuse_fixture = FuseFixture::default(); + let mount_point = Path::new(&fuse_fixture.mount_point); - let file_path = format!("{}/{}", &mount_point, "file"); + let file_path = mount_point.join("file"); let mut file = OpenOptions::new() .write(true) .create(true) @@ -39,22 +116,14 @@ fn write_fuse_fs() { let mut actual = Vec::new(); file.read_to_end(&mut actual).unwrap(); assert_eq!(actual, data1); - - drop(session); - fs::remove_dir_all(&mount_point).unwrap(); } #[test] fn different_data_writes() { - let db = HashMap::default(); - let fuse_fs = FuseFS::new(db, SimpleHasher, SuperChunker::default()); - let mount_point = generate_unique_mount_point(); - - fs::create_dir_all(&mount_point).unwrap(); + let fuse_fixture = FuseFixture::default(); + let mount_point = Path::new(&fuse_fixture.mount_point); - let session = fuser::spawn_mount2(fuse_fs, &mount_point, &vec![]).unwrap(); - - let file_path = format!("{}/{}", &mount_point, "file"); + let file_path = mount_point.join("file"); let mut file = OpenOptions::new() .write(true) .create(true) @@ -84,7 +153,4 @@ fn different_data_writes() { file.read_exact_at(&mut actual, (first_read_len + MB) as u64) .unwrap(); assert_eq!(actual, expected); - - drop(session); - fs::remove_dir_all(&mount_point).unwrap(); } From 7e77eda46f185696390a2940e39c5e5dde91efa3 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 20 Apr 2025 19:15:39 +0300 Subject: [PATCH 17/60] remove metadata change possibility with write access, add ctime change Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 57 +++++++++++++++------------ tests/fuse_filesystem.rs | 74 ++++++++++++++++++++++++++--------- 2 files changed, 88 insertions(+), 43 deletions(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 0b53a177..e7e22066 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -191,7 +191,7 @@ where _size: Option, atime: Option, mtime: Option, - _ctime: Option, + ctime: Option, _fh: Option, _crtime: Option, _chgtime: Option, @@ -218,42 +218,49 @@ where return; } - if let Some(atime) = atime { - if attr.uid != req.uid() && req.uid() != 0 && atime != Now { - reply.error(libc::EPERM); - return; + let set_time_with_check = |time: TimeOrNow| { + if attr.uid != req.uid() && req.uid() != 0 && time != Now { + return None; } - if attr.uid != req.uid() && !check_access(&attr, req, libc::W_OK) { - reply.error(libc::EACCES); - return; + match time { + TimeOrNow::SpecificTime(time) => Some(time), + Now => Some(now), } + }; - attr.atime = match atime { - TimeOrNow::SpecificTime(time) => time, - Now => now, + if let Some(atime) = atime { + match set_time_with_check(atime) { + Some(atime) => attr.atime = atime, + None => { + reply.error(libc::EPERM); + return; + } }; attr.ctime = now; } if let Some(mtime) = mtime { - if attr.uid != req.uid() && req.uid() != 0 && mtime != Now { - reply.error(libc::EPERM); - return; - } - - if attr.uid != req.uid() && !check_access(&attr, req, libc::W_OK) { - reply.error(libc::EACCES); - return; - } - - attr.mtime = match mtime { - TimeOrNow::SpecificTime(time) => time, - Now => now, + match set_time_with_check(mtime) { + Some(mtime) => attr.mtime = mtime, + None => { + reply.error(libc::EPERM); + return; + } }; attr.ctime = now; } + if let Some(ctime) = ctime { + match set_time_with_check(TimeOrNow::SpecificTime(ctime)) { + Some(ctime) => attr.ctime = ctime, + None => { + reply.error(libc::EPERM); + return; + } + }; + } + reply.attr(&Duration::new(0, 0), &attr); return; } @@ -482,7 +489,6 @@ where reply.error(libc::EINVAL); return; } - let fh = self.get_new_fh(); let Ok(underlying_file_handle) = self .underlying_fs .create_file(name.clone(), (&self.chunker).clone()) @@ -535,6 +541,7 @@ where handles: 1, }; + let fh = self.get_new_fh(); reply.created( &Duration::new(0, 0), &file.attr, diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 88af13ec..ffb92c98 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -44,21 +44,18 @@ impl Drop for FuseFixture { } } +fn file_size(file: &File) -> u64 { + file.metadata().unwrap().len() +} + #[test] fn permissions() { let fuse_fixture = FuseFixture::default(); let mount_point = Path::new(&fuse_fixture.mount_point); let file_path = mount_point.join("file"); - OpenOptions::new() - .write(true) - .create(true) - .open(&file_path) - .unwrap(); - - let perms: Vec<_> = (0o000..=0o777).map(|m| Permissions::from_mode(m)).collect(); + File::create(&file_path).unwrap(); - let file_size = |file: &File| file.metadata().unwrap().len(); let read_ok = || { let mut file = OpenOptions::new().read(true).open(&file_path).unwrap(); let mut buf = vec![]; @@ -78,6 +75,8 @@ fn permissions() { let res = OpenOptions::new().write(true).open(&file_path); assert!(res.is_err()); }; + + let perms: Vec<_> = (0o000..=0o777).map(|m| Permissions::from_mode(m)).collect(); for perm in perms { fs::set_permissions(&file_path, perm.clone()).unwrap(); if perm.mode() & 0o400 != 0 { @@ -94,17 +93,60 @@ fn permissions() { } } +#[test] +fn write_not_to_end_fails() { + let fuse_fixture = FuseFixture::default(); + let mount_point = Path::new(&fuse_fixture.mount_point); + + let dir_path = mount_point.join("directory"); + let res = fs::create_dir(&dir_path); + assert!(res.is_err()); +} + +#[test] +fn create_dir_fails() { + let fuse_fixture = FuseFixture::default(); + let mount_point = Path::new(&fuse_fixture.mount_point); + + let file_path = mount_point.join("file"); + let mut file = File::create(&file_path).unwrap(); + + file.write_all(b"Hello, Chunkfs!").unwrap(); + file.write_all(&vec![0; MB]).unwrap(); + + let res1 = file.write_at(&vec![1, 2, 3], 10); + let res2 = file.write_at(&vec![1, 2, 3], file_size(&file) + 1); + assert!(res1.is_err()); + assert!(res2.is_err()); +} + +#[test] +fn filehandles_mods() { + let fuse_fixture = FuseFixture::default(); + let mount_point = Path::new(&fuse_fixture.mount_point); + + let file_path = mount_point.join("file"); + File::create(&file_path).unwrap(); + + let mut file = OpenOptions::new().write(true).open(&file_path).unwrap(); + let res = file.read(&mut vec![0; 512]); + assert!(res.is_err()); + + let file = OpenOptions::new().read(true).open(&file_path).unwrap(); + let res = file.write_at(&mut vec![0; 512], file_size(&file)); + assert!(res.is_err()); + + let res = OpenOptions::new().open(&file_path); + assert!(res.is_err()); +} + #[test] fn write_fuse_fs() { let fuse_fixture = FuseFixture::default(); let mount_point = Path::new(&fuse_fixture.mount_point); let file_path = mount_point.join("file"); - let mut file = OpenOptions::new() - .write(true) - .create(true) - .open(&file_path) - .unwrap(); + let mut file = File::create(&file_path).unwrap(); let mut data1 = vec![1u8; 2000]; let mut data2 = vec![2u8; 5000]; @@ -124,11 +166,7 @@ fn different_data_writes() { let mount_point = Path::new(&fuse_fixture.mount_point); let file_path = mount_point.join("file"); - let mut file = OpenOptions::new() - .write(true) - .create(true) - .open(&file_path) - .unwrap(); + let mut file = File::create(&file_path).unwrap(); let mut data1 = vec![1u8; 500]; let mut data2 = vec![2u8; 700]; From cb5f6af471610ee5b7fc081870d33fa33f60a45a Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 20 Apr 2025 19:50:58 +0300 Subject: [PATCH 18/60] add atime and ctime update in read() Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index e7e22066..62e8798e 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -338,7 +338,7 @@ where reply.error(libc::ESTALE); return; } - let Some(file) = self.files.get(&ino) else { + let Some(file) = self.files.get_mut(&ino) else { reply.error(libc::ENOENT); return; }; @@ -354,6 +354,9 @@ where let underlying_fh = &mut file_handle.underlying_file_handle; underlying_fh.set_offset(offset as usize); + let now = SystemTime::now(); + file.attr.atime = now; + file.attr.ctime = now; if let Ok(data) = self.underlying_fs.read(underlying_fh, size as usize) { reply.data(&data); return; From db0cfe0eba65d77b1e39e1f7761c0cb2406a3b2e Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 20 Apr 2025 19:52:58 +0300 Subject: [PATCH 19/60] add fuse tests: readdir, metadata time Signed-off-by: Gleb Nasretdinov --- Cargo.lock | 33 +++++++++++++ Cargo.toml | 1 + tests/fuse_filesystem.rs | 102 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 135 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 4e877a1a..6ead882c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,6 +188,7 @@ dependencies = [ "criterion", "csv", "fastcdc", + "filetime", "fuser", "itertools 0.14.0", "libc", @@ -489,6 +490,18 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys", +] + [[package]] name = "fnv" version = "1.0.7" @@ -708,6 +721,17 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + [[package]] name = "linux-raw-sys" version = "0.9.3" @@ -939,6 +963,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.11.1" diff --git a/Cargo.toml b/Cargo.toml index 0aaf0a90..dc47206b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ criterion = "0.5" sha3 = "0.10" approx = "0.5" tempfile = "3.14" +filetime = "0.2.25" serde_json = "1" diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index ffb92c98..0ca39ac8 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -1,13 +1,16 @@ use chunkfs::chunkers::SuperChunker; use chunkfs::hashers::SimpleHasher; use chunkfs::{FuseFS, MB}; +use filetime::FileTime; use fuser::BackgroundSession; use std::collections::HashMap; +use std::ffi::OsString; use std::fs; use std::fs::{File, OpenOptions, Permissions}; use std::io::{Read, Write}; -use std::os::unix::fs::{FileExt, PermissionsExt}; +use std::os::unix::fs::{FileExt, MetadataExt, PermissionsExt}; use std::path::Path; +use std::time::SystemTime; use uuid::Uuid; fn generate_unique_mount_point() -> String { @@ -48,6 +51,103 @@ fn file_size(file: &File) -> u64 { file.metadata().unwrap().len() } +fn to_unix_secs(time: &SystemTime) -> u64 { + time.duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() +} + +fn get_metadata_times(file: &File) -> (u64, u64, u64) { + let metadata = file.metadata().unwrap(); + ( + metadata.atime() as u64, + metadata.mtime() as u64, + metadata.ctime() as u64, + ) +} +#[test] +fn metadata_times() { + let fuse_fixture = FuseFixture::default(); + let mount_point = Path::new(&fuse_fixture.mount_point); + let file_path = mount_point.join("file"); + + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&file_path) + .unwrap(); + let (atime_init, mtime_init, ctime_init) = get_metadata_times(&file); + + std::thread::sleep(std::time::Duration::from_secs(1)); + + file.write(&mut vec![0; 512]).unwrap(); + let (atime1, mtime1, ctime1) = get_metadata_times(&file); + assert!(mtime1 > mtime_init); + assert!(ctime1 > ctime_init); + assert_eq!(atime1, atime_init); + + std::thread::sleep(std::time::Duration::from_secs(1)); + + file.read_at(&mut vec![0; 512], 0).unwrap(); + let (atime2, mtime2, ctime2) = get_metadata_times(&file); + assert!(atime2 > atime1); + assert_eq!(mtime2, mtime1); + assert!(ctime2 > ctime1); +} + +#[test] +fn manual_setattr() { + let fuse_fixture = FuseFixture::default(); + let mount_point = Path::new(&fuse_fixture.mount_point); + let file_path = mount_point.join("file"); + + let before_creation = SystemTime::now(); + let file = File::create(&file_path).unwrap(); + + let (atime1, mtime1, ctime1) = get_metadata_times(&file); + assert_eq!(atime1, mtime1); + assert_eq!(mtime1, ctime1); + let before_creation_in_unix_secs = to_unix_secs(&before_creation); + assert!(ctime1 >= before_creation_in_unix_secs); + + std::thread::sleep(std::time::Duration::from_secs(1)); + + let now = SystemTime::now(); + let now_minus10s = now - std::time::Duration::from_secs(10); + let now_minus100s = now - std::time::Duration::from_secs(100); + + let new_atime = FileTime::from_system_time(now_minus10s); + let new_mtime = FileTime::from_system_time(now_minus100s); + + filetime::set_file_atime(&file_path, new_atime).unwrap(); + filetime::set_file_mtime(&file_path, new_mtime).unwrap(); + + let (atime2, mtime2, ctime2) = get_metadata_times(&file); + assert_eq!(atime2, to_unix_secs(&now_minus10s)); + assert_eq!(mtime2, to_unix_secs(&now_minus100s)); + assert!(ctime2 > ctime1); +} +#[test] +fn readdir() { + let fuse_fixture = FuseFixture::default(); + let mount_point = Path::new(&fuse_fixture.mount_point); + + File::create(mount_point.join("file1")).unwrap(); + File::create(mount_point.join("file2")).unwrap(); + + let mut files = vec![]; + for entry in fs::read_dir(mount_point).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + assert!(path.is_file()); + files.push(path.file_name().unwrap().to_owned()); + } + assert!(files.contains(&OsString::from("file1"))); + assert!(files.contains(&OsString::from("file1"))); + assert_eq!(files.len(), 2) +} + #[test] fn permissions() { let fuse_fixture = FuseFixture::default(); From b60c015d444202df1a43a019be75812061491a50 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 20 Apr 2025 20:56:50 +0300 Subject: [PATCH 20/60] fix release(): was removing by file handle by inode counter instead of fh counter Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 62e8798e..50205f4e 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -422,7 +422,7 @@ where &mut self, _req: &Request<'_>, ino: u64, - _fh: u64, + fh: u64, _flags: i32, _lock_owner: Option, _flush: bool, @@ -436,7 +436,7 @@ where reply.error(libc::EINVAL); return; } - let Some(file_handle) = self.file_handles.remove(&ino) else { + let Some(file_handle) = self.file_handles.remove(&fh) else { reply.error(libc::EINVAL); return; }; From 77195123f0193f8bc8d7dfe8d4c650b53d15924c Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Mon, 21 Apr 2025 22:28:01 +0300 Subject: [PATCH 21/60] fix clippy warnings Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 23 +++++++---------------- src/system/storage.rs | 2 +- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 50205f4e..58ed258a 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -261,8 +261,7 @@ where }; } - reply.attr(&Duration::new(0, 0), &attr); - return; + reply.attr(&Duration::new(0, 0), attr); } fn open(&mut self, req: &Request<'_>, ino: u64, flags: i32, reply: ReplyOpen) { @@ -359,10 +358,8 @@ where file.attr.ctime = now; if let Ok(data) = self.underlying_fs.read(underlying_fh, size as usize) { reply.data(&data); - return; } else { reply.error(libc::EIO); - return; }; } @@ -400,10 +397,10 @@ where return; } - if !self + if self .underlying_fs .write_to_file(&mut file_handle.underlying_file_handle, data) - .is_ok() + .is_err() { reply.error(libc::EIO); return; @@ -432,7 +429,7 @@ where reply.error(libc::EINVAL); return; }; - if file.handles <= 0 { + if file.handles == 0 { reply.error(libc::EINVAL); return; } @@ -469,7 +466,7 @@ where .map(|(inode, file)| (inode, file.attr.kind, &file.name)); for (i, entry) in entries.enumerate().skip(offset as usize) { let (inode, kind, name) = entry; - if reply.add(inode.clone(), offset + i as i64 + 1, kind, name) { + if reply.add(*inode, offset + i as i64 + 1, kind, name) { break; } } @@ -494,7 +491,7 @@ where } let Ok(underlying_file_handle) = self .underlying_fs - .create_file(name.clone(), (&self.chunker).clone()) + .create_file(name.clone(), (self.chunker).clone()) else { reply.error(libc::EEXIST); return; @@ -545,13 +542,7 @@ where }; let fh = self.get_new_fh(); - reply.created( - &Duration::new(0, 0), - &file.attr, - 0, - fh.clone(), - flags as u32, - ); + reply.created(&Duration::new(0, 0), &file.attr, 0, fh, flags as u32); self.files.insert(ino, file); self.inodes.insert(name, ino); diff --git a/src/system/storage.rs b/src/system/storage.rs index 1faa98a7..df8f2740 100644 --- a/src/system/storage.rs +++ b/src/system/storage.rs @@ -147,7 +147,7 @@ where Data::Chunk(chunk) => Ok(chunk.clone()), Data::TargetChunk(keys) => Ok(self .target_map - .get_multi(&keys.into_iter().collect::>())? + .get_multi(&keys.iter().collect::>())? .into_iter() .flatten() .collect()), From dc903c9ab987dc9cf9fc54eb935edae063b3c1a5 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Tue, 22 Apr 2025 01:26:19 +0300 Subject: [PATCH 22/60] add installation of FUSE dependencies in CI Signed-off-by: Gleb Nasretdinov --- .github/workflows/rust.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8892e3a4..4910bff1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -12,13 +12,23 @@ jobs: steps: - uses: actions/checkout@v3 + + - name: Install FUSE dependencies + run: | + sudo apt update + sudo apt install -y fuse3 libfuse3-dev + - name: Formatting run: cargo fmt -- --check + - name: Clippy run: cargo clippy --all-targets --tests -- -D warnings + - name: Build run: cargo build --all-features --all-targets --verbose + - name: Run tests run: cargo test --verbose + - name: Run binary run: cargo build -p chunkfscli --verbose From e82352bcea1322afce3b91049637126c7e2b2a84 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Tue, 22 Apr 2025 01:40:28 +0300 Subject: [PATCH 23/60] fix clippy warnings Signed-off-by: Gleb Nasretdinov --- examples/mount.rs | 3 ++- src/system/scrub.rs | 4 ++-- tests/fuse_filesystem.rs | 15 ++++++++------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/examples/mount.rs b/examples/mount.rs index 23d25c59..5bcbde95 100644 --- a/examples/mount.rs +++ b/examples/mount.rs @@ -15,7 +15,7 @@ fn main() { fs::create_dir_all(mount_point).unwrap(); - let session = fuser::spawn_mount2(fuse_fs, mount_point, &vec![]).unwrap(); + let session = fuser::spawn_mount2(fuse_fs, mount_point, &[]).unwrap(); let file_path = mount_point.join("file"); // careful: writing is sequential only @@ -23,6 +23,7 @@ fn main() { .write(true) .read(true) .create(true) + .truncate(true) .open(&file_path) .unwrap(); diff --git a/src/system/scrub.rs b/src/system/scrub.rs index 0ee4860e..cd48fbc4 100644 --- a/src/system/scrub.rs +++ b/src/system/scrub.rs @@ -26,8 +26,8 @@ use super::storage::DataContainer; /// [DataContainer::extract] or [DataContainer::extract_mut] should be used. /// /// 2. A target map, which contains `Key`-`Vec` pairs, where `Key` is a generic value determined by the implementation. -/// The way data is stored is determined by the target map implementation, the only information known to the scrubber is that -/// the target map implements [Database] trait. It should only be used for storage purposes and not contain any algorithm logic. +/// The way data is stored is determined by the target map implementation, the only information known to the scrubber is that +/// the target map implements [Database] trait. It should only be used for storage purposes and not contain any algorithm logic. pub trait Scrub: Send where Hash: ChunkHash, diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 0ca39ac8..4370a5c2 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -29,7 +29,7 @@ impl FuseFixture { let fuse_fs = FuseFS::new(db, SimpleHasher, SuperChunker::default()); fs::create_dir_all(&mount_point).unwrap(); - let fuse_session = fuser::spawn_mount2(fuse_fs, &mount_point, &vec![]).unwrap(); + let fuse_session = fuser::spawn_mount2(fuse_fs, &mount_point, &[]).unwrap(); Self { mount_point, @@ -75,13 +75,14 @@ fn metadata_times() { .read(true) .write(true) .create(true) + .truncate(true) .open(&file_path) .unwrap(); let (atime_init, mtime_init, ctime_init) = get_metadata_times(&file); std::thread::sleep(std::time::Duration::from_secs(1)); - file.write(&mut vec![0; 512]).unwrap(); + file.write_all(&vec![0; 512]).unwrap(); let (atime1, mtime1, ctime1) = get_metadata_times(&file); assert!(mtime1 > mtime_init); assert!(ctime1 > ctime_init); @@ -168,7 +169,7 @@ fn permissions() { }; let write_ok = || { let file = OpenOptions::new().write(true).open(&file_path).unwrap(); - let write_len = file.write_at(&mut vec![0; 512], file_size(&file)).unwrap(); + let write_len = file.write_at(&vec![0; 512], file_size(&file)).unwrap(); assert_eq!(write_len, 512); }; let write_denied = || { @@ -176,7 +177,7 @@ fn permissions() { assert!(res.is_err()); }; - let perms: Vec<_> = (0o000..=0o777).map(|m| Permissions::from_mode(m)).collect(); + let perms: Vec<_> = (0o000..=0o777).map(Permissions::from_mode).collect(); for perm in perms { fs::set_permissions(&file_path, perm.clone()).unwrap(); if perm.mode() & 0o400 != 0 { @@ -214,8 +215,8 @@ fn create_dir_fails() { file.write_all(b"Hello, Chunkfs!").unwrap(); file.write_all(&vec![0; MB]).unwrap(); - let res1 = file.write_at(&vec![1, 2, 3], 10); - let res2 = file.write_at(&vec![1, 2, 3], file_size(&file) + 1); + let res1 = file.write_at(&[1, 2, 3], 10); + let res2 = file.write_at(&[1, 2, 3], file_size(&file) + 1); assert!(res1.is_err()); assert!(res2.is_err()); } @@ -233,7 +234,7 @@ fn filehandles_mods() { assert!(res.is_err()); let file = OpenOptions::new().read(true).open(&file_path).unwrap(); - let res = file.write_at(&mut vec![0; 512], file_size(&file)); + let res = file.write_at(&vec![0; 512], file_size(&file)); assert!(res.is_err()); let res = OpenOptions::new().open(&file_path); From 54b9b25f1c96868724b3b24e9f1dbe5dfa3a42d5 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 23 Apr 2025 23:16:56 +0300 Subject: [PATCH 24/60] add cache to FuseFS Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 116 +++++++++++++++++++++++++++++----- 1 file changed, 99 insertions(+), 17 deletions(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 58ed258a..08a2f255 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -1,6 +1,6 @@ use crate::system::file_layer::FileHandle; use crate::{ - create_cdc_filesystem, ChunkHash, ChunkerRef, DataContainer, Database, FileSystem, Hasher, + create_cdc_filesystem, ChunkHash, ChunkerRef, DataContainer, Database, FileSystem, Hasher, MB, }; use fuser::FileType::RegularFile; use fuser::TimeOrNow::Now; @@ -8,8 +8,10 @@ use fuser::{ FileAttr, FileType, Filesystem, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyOpen, ReplyWrite, Request, TimeOrNow, }; +use std::cmp::min; use std::collections::HashMap; use std::ffi::OsStr; +use std::io; use std::time::{Duration, SystemTime}; type Inode = u64; @@ -17,9 +19,12 @@ type Fh = u64; /// File is opened for execution. const FMODE_EXEC: i32 = 0x20; +const FILESYSTEM_CACHE_MAX_SIZE: usize = 500 * MB; +const FILE_CACHE_MAX_SIZE: usize = 200 * MB; #[derive(Clone)] struct FuseFile { + cache: Vec, attr: FileAttr, name: String, generation: u64, @@ -48,6 +53,7 @@ where next_fh: u64, file_handles: HashMap, chunker: ChunkerRef, + total_cache: usize, } impl FuseFS @@ -86,6 +92,7 @@ where blksize: 512, }; let root_dir = FuseFile { + cache: Vec::new(), attr: root_attr, name: ".".to_string(), generation: 0, @@ -104,6 +111,7 @@ where file_handles: HashMap::default(), next_fh: 0, chunker: chunker.into(), + total_cache: 0, } } @@ -116,6 +124,43 @@ where self.next_fh += 1; next_fh } + + fn drop_and_shrink_cache(&mut self, file: Inode, handle: Fh) -> io::Result<()> { + self.drop_cache(file, handle)?; + + let file = self.files.get_mut(&file).ok_or(io::ErrorKind::NotFound)?; + self.total_cache -= file.cache.len(); + file.cache = vec![]; + Ok(()) + } + + fn drop_cache(&mut self, file: Inode, handle: Fh) -> io::Result<()> { + let file = self.files.get_mut(&file).ok_or(io::ErrorKind::NotFound)?; + let handle = self + .file_handles + .get_mut(&handle) + .ok_or(io::ErrorKind::NotFound)?; + self.underlying_fs + .write_to_file(&mut handle.underlying_file_handle, &file.cache)?; + + file.cache.clear(); + Ok(()) + } + + fn drop_and_shrink_caches(&mut self) -> io::Result<()> { + for handle in self.file_handles.values_mut() { + let file = self + .files + .get_mut(&handle.inode) + .ok_or(io::ErrorKind::NotFound)?; + self.underlying_fs + .write_to_file(&mut handle.underlying_file_handle, &file.cache)?; + + self.total_cache -= file.cache.len(); + file.cache = vec![]; + } + Ok(()) + } } /// Checks the request rights for the file with the specified access mask (flags). @@ -297,13 +342,13 @@ where return; } - let underlying_file_handle = if write { - self.underlying_fs - .open_file(&file.name, self.chunker.clone()) - } else { - self.underlying_fs.open_file_readonly(&file.name) - } - .unwrap(); + let Ok(underlying_file_handle) = self + .underlying_fs + .open_file(&file.name, self.chunker.clone()) + else { + reply.error(libc::EBADF); + return; + }; let file_handle = FuseFileHandle { underlying_file_handle, @@ -345,18 +390,45 @@ where reply.error(libc::EINVAL); return; } + let offset = offset as usize; + let size = size as usize; if !check_access(&file.attr, req, libc::R_OK) || !file_handle.read { reply.error(libc::EACCES); return; } let underlying_fh = &mut file_handle.underlying_file_handle; - underlying_fh.set_offset(offset as usize); + underlying_fh.set_offset(offset); let now = SystemTime::now(); file.attr.atime = now; file.attr.ctime = now; - if let Ok(data) = self.underlying_fs.read(underlying_fh, size as usize) { + + if let Ok(mut data) = self.underlying_fs.read(underlying_fh, size) { + let read_size = data.len(); + let new_offset = offset + read_size; + underlying_fh.set_offset(new_offset); + if read_size > size || file.cache.len() > file.attr.size as usize { + reply.error(libc::EIO); + return; + } + if read_size == size || new_offset >= file.attr.size as usize { + reply.data(&data); + return; + } + let missing_size = size - read_size; + let disk_data_size = file.attr.size as usize - file.cache.len(); + if new_offset < disk_data_size { + reply.error(libc::EIO); + return; + } + + let cache_start_offset = new_offset - disk_data_size; + let cache_end_offset = min(file.cache.len(), cache_start_offset + missing_size); + data.extend_from_slice(&file.cache[cache_start_offset..cache_end_offset]); + let new_offset = offset + data.len(); + + underlying_fh.set_offset(new_offset); reply.data(&data); } else { reply.error(libc::EIO); @@ -397,16 +469,18 @@ where return; } - if self - .underlying_fs - .write_to_file(&mut file_handle.underlying_file_handle, data) - .is_err() - { + file.cache.extend_from_slice(data); + if file.cache.len() > FILE_CACHE_MAX_SIZE && self.drop_cache(ino, fh).is_err() { + reply.error(libc::EIO); + return; + } + if self.total_cache > FILESYSTEM_CACHE_MAX_SIZE && self.drop_and_shrink_caches().is_err() { reply.error(libc::EIO); return; } let now = SystemTime::now(); + let file = self.files.get_mut(&ino).unwrap(); file.attr.ctime = now; file.attr.mtime = now; file.attr.size += data.len() as u64; @@ -425,7 +499,7 @@ where _flush: bool, reply: ReplyEmpty, ) { - let Some(file) = self.files.get_mut(&ino) else { + let Some(file) = self.files.get(&ino) else { reply.error(libc::EINVAL); return; }; @@ -437,7 +511,14 @@ where reply.error(libc::EINVAL); return; }; + + if self.drop_and_shrink_cache(ino, fh).is_err() { + reply.error(libc::EIO); + return; + } + file_handle.underlying_file_handle.close(); + let file = self.files.get_mut(&ino).unwrap(); file.handles -= 1; reply.ok() } @@ -491,7 +572,7 @@ where } let Ok(underlying_file_handle) = self .underlying_fs - .create_file(name.clone(), (self.chunker).clone()) + .create_file(name.clone(), self.chunker.clone()) else { reply.error(libc::EEXIST); return; @@ -535,6 +616,7 @@ where write, }; let file = FuseFile { + cache: Vec::new(), attr, name: name.clone(), generation: 0, From 0f1e8d9c0be2c58d50e8d99732c34f7c507bda7d Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 23 Apr 2025 23:27:47 +0300 Subject: [PATCH 25/60] refactor FuseFS Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 83 +++++++++++++++++------------------ 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 08a2f255..5fb711d1 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -275,35 +275,29 @@ where }; if let Some(atime) = atime { - match set_time_with_check(atime) { - Some(atime) => attr.atime = atime, - None => { - reply.error(libc::EPERM); - return; - } + let Some(atime) = set_time_with_check(atime) else { + reply.error(libc::EPERM); + return; }; + attr.atime = atime; attr.ctime = now; } if let Some(mtime) = mtime { - match set_time_with_check(mtime) { - Some(mtime) => attr.mtime = mtime, - None => { - reply.error(libc::EPERM); - return; - } + let Some(mtime) = set_time_with_check(mtime) else { + reply.error(libc::EPERM); + return; }; + attr.mtime = mtime; attr.ctime = now; } if let Some(ctime) = ctime { - match set_time_with_check(TimeOrNow::SpecificTime(ctime)) { - Some(ctime) => attr.ctime = ctime, - None => { - reply.error(libc::EPERM); - return; - } + let Some(ctime) = set_time_with_check(TimeOrNow::SpecificTime(ctime)) else { + reply.error(libc::EPERM); + return; }; + attr.ctime = ctime; } reply.attr(&Duration::new(0, 0), attr); @@ -404,35 +398,38 @@ where file.attr.atime = now; file.attr.ctime = now; - if let Ok(mut data) = self.underlying_fs.read(underlying_fh, size) { - let read_size = data.len(); - let new_offset = offset + read_size; - underlying_fh.set_offset(new_offset); - if read_size > size || file.cache.len() > file.attr.size as usize { - reply.error(libc::EIO); - return; - } - if read_size == size || new_offset >= file.attr.size as usize { - reply.data(&data); - return; - } - let missing_size = size - read_size; - let disk_data_size = file.attr.size as usize - file.cache.len(); - if new_offset < disk_data_size { - reply.error(libc::EIO); - return; - } + let Ok(mut data) = self.underlying_fs.read(underlying_fh, size) else { + reply.error(libc::EIO); + return; + }; - let cache_start_offset = new_offset - disk_data_size; - let cache_end_offset = min(file.cache.len(), cache_start_offset + missing_size); - data.extend_from_slice(&file.cache[cache_start_offset..cache_end_offset]); - let new_offset = offset + data.len(); + let read_size = data.len(); + let new_offset = offset + read_size; + underlying_fh.set_offset(new_offset); - underlying_fh.set_offset(new_offset); + if read_size > size || file.cache.len() > file.attr.size as usize { + reply.error(libc::EIO); + return; + } + if read_size == size || new_offset >= file.attr.size as usize { reply.data(&data); - } else { + return; + } + + let missing_size = size - read_size; + let disk_data_size = file.attr.size as usize - file.cache.len(); + if new_offset < disk_data_size { reply.error(libc::EIO); - }; + return; + } + + let cache_start_offset = new_offset - disk_data_size; + let cache_end_offset = min(file.cache.len(), cache_start_offset + missing_size); + data.extend_from_slice(&file.cache[cache_start_offset..cache_end_offset]); + let new_offset = offset + data.len(); + + underlying_fh.set_offset(new_offset); + reply.data(&data); } fn write( From d51a4e239ef8870db078b8ab0f50b1c596ae6381 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 24 Apr 2025 00:53:57 +0300 Subject: [PATCH 26/60] renamed mixed-up fuse tests Signed-off-by: Gleb Nasretdinov --- tests/fuse_filesystem.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 4370a5c2..96a28eb1 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -195,7 +195,7 @@ fn permissions() { } #[test] -fn write_not_to_end_fails() { +fn create_dir_fails() { let fuse_fixture = FuseFixture::default(); let mount_point = Path::new(&fuse_fixture.mount_point); @@ -205,7 +205,7 @@ fn write_not_to_end_fails() { } #[test] -fn create_dir_fails() { +fn write_not_to_end_fails() { let fuse_fixture = FuseFixture::default(); let mount_point = Path::new(&fuse_fixture.mount_point); From e93f78517fa09844b37c31a4beecd22c32972dc2 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 24 Apr 2025 03:02:19 +0300 Subject: [PATCH 27/60] fix global cache drop in FuseFS: was removing file handle before iterating Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 5fb711d1..c51a0287 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -504,16 +504,20 @@ where reply.error(libc::EINVAL); return; } - let Some(file_handle) = self.file_handles.remove(&fh) else { + if !self.file_handles.contains_key(&fh) { reply.error(libc::EINVAL); return; - }; + } if self.drop_and_shrink_cache(ino, fh).is_err() { reply.error(libc::EIO); return; } + let Some(file_handle) = self.file_handles.remove(&fh) else { + reply.error(libc::EINVAL); + return; + }; file_handle.underlying_file_handle.close(); let file = self.files.get_mut(&ino).unwrap(); file.handles -= 1; From 69618d99f611492488c6822c83ea83a4fafd58f0 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 24 Apr 2025 03:03:06 +0300 Subject: [PATCH 28/60] add tests with different cache scenarios, concurrent file handles Signed-off-by: Gleb Nasretdinov --- tests/fuse_filesystem.rs | 116 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 96a28eb1..2727dccc 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -293,3 +293,119 @@ fn different_data_writes() { .unwrap(); assert_eq!(actual, expected); } + +#[test] +fn read_dropped_cache() { + let fuse_fixture = FuseFixture::default(); + let mount_point = Path::new(&fuse_fixture.mount_point); + + let file_path = mount_point.join("file"); + let mut file = OpenOptions::new() + .create(true) + .write(true) + .open(&file_path) + .unwrap(); + file.write_all(&[0; 10 * MB]).unwrap(); + drop(file); + let file = OpenOptions::new() + .write(true) + .read(true) + .open(&file_path) + .unwrap(); + file.write_all_at(&[1; 10 * MB], file_size(&file)).unwrap(); + + // read dropped cache from start to end - epsilon + let mut actual = vec![10; 7 * MB]; + assert_eq!(file.read_at(&mut actual, 0).unwrap(), 7 * MB); + assert_eq!(actual, [0; 7 * MB]); + + // read dropped cache from start to end + epsilon + let mut actual = vec![10; 12 * MB]; + assert_eq!(file.read_at(&mut actual, 0).unwrap(), 12 * MB); + let expected = [vec![0; 10 * MB], vec![1; 2 * MB]].concat(); + assert_eq!(actual, expected); + + // read dropped cache from start + epsilon to end - epsilon + actual = vec![10; 7 * MB]; + assert_eq!(file.read_at(&mut actual, 2 * MB as u64).unwrap(), 7 * MB); + assert_eq!(actual, [0; 7 * MB]); + + // read dropped cache from start + epsilon to end + epsilon + actual = vec![10; 7 * MB]; + assert_eq!(file.read_at(&mut actual, 7 * MB as u64).unwrap(), 7 * MB); + let expected = [vec![0; 3 * MB], vec![1; 4 * MB]].concat(); + assert_eq!(actual, expected); +} + +#[test] +fn read_cache() { + let fuse_fixture = FuseFixture::default(); + let mount_point = Path::new(&fuse_fixture.mount_point); + + let file_path = mount_point.join("file"); + let mut file = OpenOptions::new() + .create(true) + .write(true) + .open(&file_path) + .unwrap(); + file.write_all(&[0; 10 * MB]).unwrap(); + drop(file); + let file = OpenOptions::new() + .write(true) + .read(true) + .open(&file_path) + .unwrap(); + file.write_all_at(&[1; 20 * MB], file_size(&file)).unwrap(); + + // read cache from start - epsilon to end + epsilon + let mut actual = vec![10; 30 * MB]; + assert_eq!(file.read_at(&mut actual, 5 * MB as u64).unwrap(), 25 * MB); + let expected = [vec![0; 5 * MB], vec![1; 20 * MB], vec![10; 5 * MB]].concat(); + assert_eq!(actual, expected); + + // read cache from start to end - epsilon + actual = vec![10; 15 * MB]; + assert_eq!(file.read_at(&mut actual, 10 * MB as u64).unwrap(), 15 * MB); + assert_eq!(actual, [1; 15 * MB]); + + // read cache from start + epsilon to end + epsilon + actual = vec![10; 40 * MB]; + assert_eq!(file.read_at(&mut actual, 12 * MB as u64).unwrap(), 18 * MB); + let expected = [vec![1; 18 * MB], vec![10; 22 * MB]].concat(); + assert_eq!(actual, expected); +} + +#[test] +fn concurrent_file_handles() { + let fuse_fixture = FuseFixture::default(); + let mount_point = Path::new(&fuse_fixture.mount_point); + + let file_path = mount_point.join("file"); + File::create(&file_path).unwrap(); + + let handle1 = OpenOptions::new().append(true).open(&file_path).unwrap(); + let handle2 = OpenOptions::new().append(true).open(&file_path).unwrap(); + let handle3 = OpenOptions::new().append(true).open(&file_path).unwrap(); + for _ in 0..12 { + handle1 + .write_all_at(&vec![1; MB], file_size(&handle1)) + .unwrap(); + handle2 + .write_all_at(&vec![2; MB], file_size(&handle1)) + .unwrap(); + handle3 + .write_all_at(&vec![3; MB], file_size(&handle1)) + .unwrap(); + } + drop(handle2); + drop(handle3); + drop(handle1); + + let expected = [[1; MB], [2; MB], [3; MB]].concat().repeat(12); + + let mut file = File::open(&file_path).unwrap(); + let mut actual = vec![0; 12 * 3 * MB]; + assert_eq!(file.read(&mut actual).unwrap(), 12 * 3 * MB); + assert_eq!(actual, expected); + assert_eq!(file.metadata().unwrap().len(), 12 * 3 * MB as u64); +} From 5d9201788eb9f3d71e62b77b538ac37abb4248d3 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 24 Apr 2025 03:25:19 +0300 Subject: [PATCH 29/60] clippy warnings Signed-off-by: Gleb Nasretdinov --- tests/fuse_filesystem.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 2727dccc..f58e71ea 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -302,6 +302,7 @@ fn read_dropped_cache() { let file_path = mount_point.join("file"); let mut file = OpenOptions::new() .create(true) + .truncate(true) .write(true) .open(&file_path) .unwrap(); @@ -345,6 +346,7 @@ fn read_cache() { let file_path = mount_point.join("file"); let mut file = OpenOptions::new() .create(true) + .truncate(true) .write(true) .open(&file_path) .unwrap(); From 54fea19dcfc7d23602abb0efa5eacf66f28b48b0 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 24 Apr 2025 22:39:14 +0300 Subject: [PATCH 30/60] add 7.28 FUSE ABI version as fuser feature Signed-off-by: Gleb Nasretdinov --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index dc47206b..1151e737 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ serde = { version = "1", optional = true, features = ["derive"] } serde_with = { version = "3", optional = true } chrono = { version = "0.4", optional = true, features = ["serde"] } libc = "0.2.171" -fuser = "0.15.1" +fuser = { version = "0.15.1", features = ["abi-7-28"] } [features] chunkers = ["cdc-chunkers", "fastcdc"] From db6689cf3a1fd4f7d56f7177365b46e3e03057d2 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 24 Apr 2025 23:31:46 +0300 Subject: [PATCH 31/60] add AutoUnmount option to tests and example Signed-off-by: Gleb Nasretdinov --- examples/mount.rs | 3 ++- tests/fuse_filesystem.rs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/mount.rs b/examples/mount.rs index 5bcbde95..2d9d8d30 100644 --- a/examples/mount.rs +++ b/examples/mount.rs @@ -1,6 +1,7 @@ use chunkfs::chunkers::SuperChunker; use chunkfs::hashers::SimpleHasher; use chunkfs::{FuseFS, MB}; +use fuser::MountOption::AutoUnmount; use std::collections::HashMap; use std::fs; use std::fs::OpenOptions; @@ -15,7 +16,7 @@ fn main() { fs::create_dir_all(mount_point).unwrap(); - let session = fuser::spawn_mount2(fuse_fs, mount_point, &[]).unwrap(); + let session = fuser::spawn_mount2(fuse_fs, mount_point, &[AutoUnmount]).unwrap(); let file_path = mount_point.join("file"); // careful: writing is sequential only diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index f58e71ea..0a7264f7 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -3,6 +3,7 @@ use chunkfs::hashers::SimpleHasher; use chunkfs::{FuseFS, MB}; use filetime::FileTime; use fuser::BackgroundSession; +use fuser::MountOption::AutoUnmount; use std::collections::HashMap; use std::ffi::OsString; use std::fs; @@ -29,7 +30,7 @@ impl FuseFixture { let fuse_fs = FuseFS::new(db, SimpleHasher, SuperChunker::default()); fs::create_dir_all(&mount_point).unwrap(); - let fuse_session = fuser::spawn_mount2(fuse_fs, &mount_point, &[]).unwrap(); + let fuse_session = fuser::spawn_mount2(fuse_fs, &mount_point, &[AutoUnmount]).unwrap(); Self { mount_point, From 2c95124a88fd9c0a15c877a4f368ee4b98e92682 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 24 Apr 2025 23:35:39 +0300 Subject: [PATCH 32/60] CI: add user_allow_other to /etc/fuse.conf Signed-off-by: Gleb Nasretdinov --- .github/workflows/rust.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4910bff1..703312a7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -17,6 +17,7 @@ jobs: run: | sudo apt update sudo apt install -y fuse3 libfuse3-dev + echo "user_allow_other" | sudo tee -a /etc/fuse.conf - name: Formatting run: cargo fmt -- --check From 88fe88fb85fb976d574cffe1b1edd7cf9869e50b Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Fri, 25 Apr 2025 18:21:16 +0300 Subject: [PATCH 33/60] reduce FuseFS cache: big cache was causing rare, long writes Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index c51a0287..7d463302 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -19,8 +19,8 @@ type Fh = u64; /// File is opened for execution. const FMODE_EXEC: i32 = 0x20; -const FILESYSTEM_CACHE_MAX_SIZE: usize = 500 * MB; -const FILE_CACHE_MAX_SIZE: usize = 200 * MB; +const FILESYSTEM_CACHE_MAX_SIZE: usize = 25 * MB; +const FILE_CACHE_MAX_SIZE: usize = 5 * MB; #[derive(Clone)] struct FuseFile { From 77a522579adf56040e4998fca7110d2a1abc563b Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Fri, 25 Apr 2025 18:31:46 +0300 Subject: [PATCH 34/60] add fuse bench Signed-off-by: Gleb Nasretdinov --- Cargo.toml | 4 + benches/fuse_write_read.rs | 197 +++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 benches/fuse_write_read.rs diff --git a/Cargo.toml b/Cargo.toml index 1151e737..3317c527 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,3 +53,7 @@ harness = false [[bench]] name = "chunker_sizes" harness = false + +[[bench]] +name = "fuse_write_read" +harness = false diff --git a/benches/fuse_write_read.rs b/benches/fuse_write_read.rs new file mode 100644 index 00000000..4574ece6 --- /dev/null +++ b/benches/fuse_write_read.rs @@ -0,0 +1,197 @@ +use cdc_chunkers::SizeParams; +use chunkfs::chunkers::{LeapChunker, RabinChunker, SuperChunker, UltraChunker}; +use chunkfs::hashers::Sha256Hasher; +use chunkfs::{ChunkerRef, FuseFS, MB}; +use criterion::measurement::WallTime; +use criterion::{BatchSize, BenchmarkGroup, BenchmarkId, Criterion, Throughput}; +use fuser::MountOption::AutoUnmount; +use std::collections::HashMap; +use std::fs; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; +use std::path::Path; + +const SAMPLE_SIZE: usize = 30; + +#[allow(dead_code)] +#[derive(Copy, Clone, Debug)] +enum Algorithms { + Rabin, + Leap, + Super, + Ultra, +} + +#[allow(dead_code)] +fn chunkers() -> Vec { + vec![ + Algorithms::Rabin, + Algorithms::Leap, + Algorithms::Super, + Algorithms::Ultra, + ] +} + +#[allow(dead_code)] +fn get_chunker(algorithm: Algorithms, params: SizeParams) -> ChunkerRef { + match algorithm { + Algorithms::Rabin => RabinChunker::new(params).into(), + Algorithms::Leap => LeapChunker::new(params).into(), + Algorithms::Super => UltraChunker::new(params).into(), + Algorithms::Ultra => SuperChunker::new(params).into(), + } +} + +#[allow(dead_code)] +fn get_default_sizes(algorithm: Algorithms) -> SizeParams { + match algorithm { + Algorithms::Rabin => SizeParams::rabin_default(), + Algorithms::Leap => SizeParams::leap_default(), + Algorithms::Super => SizeParams::super_default(), + Algorithms::Ultra => SizeParams::ultra_default(), + } +} + +struct Dataset { + filename: String, + size: u64, +} + +pub fn bench(c: &mut Criterion) { + let dataset1_len = File::open("archX4.tar").unwrap().metadata().unwrap().len(); + let dataset1 = Dataset { + filename: "archX4.tar".to_string(), + size: dataset1_len, + }; + let datasets = vec![dataset1]; + + for dataset in datasets { + let mut group = c.benchmark_group("FuseChunkers"); + group.sample_size(SAMPLE_SIZE); + group.throughput(Throughput::Bytes(dataset.size)); + + for chunker in chunkers() { + let params = get_default_sizes(chunker); + bench_write(&dataset, &mut group, chunker, params); + } + + for chunker in chunkers() { + let params = get_default_sizes(chunker); + bench_read(&dataset, &mut group, chunker, params); + } + } +} + +fn bench_write( + dataset: &Dataset, + group: &mut BenchmarkGroup, + algorithm: Algorithms, + params: SizeParams, +) { + let bench_name = dataset.filename.clone(); + let parameter = format!("write_fuse-{:?}-{}", algorithm, params); + group.bench_function(BenchmarkId::new(bench_name, parameter), |b| { + b.iter_batched( + || { + let mount_point = Path::new("mount_dir/mount_point"); + let db = HashMap::default(); + let chunker = get_chunker(algorithm, params); + let fuse_fs = FuseFS::new(db, Sha256Hasher::default(), chunker); + + fs::create_dir_all(mount_point).unwrap(); + let session = fuser::spawn_mount2(fuse_fs, mount_point, &[AutoUnmount]).unwrap(); + + let fuse_path = mount_point.join("file"); + let fuse_file = OpenOptions::new() + .write(true) + .read(true) + .create(true) + .truncate(true) + .open(&fuse_path) + .unwrap(); + + let source = File::open(&dataset.filename).unwrap(); + + (session, source, fuse_file) + }, + |(_session, mut source, mut fuse_file)| { + let mut buf = vec![0; 50 * MB]; + loop { + let bytes_read = source.read(&mut buf).unwrap(); + if bytes_read == 0 { + break; + } + fuse_file.write_all(&buf[..bytes_read]).unwrap(); + } + drop(fuse_file); + }, + BatchSize::PerIteration, + ) + }); +} + +fn bench_read( + dataset: &Dataset, + group: &mut BenchmarkGroup, + algorithm: Algorithms, + params: SizeParams, +) { + let mount_point = Path::new("mount_dir/mount_point"); + let db = HashMap::default(); + let chunker = get_chunker(algorithm, params); + let fuse_fs = FuseFS::new(db, Sha256Hasher::default(), chunker); + + fs::create_dir_all(mount_point).unwrap(); + let _session = fuser::spawn_mount2(fuse_fs, mount_point, &[AutoUnmount]).unwrap(); + + let fuse_path = mount_point.join("file"); + let mut fuse_file = OpenOptions::new() + .write(true) + .read(true) + .create(true) + .truncate(true) + .open(&fuse_path) + .unwrap(); + + let mut source = File::open(&dataset.filename).unwrap(); + + let mut buf = vec![0; 50 * MB]; + loop { + let bytes_read = source.read(&mut buf).unwrap(); + if bytes_read == 0 { + break; + } + fuse_file.write_all(&buf[..bytes_read]).unwrap(); + } + fuse_file.flush().unwrap(); + drop(fuse_file); + + let bench_name = dataset.filename.clone(); + let parameter = format!("read_fuse-{:?}-{}", algorithm, params); + group.bench_function(BenchmarkId::new(bench_name, parameter), |b| { + b.iter_batched( + || File::open(&fuse_path).unwrap(), + |mut fuse_file| { + let mut buf = vec![0; 50 * MB]; + loop { + let bytes_read = fuse_file.read(&mut buf).unwrap(); + if bytes_read == 0 { + break; + } + } + }, + BatchSize::PerIteration, + ) + }); +} + +pub fn benches() { + let mut criterion: Criterion<_> = Criterion::default().configure_from_args(); + bench(&mut criterion); +} + +fn main() { + benches(); + + Criterion::default().configure_from_args().final_summary(); +} From bb7d22d5920a30c6cc35c01f43aedd3bf7a2a7b8 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 26 Apr 2025 02:39:17 +0300 Subject: [PATCH 35/60] add init method in FuseFS Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 7d463302..b046b3f0 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -2,12 +2,14 @@ use crate::system::file_layer::FileHandle; use crate::{ create_cdc_filesystem, ChunkHash, ChunkerRef, DataContainer, Database, FileSystem, Hasher, MB, }; +use fuser::consts::FUSE_BIG_WRITES; use fuser::FileType::RegularFile; use fuser::TimeOrNow::Now; use fuser::{ - FileAttr, FileType, Filesystem, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, - ReplyEntry, ReplyOpen, ReplyWrite, Request, TimeOrNow, + FileAttr, FileType, Filesystem, KernelConfig, ReplyAttr, ReplyCreate, ReplyData, + ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyOpen, ReplyWrite, Request, TimeOrNow, }; +use libc::c_int; use std::cmp::min; use std::collections::HashMap; use std::ffi::OsStr; @@ -204,6 +206,13 @@ where B: Database>, Hash: ChunkHash, { + fn init(&mut self, _req: &Request<'_>, config: &mut KernelConfig) -> Result<(), c_int> { + let _ = config.add_capabilities(FUSE_BIG_WRITES); + if let Err(nearest) = config.set_max_write(128 * MB as u32) { + let _ = config.set_max_write(nearest); + }; + Ok(()) + } fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) { let name = name.to_str().unwrap().to_owned(); if parent != 1 { From 3c4ef623bd7cb98e51023d9d83dfeb0fc74b4175 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 26 Apr 2025 02:49:56 +0300 Subject: [PATCH 36/60] add flush() method in FuseFS Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index b046b3f0..c754d8b8 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -213,6 +213,32 @@ where }; Ok(()) } + + fn flush(&mut self, _req: &Request<'_>, ino: u64, fh: u64, lock_owner: u64, reply: ReplyEmpty) { + let Some(file_handle) = self.file_handles.get_mut(&fh) else { + reply.error(libc::EBADF); + return; + }; + if file_handle.inode != ino { + reply.error(libc::ESTALE); + return; + } + let Some(_) = self.files.get_mut(&ino) else { + reply.error(libc::ENOENT); + return; + }; + if !file_handle.write && !file_handle.read { + reply.error(libc::EACCES); + return; + } + + if self.drop_cache(ino, fh).is_err() { + reply.error(libc::EIO); + return; + } + reply.ok() + } + fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) { let name = name.to_str().unwrap().to_owned(); if parent != 1 { From a429b0473376a950b205a3ee2176072bfe3fd8cf Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 26 Apr 2025 03:05:50 +0300 Subject: [PATCH 37/60] change mount points in fuse tests to mount_dir/X_mount_point Signed-off-by: Gleb Nasretdinov --- tests/fuse_filesystem.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 0a7264f7..7367955a 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -10,7 +10,7 @@ use std::fs; use std::fs::{File, OpenOptions, Permissions}; use std::io::{Read, Write}; use std::os::unix::fs::{FileExt, MetadataExt, PermissionsExt}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::time::SystemTime; use uuid::Uuid; @@ -19,13 +19,14 @@ fn generate_unique_mount_point() -> String { } struct FuseFixture { - mount_point: String, + mount_point: PathBuf, fuse_session: Option, } impl FuseFixture { fn default() -> Self { - let mount_point = generate_unique_mount_point(); + let mount_dir = Path::new("mount_dir"); + let mount_point = mount_dir.join(generate_unique_mount_point()); let db = HashMap::default(); let fuse_fs = FuseFS::new(db, SimpleHasher, SuperChunker::default()); fs::create_dir_all(&mount_point).unwrap(); From 65712c3ef906855b6441820b5bffed745ac26822 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 26 Apr 2025 04:20:32 +0300 Subject: [PATCH 38/60] fix single chunk read with uneven offset Signed-off-by: Gleb Nasretdinov --- src/system/mod.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/system/mod.rs b/src/system/mod.rs index cf6dbd3d..086254ad 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -186,10 +186,8 @@ where let read_size_possible = last_span.offset() + last_span.len() - original_offset; let read_size_actual = min(size, read_size_possible); let data_extra_end = read_size_possible - read_size_actual; // amount of data from end to be removed - data_vectors - .last_mut() - .unwrap() - .truncate(last_span.len() - data_extra_end); + let last_vector = data_vectors.last_mut().unwrap(); + last_vector.truncate(last_vector.len() - data_extra_end); Ok(data_vectors.concat()) } From 462a815575339221a1f6559c6a247ff42d0f2360 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 26 Apr 2025 04:27:25 +0300 Subject: [PATCH 39/60] fix cache drop: was dropping by underlying fh offset without it's assignment Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index c754d8b8..edb238e6 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -142,6 +142,10 @@ where .file_handles .get_mut(&handle) .ok_or(io::ErrorKind::NotFound)?; + + handle + .underlying_file_handle + .set_offset(file.attr.size as usize - file.cache.len()); self.underlying_fs .write_to_file(&mut handle.underlying_file_handle, &file.cache)?; @@ -502,6 +506,7 @@ where } file.cache.extend_from_slice(data); + file.attr.size += data.len() as u64; if file.cache.len() > FILE_CACHE_MAX_SIZE && self.drop_cache(ino, fh).is_err() { reply.error(libc::EIO); return; @@ -515,7 +520,6 @@ where let file = self.files.get_mut(&ino).unwrap(); file.attr.ctime = now; file.attr.mtime = now; - file.attr.size += data.len() as u64; file.generation += 1; reply.written(data.len() as u32); From 741e1abd0354ba1e7ccf8006f7e1f29316cd4fb3 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 26 Apr 2025 05:14:31 +0300 Subject: [PATCH 40/60] update cache tests with flush() and new cache sizes Signed-off-by: Gleb Nasretdinov --- tests/fuse_filesystem.rs | 124 ++++++++++++++++++++++++--------------- 1 file changed, 78 insertions(+), 46 deletions(-) diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 7367955a..1c048188 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -305,39 +305,53 @@ fn read_dropped_cache() { let mut file = OpenOptions::new() .create(true) .truncate(true) - .write(true) - .open(&file_path) - .unwrap(); - file.write_all(&[0; 10 * MB]).unwrap(); - drop(file); - let file = OpenOptions::new() .write(true) .read(true) .open(&file_path) .unwrap(); - file.write_all_at(&[1; 10 * MB], file_size(&file)).unwrap(); + file.write_all(&[0; 10 * MB]).unwrap(); + file.flush().unwrap(); + file.write_all(&[1; 3 * MB]).unwrap(); + + let mut actual = vec![10; 14 * MB]; + assert_eq!(file.read_at(&mut actual, 0).unwrap(), 13 * MB); + let expected = [vec![0; 10 * MB], vec![1; 3 * MB], vec![10; MB]].concat(); + assert_eq!( + actual, expected, + "read entire file with dropped and dirty cache is correct" + ); - // read dropped cache from start to end - epsilon - let mut actual = vec![10; 7 * MB]; + actual = vec![10; 7 * MB]; assert_eq!(file.read_at(&mut actual, 0).unwrap(), 7 * MB); - assert_eq!(actual, [0; 7 * MB]); + assert_eq!( + actual, + [0; 7 * MB], + "read dropped cache from start to end - epsilon is correct" + ); - // read dropped cache from start to end + epsilon - let mut actual = vec![10; 12 * MB]; + actual = vec![10; 12 * MB]; assert_eq!(file.read_at(&mut actual, 0).unwrap(), 12 * MB); let expected = [vec![0; 10 * MB], vec![1; 2 * MB]].concat(); - assert_eq!(actual, expected); + assert_eq!( + actual, expected, + "read dropped cache from start to end + epsilon is correct" + ); - // read dropped cache from start + epsilon to end - epsilon actual = vec![10; 7 * MB]; - assert_eq!(file.read_at(&mut actual, 2 * MB as u64).unwrap(), 7 * MB); - assert_eq!(actual, [0; 7 * MB]); - - // read dropped cache from start + epsilon to end + epsilon - actual = vec![10; 7 * MB]; - assert_eq!(file.read_at(&mut actual, 7 * MB as u64).unwrap(), 7 * MB); - let expected = [vec![0; 3 * MB], vec![1; 4 * MB]].concat(); - assert_eq!(actual, expected); + assert_eq!(file.read_at(&mut actual, MB as u64).unwrap(), 7 * MB); + assert_eq!( + actual, + [0; 7 * MB], + "read dropped cache from start + epsilon to end - epsilon is correct" + ); + + actual = vec![10; 10 * MB]; + assert_eq!(file.read_at(&mut actual, 7 * MB as u64).unwrap(), 6 * MB); + let expected = [vec![0; 3 * MB], vec![1; 3 * MB], vec![10; 4 * MB]].concat(); + assert_eq!( + actual, expected, + "read dropped cache from start + epsilon to end + epsilon is correct" + ); } #[test] @@ -349,34 +363,52 @@ fn read_cache() { let mut file = OpenOptions::new() .create(true) .truncate(true) - .write(true) - .open(&file_path) - .unwrap(); - file.write_all(&[0; 10 * MB]).unwrap(); - drop(file); - let file = OpenOptions::new() .write(true) .read(true) .open(&file_path) .unwrap(); - file.write_all_at(&[1; 20 * MB], file_size(&file)).unwrap(); - - // read cache from start - epsilon to end + epsilon - let mut actual = vec![10; 30 * MB]; - assert_eq!(file.read_at(&mut actual, 5 * MB as u64).unwrap(), 25 * MB); - let expected = [vec![0; 5 * MB], vec![1; 20 * MB], vec![10; 5 * MB]].concat(); - assert_eq!(actual, expected); - - // read cache from start to end - epsilon - actual = vec![10; 15 * MB]; - assert_eq!(file.read_at(&mut actual, 10 * MB as u64).unwrap(), 15 * MB); - assert_eq!(actual, [1; 15 * MB]); - - // read cache from start + epsilon to end + epsilon - actual = vec![10; 40 * MB]; - assert_eq!(file.read_at(&mut actual, 12 * MB as u64).unwrap(), 18 * MB); - let expected = [vec![1; 18 * MB], vec![10; 22 * MB]].concat(); - assert_eq!(actual, expected); + file.write_all(&[0; 10 * MB]).unwrap(); + file.flush().unwrap(); + file.write_all(&[1; 3 * MB]).unwrap(); + + let mut actual = vec![10; 15 * MB]; + assert_eq!(file.read_at(&mut actual, 0).unwrap(), 13 * MB); + let expected = [vec![0; 10 * MB], vec![1; 3 * MB], vec![10; 2 * MB]].concat(); + assert_eq!( + actual, expected, + "read entire file with dropped and dirty cache is correct" + ); + + actual = vec![10; 10 * MB]; + assert_eq!(file.read_at(&mut actual, 5 * MB as u64).unwrap(), 8 * MB); + let expected = [vec![0; 5 * MB], vec![1; 3 * MB], vec![10; 2 * MB]].concat(); + assert_eq!( + actual, expected, + "read cache from start - epsilon to end + epsilon is correct" + ); + + actual = vec![10; 2 * MB]; + assert_eq!(file.read_at(&mut actual, 10 * MB as u64).unwrap(), 2 * MB); + assert_eq!( + actual, + [1; 2 * MB], + "read cache from start to end - epsilon is correct" + ); + + actual = vec![10; 5 * MB]; + assert_eq!(file.read_at(&mut actual, 11 * MB as u64).unwrap(), 2 * MB); + let expected = [vec![1; 2 * MB], vec![10; 3 * MB]].concat(); + assert_eq!( + actual, expected, + "read cache from start + epsilon to end + epsilon is correct" + ); + + actual = vec![10; MB]; + assert_eq!(file.read_at(&mut actual, 11 * MB as u64).unwrap(), 1 * MB); + assert_eq!( + actual, [1; MB], + "read cache from start + epsilon to end - epsilon is correct" + ); } #[test] From bcdbfa9f7b380553d10f41b37f1775d646fc0670 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 26 Apr 2025 05:25:28 +0300 Subject: [PATCH 41/60] add O_DIRECT to all write-read fuse tests: cache can return wrongly correct read() Signed-off-by: Gleb Nasretdinov --- tests/fuse_filesystem.rs | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 1c048188..55b9cc7c 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -4,12 +4,13 @@ use chunkfs::{FuseFS, MB}; use filetime::FileTime; use fuser::BackgroundSession; use fuser::MountOption::AutoUnmount; +use libc::O_DIRECT; use std::collections::HashMap; use std::ffi::OsString; use std::fs; use std::fs::{File, OpenOptions, Permissions}; use std::io::{Read, Write}; -use std::os::unix::fs::{FileExt, MetadataExt, PermissionsExt}; +use std::os::unix::fs::{FileExt, MetadataExt, OpenOptionsExt, PermissionsExt}; use std::path::{Path, PathBuf}; use std::time::SystemTime; use uuid::Uuid; @@ -78,6 +79,7 @@ fn metadata_times() { .write(true) .create(true) .truncate(true) + .custom_flags(O_DIRECT) .open(&file_path) .unwrap(); let (atime_init, mtime_init, ctime_init) = get_metadata_times(&file); @@ -106,7 +108,13 @@ fn manual_setattr() { let file_path = mount_point.join("file"); let before_creation = SystemTime::now(); - let file = File::create(&file_path).unwrap(); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .custom_flags(O_DIRECT) + .open(&file_path) + .unwrap(); let (atime1, mtime1, ctime1) = get_metadata_times(&file); assert_eq!(atime1, mtime1); @@ -249,14 +257,24 @@ fn write_fuse_fs() { let mount_point = Path::new(&fuse_fixture.mount_point); let file_path = mount_point.join("file"); - let mut file = File::create(&file_path).unwrap(); + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .custom_flags(O_DIRECT) + .open(&file_path) + .unwrap(); let mut data1 = vec![1u8; 2000]; let mut data2 = vec![2u8; 5000]; file.write_all(&data1).unwrap(); file.write_at(&data2, data1.len() as u64).unwrap(); - let mut file = OpenOptions::new().read(true).open(&file_path).unwrap(); + let mut file = OpenOptions::new() + .custom_flags(O_DIRECT) + .read(true) + .open(&file_path) + .unwrap(); data1.append(&mut data2); let mut actual = Vec::new(); file.read_to_end(&mut actual).unwrap(); @@ -269,7 +287,13 @@ fn different_data_writes() { let mount_point = Path::new(&fuse_fixture.mount_point); let file_path = mount_point.join("file"); - let mut file = File::create(&file_path).unwrap(); + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .custom_flags(O_DIRECT) + .open(&file_path) + .unwrap(); let mut data1 = vec![1u8; 500]; let mut data2 = vec![2u8; 700]; @@ -280,7 +304,11 @@ fn different_data_writes() { file.write_all(&data3).unwrap(); file.write_all(&data4).unwrap(); - let mut file = OpenOptions::new().read(true).open(&file_path).unwrap(); + let mut file = OpenOptions::new() + .custom_flags(O_DIRECT) + .read(true) + .open(&file_path) + .unwrap(); data1.append(&mut data2); data1.append(&mut vec![3u8; MB + 11]); let mut actual = vec![0u8; 500 + 700 + MB + 11]; From 542644ebb9408a760a772311744935d788dac3ec Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 26 Apr 2025 05:36:29 +0300 Subject: [PATCH 42/60] fuse tests: replace SimpleHasher with Sha256Hasher, add FuseFixture::with_chunker --- tests/fuse_filesystem.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 55b9cc7c..0f315a1b 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -1,6 +1,6 @@ use chunkfs::chunkers::SuperChunker; -use chunkfs::hashers::SimpleHasher; -use chunkfs::{FuseFS, MB}; +use chunkfs::hashers::Sha256Hasher; +use chunkfs::{ChunkerRef, FuseFS, MB}; use filetime::FileTime; use fuser::BackgroundSession; use fuser::MountOption::AutoUnmount; @@ -26,10 +26,17 @@ struct FuseFixture { impl FuseFixture { fn default() -> Self { + Self::with_chunker(SuperChunker::default()) + } + + fn with_chunker(chunker: C) -> Self + where + C: Into, + { let mount_dir = Path::new("mount_dir"); let mount_point = mount_dir.join(generate_unique_mount_point()); let db = HashMap::default(); - let fuse_fs = FuseFS::new(db, SimpleHasher, SuperChunker::default()); + let fuse_fs = FuseFS::new(db, Sha256Hasher::default(), chunker); fs::create_dir_all(&mount_point).unwrap(); let fuse_session = fuser::spawn_mount2(fuse_fs, &mount_point, &[AutoUnmount]).unwrap(); From c6de0566ae4d63334df5bdb3959e107254928410 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 26 Apr 2025 06:21:14 +0300 Subject: [PATCH 43/60] add fuse tests with boundary chunk reads Signed-off-by: Gleb Nasretdinov --- tests/fuse_filesystem.rs | 237 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 236 insertions(+), 1 deletion(-) diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 0f315a1b..768f17cb 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -1,4 +1,5 @@ -use chunkfs::chunkers::SuperChunker; +use cdc_chunkers::SizeParams; +use chunkfs::chunkers::{LeapChunker, SuperChunker}; use chunkfs::hashers::Sha256Hasher; use chunkfs::{ChunkerRef, FuseFS, MB}; use filetime::FileTime; @@ -480,3 +481,237 @@ fn concurrent_file_handles() { assert_eq!(actual, expected); assert_eq!(file.metadata().unwrap().len(), 12 * 3 * MB as u64); } + +#[test] +fn single_chunk_read() { + let fuse_fixture = FuseFixture::with_chunker(LeapChunker::new(SizeParams { + min: 1000, + avg: 1000, + max: 1000, + })); + let mount_point = Path::new(&fuse_fixture.mount_point); + + let file_path = mount_point.join("file"); + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .custom_flags(O_DIRECT) + .write(true) + .read(true) + .open(&file_path) + .unwrap(); + + file.write_all(&[0; 300]).unwrap(); + + let mut actual = vec![10; 100]; + assert_eq!(100, file.read_at(&mut actual, 100).unwrap()); + assert_eq!( + actual, [0; 100], + "read from start + epsilon to end - epsilon of a single chunk is correct" + ); + + actual = vec![10; 200]; + assert_eq!(200, file.read_at(&mut actual, 100).unwrap()); + assert_eq!( + actual, [0; 200], + "read from start + epsilon to end of a single chunk is correct" + ); + + actual = vec![10; 1000]; + assert_eq!(200, file.read_at(&mut actual, 100).unwrap()); + assert_eq!( + actual, + [vec![0; 200], vec![10; 800]].concat(), + "read from start + epsilon to end + epsilon of a single chunk is correct" + ); + + actual = vec![10; 1000]; + assert_eq!(0, file.read_at(&mut actual, 1000).unwrap()); + assert_eq!( + actual, [10; 1000], + "read from end to end + epsilon of a single chunk is correct" + ); +} + +#[test] +fn read_first_chunk_piece() { + let fuse_fixture = FuseFixture::with_chunker(LeapChunker::new(SizeParams { + min: 1000, + avg: 1000, + max: 1000, + })); + let mount_point = Path::new(&fuse_fixture.mount_point); + + let file_path = mount_point.join("file"); + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .custom_flags(O_DIRECT) + .write(true) + .read(true) + .open(&file_path) + .unwrap(); + + file.write_all(&[0; 1000]).unwrap(); + file.write_all(&[1; 4500]).unwrap(); + + let mut actual = vec![10; 40]; + assert_eq!(40, file.read_at(&mut actual, 0).unwrap()); + assert_eq!( + actual, [0; 40], + "read from start to end - epsilon of first chunk is correct" + ); + + actual = vec![10; 30]; + assert_eq!(30, file.read_at(&mut actual, 40).unwrap()); + assert_eq!( + actual, [0; 30], + "read from start + epsilon to end - epsilon of first chunk is correct" + ); + + actual = vec![10; 960]; + assert_eq!(960, file.read_at(&mut actual, 40).unwrap()); + assert_eq!( + actual, [0; 960], + "read from start + epsilon to end of first chunk is correct" + ); + + actual = vec![10; 60]; + assert_eq!(60, file.read_at(&mut actual, 970).unwrap()); + assert_eq!( + actual, + vec![[0; 30], [1; 30]].concat(), + "read from start + epsilon to end + epsilon of first chunk is correct" + ); + + actual = vec![10; 0]; + assert_eq!(0, file.read_at(&mut actual, 0).unwrap()); + assert_eq!( + actual, [0; 0], + "read zero bytes from start of first chunk if correct" + ); + + actual = vec![10; 0]; + assert_eq!(0, file.read_at(&mut actual, 1000).unwrap()); + assert_eq!( + actual, [0; 0], + "read zero bytes from end of first chunk if correct" + ); +} + +#[test] +fn read_middle_chunk_piece() { + let fuse_fixture = FuseFixture::with_chunker(LeapChunker::new(SizeParams { + min: 1000, + avg: 1000, + max: 1000, + })); + let mount_point = Path::new(&fuse_fixture.mount_point); + + let file_path = mount_point.join("file"); + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .custom_flags(O_DIRECT) + .write(true) + .read(true) + .open(&file_path) + .unwrap(); + + file.write_all(&[0; 5500]).unwrap(); + + let mut actual = vec![10; 40]; + assert_eq!(40, file.read_at(&mut actual, 1040).unwrap()); + assert_eq!( + actual, [0; 40], + "read from start + epsilon to end - epsilon of middle chunk is correct" + ); + + actual = vec![10; 0]; + assert_eq!(0, file.read_at(&mut actual, 1040).unwrap()); + assert_eq!( + actual, [0; 0], + "read zero bytes from start + epsilon of middle chunk is correct" + ); +} + +#[test] +fn read_last_chunk_piece() { + let fuse_fixture = FuseFixture::with_chunker(LeapChunker::new(SizeParams { + min: 1000, + avg: 1000, + max: 1000, + })); + let mount_point = Path::new(&fuse_fixture.mount_point); + + let file_path = mount_point.join("file"); + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .custom_flags(O_DIRECT) + .write(true) + .read(true) + .open(&file_path) + .unwrap(); + + file.write_all(&[0; 5000]).unwrap(); + file.write_all(&[1; 500]).unwrap(); + + let mut actual = vec![10; 40]; + assert_eq!(40, file.read_at(&mut actual, 5000).unwrap()); + assert_eq!( + actual, [1; 40], + "read from start to end - epsilon of last chunk is correct" + ); + + actual = vec![10; 30]; + assert_eq!(30, file.read_at(&mut actual, 5010).unwrap()); + assert_eq!( + actual, [1; 30], + "read from start + epsilon to end - epsilon of last chunk is correct" + ); + + actual = vec![10; 470]; + assert_eq!(470, file.read_at(&mut actual, 5030).unwrap()); + assert_eq!( + actual, [1; 470], + "read from start + epsilon to end of last chunk is correct" + ); + + actual = vec![10; 500]; + assert_eq!(470, file.read_at(&mut actual, 5030).unwrap()); + assert_eq!( + actual, + [vec![1; 470], vec![10; 30]].concat(), + "read from start + epsilon to end + epsilon of last chunk is correct" + ); + + actual = vec![10; 530]; + assert_eq!(500, file.read_at(&mut actual, 5000).unwrap()); + assert_eq!( + actual, + [vec![1; 500], vec![10; 30]].concat(), + "read from start to end + epsilon of last chunk is correct" + ); + + actual = vec![10; 0]; + assert_eq!(0, file.read_at(&mut actual, 5000).unwrap()); + assert_eq!( + actual, [0; 0], + "read zero bytes from start of last chunk if correct" + ); + + actual = vec![10; 0]; + assert_eq!(0, file.read_at(&mut actual, 5500).unwrap()); + assert_eq!( + actual, [0; 0], + "read zero bytes from end of last chunk if correct" + ); + + actual = vec![10; 40]; + assert_eq!(0, file.read_at(&mut actual, 5600).unwrap()); + assert_eq!( + actual, [10; 40], + "read n bytes from end + epsilon of last chunk if correct" + ); +} From a88e74a58d44c324634a27015622470fcfc73381 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 26 Apr 2025 06:45:56 +0300 Subject: [PATCH 44/60] add cache drop test Signed-off-by: Gleb Nasretdinov --- tests/fuse_filesystem.rs | 53 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 768f17cb..9b846e3e 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -341,6 +341,7 @@ fn read_dropped_cache() { let mut file = OpenOptions::new() .create(true) .truncate(true) + .custom_flags(O_DIRECT) .write(true) .read(true) .open(&file_path) @@ -400,6 +401,7 @@ fn read_cache() { .create(true) .truncate(true) .write(true) + .custom_flags(O_DIRECT) .read(true) .open(&file_path) .unwrap(); @@ -455,9 +457,21 @@ fn concurrent_file_handles() { let file_path = mount_point.join("file"); File::create(&file_path).unwrap(); - let handle1 = OpenOptions::new().append(true).open(&file_path).unwrap(); - let handle2 = OpenOptions::new().append(true).open(&file_path).unwrap(); - let handle3 = OpenOptions::new().append(true).open(&file_path).unwrap(); + let handle1 = OpenOptions::new() + .append(true) + .custom_flags(O_DIRECT) + .open(&file_path) + .unwrap(); + let handle2 = OpenOptions::new() + .append(true) + .custom_flags(O_DIRECT) + .open(&file_path) + .unwrap(); + let handle3 = OpenOptions::new() + .append(true) + .custom_flags(O_DIRECT) + .open(&file_path) + .unwrap(); for _ in 0..12 { handle1 .write_all_at(&vec![1; MB], file_size(&handle1)) @@ -482,6 +496,39 @@ fn concurrent_file_handles() { assert_eq!(file.metadata().unwrap().len(), 12 * 3 * MB as u64); } +#[test] +fn offset_change_not_affects_cache_drop() { + let fuse_fixture = FuseFixture::default(); + let mount_point = Path::new(&fuse_fixture.mount_point); + + let file_path = mount_point.join("file"); + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .custom_flags(O_DIRECT) + .write(true) + .read(true) + .open(&file_path) + .unwrap(); + file.write_all(&[0; 3 * MB]).unwrap(); + assert_eq!(MB, file.read_at(&mut [0; MB], 2 * MB as u64).unwrap()); + drop(file); + + let mut file = OpenOptions::new() + .custom_flags(O_DIRECT) + .read(true) + .append(true) + .open(&file_path) + .unwrap(); + file.write_all(&[0; 3 * MB]).unwrap(); + file.flush().unwrap(); + + let mut actual = vec![10; 6 * MB]; + assert_eq!(file.read_at(&mut actual, 0).unwrap(), 6 * MB); + assert_eq!(actual, [0; 6 * MB]); + assert_eq!(file.metadata().unwrap().len(), 6 * MB as u64); +} + #[test] fn single_chunk_read() { let fuse_fixture = FuseFixture::with_chunker(LeapChunker::new(SizeParams { From a246e1e4ba476ae320b380f640a1527e5ad42714 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 26 Apr 2025 06:47:46 +0300 Subject: [PATCH 45/60] some refactor Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 9 ++++++++- tests/fuse_filesystem.rs | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index edb238e6..276d8b3c 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -218,7 +218,14 @@ where Ok(()) } - fn flush(&mut self, _req: &Request<'_>, ino: u64, fh: u64, lock_owner: u64, reply: ReplyEmpty) { + fn flush( + &mut self, + _req: &Request<'_>, + ino: u64, + fh: u64, + _lock_owner: u64, + reply: ReplyEmpty, + ) { let Some(file_handle) = self.file_handles.get_mut(&fh) else { reply.error(libc::EBADF); return; diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 9b846e3e..6063744b 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -442,7 +442,7 @@ fn read_cache() { ); actual = vec![10; MB]; - assert_eq!(file.read_at(&mut actual, 11 * MB as u64).unwrap(), 1 * MB); + assert_eq!(file.read_at(&mut actual, 11 * MB as u64).unwrap(), MB); assert_eq!( actual, [1; MB], "read cache from start + epsilon to end - epsilon is correct" @@ -627,7 +627,7 @@ fn read_first_chunk_piece() { assert_eq!(60, file.read_at(&mut actual, 970).unwrap()); assert_eq!( actual, - vec![[0; 30], [1; 30]].concat(), + [[0; 30], [1; 30]].concat(), "read from start + epsilon to end + epsilon of first chunk is correct" ); From 933a8e821b4ce9b16111780defce0a6305fe270c Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 1 May 2025 03:29:47 +0300 Subject: [PATCH 46/60] ref: typos, impl order, spaces Signed-off-by: Gleb Nasretdinov --- src/lib.rs | 6 +-- src/system/database.rs | 2 +- src/system/file_layer.rs | 9 +++-- src/system/fuse_filesystem.rs | 70 +++++++++++++++++------------------ src/system/scrub.rs | 6 +-- src/system/storage.rs | 12 +++--- tests/filesystem.rs | 6 +-- tests/fuse_filesystem.rs | 4 +- 8 files changed, 59 insertions(+), 56 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7ffc0201..6697ff1b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,14 +73,14 @@ impl Chunk { /// If some contents were cut because the end of `data` and not the end of the chunk was reached, /// it must be returned with [`rest`][Chunker::rest] instead of storing it in the [`chunk_data`][Chunker::chunk_data]'s output. pub trait Chunker: Debug + Send { - /// Goes through whole `data` and finds chunks. If last chunk is not actually a chunk but a leftover, + /// Goes through whole `data` and finds chunks. If the last chunk is not actually a chunk but a leftover, /// it is returned via [`rest`][Chunker::rest] method and is not contained in the vector. /// /// `empty` is an empty vector whose capacity is determined by [`estimate_chunk_count`][Chunker::estimate_chunk_count]. - /// Resulting chunks should be written right to it, and it should be returned as result. + /// Resulting chunks should be written right to it, and it should be returned as a result. fn chunk_data(&mut self, data: &[u8], empty: Vec) -> Vec; - /// Returns an estimate amount of chunks that will be created once the algorithm runs through the whole + /// Returns an estimate number of chunks that will be created once the algorithm runs through the whole /// data buffer. Used to pre-allocate the buffer with the required size so that allocation times are not counted /// towards total chunking time. fn estimate_chunk_count(&self, data: &[u8]) -> usize; diff --git a/src/system/database.rs b/src/system/database.rs index c77a6a24..ca9ff46e 100644 --- a/src/system/database.rs +++ b/src/system/database.rs @@ -14,7 +14,7 @@ pub trait Database { /// Retrieves a value by a given key. Note that it returns a value, not a reference. /// /// # Errors - /// Should return [ErrorKind::NotFound], if the key-value pair + /// Should return [ErrorKind::NotFound] if the key-value pair /// was not found in the storage. fn get(&self, key: &K) -> io::Result; diff --git a/src/system/file_layer.rs b/src/system/file_layer.rs index 9a2ba98f..db02059e 100644 --- a/src/system/file_layer.rs +++ b/src/system/file_layer.rs @@ -98,7 +98,7 @@ impl FileHandle { &self.file_name } - /// Closes handle and returns [`WriteMeasurements`] made while file was open. + /// Closes the handle and returns [`WriteMeasurements`] made while a file was open. pub(crate) fn close(self) -> WriteMeasurements { self.measurements } @@ -170,8 +170,8 @@ impl FileLayer { } /// Reads the specified amount of data from the open file and - /// returns FileSpans in which the necessary data is stored (the side FileSpans may contain it partially). - /// Starting point is based on the `FileHandle`'s offset. + /// returns FileSpans, in which the necessary data is stored (the side FileSpans may contain it partially). + /// The Starting point is based on the `FileHandle`'s offset. /// /// If `size` + file handle offset is greater than file size, then returns FileSpans up to the end of the file. pub fn read(&self, handle: &mut FileHandle, size: usize) -> Vec<&FileSpan> { @@ -226,7 +226,8 @@ impl FileLayer { } #[cfg(feature = "bench")] - /// Generate a new dataset with set deduplication ratio from the existing one. + /// Generate a new dataset with + /// a set deduplication ratio from the existing one. /// /// Returns the name of the new file. pub fn get_to_dedup_ratio(&mut self, name: &str, dedup_ratio: f64) -> io::Result { diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 276d8b3c..754c9249 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -19,7 +19,7 @@ use std::time::{Duration, SystemTime}; type Inode = u64; type Fh = u64; -/// File is opened for execution. +/// The File is opened for execution. const FMODE_EXEC: i32 = 0x20; const FILESYSTEM_CACHE_MAX_SIZE: usize = 25 * MB; const FILE_CACHE_MAX_SIZE: usize = 5 * MB; @@ -178,13 +178,13 @@ fn check_access(file_attr: &FileAttr, req: &Request, access_mask: i32) -> bool { let gid = req.gid(); let mut access_mask = access_mask; - // F_OK tests for existence of file + // F_OK tests for existence of a file if access_mask == libc::F_OK { return true; } let file_mode = i32::from(file_mode); - // root is allowed to read & write anything + // root is allowed to read and write anything if uid == 0 { // root only allowed to exec if one of the Exec bits is set access_mask &= libc::X_OK; @@ -218,38 +218,6 @@ where Ok(()) } - fn flush( - &mut self, - _req: &Request<'_>, - ino: u64, - fh: u64, - _lock_owner: u64, - reply: ReplyEmpty, - ) { - let Some(file_handle) = self.file_handles.get_mut(&fh) else { - reply.error(libc::EBADF); - return; - }; - if file_handle.inode != ino { - reply.error(libc::ESTALE); - return; - } - let Some(_) = self.files.get_mut(&ino) else { - reply.error(libc::ENOENT); - return; - }; - if !file_handle.write && !file_handle.read { - reply.error(libc::EACCES); - return; - } - - if self.drop_cache(ino, fh).is_err() { - reply.error(libc::EIO); - return; - } - reply.ok() - } - fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) { let name = name.to_str().unwrap().to_owned(); if parent != 1 { @@ -532,6 +500,38 @@ where reply.written(data.len() as u32); } + fn flush( + &mut self, + _req: &Request<'_>, + ino: u64, + fh: u64, + _lock_owner: u64, + reply: ReplyEmpty, + ) { + let Some(file_handle) = self.file_handles.get_mut(&fh) else { + reply.error(libc::EBADF); + return; + }; + if file_handle.inode != ino { + reply.error(libc::ESTALE); + return; + } + let Some(_) = self.files.get_mut(&ino) else { + reply.error(libc::ENOENT); + return; + }; + if !file_handle.write && !file_handle.read { + reply.error(libc::EACCES); + return; + } + + if self.drop_cache(ino, fh).is_err() { + reply.error(libc::EIO); + return; + } + reply.ok() + } + fn release( &mut self, _req: &Request<'_>, diff --git a/src/system/scrub.rs b/src/system/scrub.rs index cd48fbc4..7dd32080 100644 --- a/src/system/scrub.rs +++ b/src/system/scrub.rs @@ -18,7 +18,7 @@ use super::storage::DataContainer; /// /// After moving the data from `database` to `target_map`, we should be able to have access to it via the `database`. /// Therefore, after moving, we should leave a `Vec` in place of the source chunk. It is done via [DataContainer::make_target] method. -/// Not using it will lead to either not getting any benefits from the algorithm, or to being unable to access the initial chunk anymore, if it was deleted. +/// Not using it will lead to either not getting any benefits from the algorithm or to being unable to access the initial chunk anymore if it is deleted. /// /// # Arguments /// The only method [scrub][Scrub::scrub] takes two arguments: @@ -40,7 +40,7 @@ where /// [DataContainer::extract] or [DataContainer::extract_mut] should be used. /// /// If the chunk is suitable for being transferred to the `target_map`, it should NOT be deleted, but instead be replaced by the `target_map`'s keys, - /// using which the original chunk can be restored. This is accomplished by the [DataContainer::make_target] method. + /// using which the original chunk can be restored. This is achieved by the [DataContainer::make_target] method. /// /// It should also gather information to return the [measurements][ScrubMeasurements]. /// @@ -55,7 +55,7 @@ where /// /// # CDC Database /// We should be able to iterate over the `database` to process all chunks we had stored before. - /// The [IntoIterator] trait should be implemented for `database`, but it should not be a big concern, because the only structure that should be implemented + /// The [IntoIterator] trait should be implemented for `database`, but it should not be a big concern because the only structure that should be implemented /// for the algorithm is the scrubber itself. `database` should be considered a given entity, along with the `target_map`. fn scrub<'a>(&mut self, database: &mut B, target_map: &mut T) -> io::Result where diff --git a/src/system/storage.rs b/src/system/storage.rs index df8f2740..ffe242af 100644 --- a/src/system/storage.rs +++ b/src/system/storage.rs @@ -73,8 +73,8 @@ where /// Writes 1 MB of data to the [`base`][crate::base::Base] storage after deduplication. /// - /// Returns resulting lengths of [chunks][crate::chunker::Chunk] with corresponding hash, - /// along with amount of time spent on chunking and hashing. + /// Returns the resulting lengths of [chunks][crate::chunker::Chunk] with the corresponding hash, + /// along with the amount of time spent on chunking and hashing. pub fn write(&mut self, data: &[u8], chunker: &ChunkerRef) -> io::Result>> { let mut writer = StorageWriter::new(chunker, &mut self.hasher); @@ -137,7 +137,7 @@ where } /// Retrieves the data from the storage based on hashes of the data [`segments`][Segment], - /// or Error(NotFound) if some of the hashes were not present in the base. + /// or Error(NotFound) if some hashes were not present in the base. pub fn retrieve(&self, request: &[&Hash]) -> io::Result>> { let retrieved = self.database.get_multi(request)?; @@ -199,7 +199,7 @@ where }) } - /// Calculates deduplication ratio of the storage, not accounting for chunks processed with scrubber. + /// Calculates a deduplication ratio of the storage, not accounting for chunks processed with scrubber. pub fn cdc_dedup_ratio(&self) -> f64 { (self.size_written as f64) / (self.total_cdc_size() as f64) } @@ -297,8 +297,8 @@ where /// Writes 1 MB of data to the [`base`][crate::base::Base] storage after deduplication. /// - /// Returns resulting lengths of [chunks][crate::chunker::Chunk] with corresponding hash, - /// along with amount of time spent on chunking and hashing. + /// Returns the resulting lengths of [chunks][crate::chunker::Chunk] with the corresponding hash, + /// along with the amount of time spent on chunking and hashing. fn write>>( &mut self, data: &[u8], diff --git a/tests/filesystem.rs b/tests/filesystem.rs index 3a659984..e8c37922 100644 --- a/tests/filesystem.rs +++ b/tests/filesystem.rs @@ -91,7 +91,7 @@ fn write_read_blocks_test() { buffer.extend_from_slice(&buf); } assert_eq!(buffer.len(), MB * 3 + 50); - assert!(complete == buffer); + assert_eq!(complete, buffer); assert_eq!(fs.read_from_file(&mut handle).unwrap(), []); } @@ -147,11 +147,11 @@ fn non_iterable_database_can_be_used_with_fs() { struct DummyDatabase; impl Database, DataContainer<()>> for DummyDatabase { - fn insert(&mut self, _key: Vec, _value: DataContainer<()>) -> std::io::Result<()> { + fn insert(&mut self, _key: Vec, _value: DataContainer<()>) -> io::Result<()> { unimplemented!() } - fn get(&self, _key: &Vec) -> std::io::Result> { + fn get(&self, _key: &Vec) -> io::Result> { unimplemented!() } diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 6063744b..098c875f 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -76,6 +76,7 @@ fn get_metadata_times(file: &File) -> (u64, u64, u64) { metadata.ctime() as u64, ) } + #[test] fn metadata_times() { let fuse_fixture = FuseFixture::default(); @@ -147,6 +148,7 @@ fn manual_setattr() { assert_eq!(mtime2, to_unix_secs(&now_minus100s)); assert!(ctime2 > ctime1); } + #[test] fn readdir() { let fuse_fixture = FuseFixture::default(); @@ -497,7 +499,7 @@ fn concurrent_file_handles() { } #[test] -fn offset_change_not_affects_cache_drop() { +fn offset_change_does_not_affect_cache_drop() { let fuse_fixture = FuseFixture::default(); let mount_point = Path::new(&fuse_fixture.mount_point); From 96f46fe9897a5c71f441a2c4f70ff810e90a581c Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 1 May 2025 06:09:03 +0300 Subject: [PATCH 47/60] test: change error assertion to concrete error type Signed-off-by: Gleb Nasretdinov --- tests/fuse_filesystem.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 098c875f..b4c83638 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -185,7 +185,7 @@ fn permissions() { }; let read_denied = || { let res = OpenOptions::new().read(true).open(&file_path); - assert!(res.is_err()); + assert_eq!(res.unwrap_err().kind(), std::io::ErrorKind::PermissionDenied); }; let write_ok = || { let file = OpenOptions::new().write(true).open(&file_path).unwrap(); @@ -194,7 +194,7 @@ fn permissions() { }; let write_denied = || { let res = OpenOptions::new().write(true).open(&file_path); - assert!(res.is_err()); + assert_eq!(res.unwrap_err().kind(), std::io::ErrorKind::PermissionDenied); }; let perms: Vec<_> = (0o000..=0o777).map(Permissions::from_mode).collect(); @@ -221,7 +221,7 @@ fn create_dir_fails() { let dir_path = mount_point.join("directory"); let res = fs::create_dir(&dir_path); - assert!(res.is_err()); + assert_eq!(res.unwrap_err().raw_os_error(), Some(libc::ENOSYS)); } #[test] From f646ab97436265f7abc64a72ed7ccada856eaa5e Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 1 May 2025 06:15:28 +0300 Subject: [PATCH 48/60] fix readdir test Signed-off-by: Gleb Nasretdinov --- tests/fuse_filesystem.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index b4c83638..cd248d72 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -165,7 +165,7 @@ fn readdir() { files.push(path.file_name().unwrap().to_owned()); } assert!(files.contains(&OsString::from("file1"))); - assert!(files.contains(&OsString::from("file1"))); + assert!(files.contains(&OsString::from("file2"))); assert_eq!(files.len(), 2) } From f055e5c4063e779c3726b133968c44da766bec51 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 1 May 2025 06:56:19 +0300 Subject: [PATCH 49/60] fix lookup permission check: FUSE was sendind EIO on incorrect daemon behaviour Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 8 ++++- tests/fuse_filesystem.rs | 57 +++++++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 754c9249..1fbf6a5c 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -218,7 +218,7 @@ where Ok(()) } - fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) { + fn lookup(&mut self, req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) { let name = name.to_str().unwrap().to_owned(); if parent != 1 { reply.error(libc::EINVAL); @@ -229,6 +229,12 @@ where reply.error(libc::ENOENT); return; }; + let parent_attr = self.files.get(&parent).unwrap().attr; + if !check_access(&parent_attr, req, libc::X_OK) { + reply.error(libc::EACCES); + return; + } + let file = self.files.get(inode).unwrap(); reply.entry(&Duration::new(0, 0), &file.attr, file.generation) } diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index cd248d72..59f1d042 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -185,7 +185,10 @@ fn permissions() { }; let read_denied = || { let res = OpenOptions::new().read(true).open(&file_path); - assert_eq!(res.unwrap_err().kind(), std::io::ErrorKind::PermissionDenied); + assert_eq!( + res.unwrap_err().kind(), + std::io::ErrorKind::PermissionDenied + ); }; let write_ok = || { let file = OpenOptions::new().write(true).open(&file_path).unwrap(); @@ -194,7 +197,10 @@ fn permissions() { }; let write_denied = || { let res = OpenOptions::new().write(true).open(&file_path); - assert_eq!(res.unwrap_err().kind(), std::io::ErrorKind::PermissionDenied); + assert_eq!( + res.unwrap_err().kind(), + std::io::ErrorKind::PermissionDenied + ); }; let perms: Vec<_> = (0o000..=0o777).map(Permissions::from_mode).collect(); @@ -764,3 +770,50 @@ fn read_last_chunk_piece() { "read n bytes from end + epsilon of last chunk if correct" ); } + +#[test] +fn lookup_permission() { + let fuse_fixture = FuseFixture::default(); + let mount_point = Path::new(&fuse_fixture.mount_point); + File::create(mount_point.join("file1")).unwrap(); + File::create(mount_point.join("file2")).unwrap(); + + let lookup_ok = || { + let mut files = vec![]; + + for entry in fs::read_dir(mount_point).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + assert!(path.is_file()); + let _ = entry.metadata().unwrap(); + files.push(path.file_name().unwrap().to_str().unwrap().to_string()); + } + assert!(files.contains(&"file1".to_string())); + assert!(files.contains(&"file2".to_string())); + assert_eq!(files.len(), 2); + }; + let lookup_denied = || { + for entry in fs::read_dir(mount_point).unwrap() { + let metadata = entry.unwrap().metadata(); + assert_eq!( + metadata.unwrap_err().kind(), + std::io::ErrorKind::PermissionDenied + ); + } + }; + + let perms: Vec<_> = (0o000..=0o777).map(Permissions::from_mode).collect(); + for perm in perms { + // reading mount directory is handled by upper filesystem + if perm.mode() & 0o400 == 0 { + continue; + } + + fs::set_permissions(mount_point, perm.clone()).unwrap(); + if perm.mode() & 0o100 != 0 { + lookup_ok(); + } else { + lookup_denied(); + } + } +} From fe221aeec7d22d06fab9b6efd38a516df5efc128 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 1 May 2025 07:57:09 +0300 Subject: [PATCH 50/60] add more docs to FuseFS Signed-off-by: Gleb Nasretdinov --- src/system/file_layer.rs | 2 +- src/system/fuse_filesystem.rs | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/system/file_layer.rs b/src/system/file_layer.rs index db02059e..3a7189f9 100644 --- a/src/system/file_layer.rs +++ b/src/system/file_layer.rs @@ -226,7 +226,7 @@ impl FileLayer { } #[cfg(feature = "bench")] - /// Generate a new dataset with + /// Generate a new dataset with /// a set deduplication ratio from the existing one. /// /// Returns the name of the new file. diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 1fbf6a5c..30434f4e 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -21,23 +21,38 @@ type Fh = u64; /// The File is opened for execution. const FMODE_EXEC: i32 = 0x20; +/// Total cache size of all files, after which all caches will be [`dropped and shrank`][FuseFS::drop_and_shrink_cache] to the [`underlying filesystem`][FileSystem]. const FILESYSTEM_CACHE_MAX_SIZE: usize = 25 * MB; +/// Maximum cache size, after which it will drop [`dropped and shrank`][FuseFS::drop_and_shrink_cache] to the [`underlying filesystem`][FileSystem]. const FILE_CACHE_MAX_SIZE: usize = 5 * MB; +/// [`FuseFS`] file entity. #[derive(Clone)] struct FuseFile { + /// File cache. Cache fills up with every [`write`][FuseFS::write] on a file's opened [`file handles`][FuseFileHandle], + /// until it reaches its [`maximum size`][FILE_CACHE_MAX_SIZE], + /// or the total size of the file system caches reaches the [`maximum size`][FILESYSTEM_CACHE_MAX_SIZE]. cache: Vec, + /// File attributes. attr: FileAttr, + /// File name. name: String, + /// Generation of a file. Increments every time the file content is modified. generation: u64, + /// Number of opened file handles. handles: u64, } +/// [`FuseFS`] file handle entity. struct FuseFileHandle { + /// Underlying file handle. underlying_file_handle: FileHandle, + /// Read permission. read: bool, + /// Write permission. write: bool, - inode: u64, + /// Inode number with which the file handle is associated. + inode: Inode, } /// Wrap around [`FileSystem`] for implementing [`Filesystem`] trait. @@ -48,13 +63,19 @@ where B: Database>, Hash: ChunkHash, { + /// Underlying [chunkfs][`FileSystem`]. underlying_fs: FileSystem>>, + /// Files map <[`Inode`], [`FuseFile`]>. files: HashMap, + /// File handles map <[`Fh`], [`FuseFileHandle`]>. + file_handles: HashMap, + /// Inodes map . inodes: HashMap, - /// Number for the next created file handle. + /// Number for the next file handle to be created. Increments on every [`FuseFS::open`]. next_fh: u64, - file_handles: HashMap, + /// Chunker set for the filesystem. chunker: ChunkerRef, + /// Total cache size of opened file handles. total_cache: usize, } @@ -117,16 +138,20 @@ where } } + /// Get new inode number for a new file. After creation, the new inode is inserted into the inodes map, + /// incrementing the next new inode number. The filesystem does not provide a file removing, so the inodes do not overlap. fn get_new_inode(&self) -> Inode { self.inodes.len() as Inode } + /// Get new file handle number for a new file handle. Increments next fh counter on every call. fn get_new_fh(&mut self) -> Fh { let next_fh = self.next_fh; self.next_fh += 1; next_fh } + /// Writes cache content to the [`underlying filesystem`][FileSystem] and shrinks its size. fn drop_and_shrink_cache(&mut self, file: Inode, handle: Fh) -> io::Result<()> { self.drop_cache(file, handle)?; @@ -136,6 +161,7 @@ where Ok(()) } + /// Writes cache content to the [`underlying filesystem`][FileSystem]. fn drop_cache(&mut self, file: Inode, handle: Fh) -> io::Result<()> { let file = self.files.get_mut(&file).ok_or(io::ErrorKind::NotFound)?; let handle = self @@ -153,6 +179,9 @@ where Ok(()) } + /// Writes all file caches content to the [`underlying filesystem`][FileSystem] and shrinks its size. + /// + /// Called after the [`maximum filesystem cache size`][FILESYSTEM_CACHE_MAX_SIZE] has been reached. fn drop_and_shrink_caches(&mut self) -> io::Result<()> { for handle in self.file_handles.values_mut() { let file = self From e843aa1d3ccf05920b261e0b223a5cbad190b323 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Fri, 2 May 2025 08:39:05 +0300 Subject: [PATCH 51/60] fix EIO FUSE error causing by incorrect file generation update on every file's content change Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 7 +------ tests/fuse_filesystem.rs | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 30434f4e..76ee20be 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -37,8 +37,6 @@ struct FuseFile { attr: FileAttr, /// File name. name: String, - /// Generation of a file. Increments every time the file content is modified. - generation: u64, /// Number of opened file handles. handles: u64, } @@ -118,7 +116,6 @@ where cache: Vec::new(), attr: root_attr, name: ".".to_string(), - generation: 0, handles: 0, }; let mut parent_dir = root_dir.clone(); @@ -265,7 +262,7 @@ where } let file = self.files.get(inode).unwrap(); - reply.entry(&Duration::new(0, 0), &file.attr, file.generation) + reply.entry(&Duration::new(0, 0), &file.attr, 0) } fn getattr(&mut self, _req: &Request<'_>, ino: u64, _fh: Option, reply: ReplyAttr) { @@ -530,7 +527,6 @@ where let file = self.files.get_mut(&ino).unwrap(); file.attr.ctime = now; file.attr.mtime = now; - file.generation += 1; reply.written(data.len() as u32); } @@ -701,7 +697,6 @@ where cache: Vec::new(), attr, name: name.clone(), - generation: 0, handles: 1, }; diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index 59f1d042..dc392f60 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -804,7 +804,7 @@ fn lookup_permission() { let perms: Vec<_> = (0o000..=0o777).map(Permissions::from_mode).collect(); for perm in perms { - // reading mount directory is handled by upper filesystem + // reading mount directory is handled by the upper filesystem if perm.mode() & 0o400 == 0 { continue; } @@ -817,3 +817,25 @@ fn lookup_permission() { } } } + +#[test] +fn concurrent_write_and_lookup() { + let fuse_fixture = FuseFixture::default(); + let mount_point = Path::new(&fuse_fixture.mount_point); + let file_path = mount_point.join("file"); + + let mut file = File::create(&file_path).unwrap(); + let handle = std::thread::spawn(move || { + for _ in 0..10 { + std::thread::sleep(std::time::Duration::from_millis(10)); + let _ = File::open(&file_path).unwrap().metadata().unwrap(); + } + }); + let buf = vec![0; 5 * MB]; + for _ in 0..10 { + file.write_all(&buf).unwrap() + } + + handle.join().unwrap(); + assert_eq!(file.metadata().unwrap().len(), 50 * MB as u64); +} From 72afbafb0c327db0500368b42e445ce59c3d7433 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Fri, 2 May 2025 08:46:09 +0300 Subject: [PATCH 52/60] move reply in FuseFS::create() after all filesystem updates to avoid possible inconsistency Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 76ee20be..47054aa5 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -701,10 +701,10 @@ where }; let fh = self.get_new_fh(); - reply.created(&Duration::new(0, 0), &file.attr, 0, fh, flags as u32); - - self.files.insert(ino, file); + self.files.insert(ino, file.clone()); self.inodes.insert(name, ino); self.file_handles.insert(fh, file_handle); + + reply.created(&Duration::new(0, 0), &file.attr, 0, fh, flags as u32); } } From 9d579f477f10a0a798e36c3b643bcdd0e34ab24e Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Fri, 2 May 2025 08:49:49 +0300 Subject: [PATCH 53/60] add O_DIRECT flag to fuse benches Signed-off-by: Gleb Nasretdinov --- benches/fuse_write_read.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/benches/fuse_write_read.rs b/benches/fuse_write_read.rs index 4574ece6..0b45a29c 100644 --- a/benches/fuse_write_read.rs +++ b/benches/fuse_write_read.rs @@ -5,10 +5,12 @@ use chunkfs::{ChunkerRef, FuseFS, MB}; use criterion::measurement::WallTime; use criterion::{BatchSize, BenchmarkGroup, BenchmarkId, Criterion, Throughput}; use fuser::MountOption::AutoUnmount; +use libc::O_DIRECT; use std::collections::HashMap; use std::fs; use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; +use std::os::unix::fs::OpenOptionsExt; use std::path::Path; const SAMPLE_SIZE: usize = 30; @@ -106,6 +108,7 @@ fn bench_write( .write(true) .read(true) .create(true) + .custom_flags(O_DIRECT) .truncate(true) .open(&fuse_path) .unwrap(); @@ -170,7 +173,13 @@ fn bench_read( let parameter = format!("read_fuse-{:?}-{}", algorithm, params); group.bench_function(BenchmarkId::new(bench_name, parameter), |b| { b.iter_batched( - || File::open(&fuse_path).unwrap(), + || { + OpenOptions::new() + .read(true) + .custom_flags(O_DIRECT) + .open(&fuse_path) + .unwrap() + }, |mut fuse_file| { let mut buf = vec![0; 50 * MB]; loop { From f1f0ec5b8f543a0c701eb2fa23077cdfa17ac557 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 8 May 2025 15:46:42 +0300 Subject: [PATCH 54/60] fix FuseFS: total cache was not increasing in write() Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 47054aa5..9f7e27a7 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -511,9 +511,13 @@ where reply.error(libc::EACCES); return; } - + + let cache_cap_before = file.cache.capacity(); file.cache.extend_from_slice(data); + let cache_cap_after = file.cache.capacity(); + self.total_cache += cache_cap_after - cache_cap_before; file.attr.size += data.len() as u64; + if file.cache.len() > FILE_CACHE_MAX_SIZE && self.drop_cache(ino, fh).is_err() { reply.error(libc::EIO); return; From 8cb093939d518aa749e185db8f5213d8ed60e04a Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 8 May 2025 15:47:22 +0300 Subject: [PATCH 55/60] add dedup ratio and avg chunk size request for FuseFS via ioctl Signed-off-by: Gleb Nasretdinov --- Cargo.lock | 15 +++++++- Cargo.toml | 1 + benches/fuse_write_read.rs | 61 ++++++++++++++++++++++++++++++++- src/lib.rs | 2 +- src/system/fuse_filesystem.rs | 64 +++++++++++++++++++++++++++++------ tests/fuse_filesystem.rs | 44 +++++++++++++++++++++++- 6 files changed, 173 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ead882c..6ef0bf46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,6 +192,7 @@ dependencies = [ "fuser", "itertools 0.14.0", "libc", + "nix 0.30.1", "rand 0.9.0", "serde", "serde_json", @@ -517,7 +518,7 @@ dependencies = [ "libc", "log", "memchr", - "nix", + "nix 0.29.0", "page_size", "pkg-config", "smallvec", @@ -762,6 +763,18 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num-conv" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3317c527..4510b650 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ serde_with = { version = "3", optional = true } chrono = { version = "0.4", optional = true, features = ["serde"] } libc = "0.2.171" fuser = { version = "0.15.1", features = ["abi-7-28"] } +nix = { version = "0.30.1", features = ["ioctl"] } [features] chunkers = ["cdc-chunkers", "fastcdc"] diff --git a/benches/fuse_write_read.rs b/benches/fuse_write_read.rs index 0b45a29c..6c3a3712 100644 --- a/benches/fuse_write_read.rs +++ b/benches/fuse_write_read.rs @@ -1,7 +1,7 @@ use cdc_chunkers::SizeParams; use chunkfs::chunkers::{LeapChunker, RabinChunker, SuperChunker, UltraChunker}; use chunkfs::hashers::Sha256Hasher; -use chunkfs::{ChunkerRef, FuseFS, MB}; +use chunkfs::{ChunkerRef, FuseFS, IOC_GET_AVG_CHUNK_SIZE, IOC_GET_DEDUP_RATIO, MB}; use criterion::measurement::WallTime; use criterion::{BatchSize, BenchmarkGroup, BenchmarkId, Criterion, Throughput}; use fuser::MountOption::AutoUnmount; @@ -10,6 +10,7 @@ use std::collections::HashMap; use std::fs; use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; +use std::os::fd::AsRawFd; use std::os::unix::fs::OpenOptionsExt; use std::path::Path; @@ -72,6 +73,11 @@ pub fn bench(c: &mut Criterion) { group.sample_size(SAMPLE_SIZE); group.throughput(Throughput::Bytes(dataset.size)); + for chunker in chunkers() { + let params = get_default_sizes(chunker); + dedup_ratio_and_avg_chunk(&dataset, chunker, params); + } + for chunker in chunkers() { let params = get_default_sizes(chunker); bench_write(&dataset, &mut group, chunker, params); @@ -84,6 +90,58 @@ pub fn bench(c: &mut Criterion) { } } +fn dedup_ratio_and_avg_chunk(dataset: &Dataset, algorithm: Algorithms, params: SizeParams) { + let mount_point = Path::new("mount_dir/mount_point"); + let db = HashMap::default(); + let chunker = get_chunker(algorithm, params); + let fuse_fs = FuseFS::new(db, Sha256Hasher::default(), chunker); + + fs::create_dir_all(mount_point).unwrap(); + let session = fuser::spawn_mount2(fuse_fs, mount_point, &[AutoUnmount]).unwrap(); + + let fuse_path = mount_point.join("file"); + let mut fuse_file = OpenOptions::new() + .write(true) + .read(true) + .create(true) + .custom_flags(O_DIRECT) + .truncate(true) + .open(&fuse_path) + .unwrap(); + + let mut source = File::open(&dataset.filename).unwrap(); + let mut buf = vec![0; 50 * MB]; + loop { + let bytes_read = source.read(&mut buf).unwrap(); + if bytes_read == 0 { + break; + } + fuse_file.write_all(&buf[..bytes_read]).unwrap(); + } + fuse_file.flush().unwrap(); + + let mut dedup_ratio = [0u8; size_of::()]; + let mut avg_chunk_size = [0u8; size_of::()]; + unsafe { libc::ioctl(fuse_file.as_raw_fd(), IOC_GET_DEDUP_RATIO, &mut dedup_ratio) }; + unsafe { + libc::ioctl( + fuse_file.as_raw_fd(), + IOC_GET_AVG_CHUNK_SIZE, + &mut avg_chunk_size, + ) + }; + + let dedup_ratio = f64::from_ne_bytes(dedup_ratio); + let avg_chunk_size = usize::from_ne_bytes(avg_chunk_size); + println!("{:?}-{} dedup ratio: {:.2}", algorithm, params, dedup_ratio); + println!( + "{:?}-{} average chunk size: {}", + algorithm, params, avg_chunk_size + ); + + drop(session) +} + fn bench_write( dataset: &Dataset, group: &mut BenchmarkGroup, @@ -126,6 +184,7 @@ fn bench_write( } fuse_file.write_all(&buf[..bytes_read]).unwrap(); } + fuse_file.flush().unwrap(); drop(fuse_file); }, BatchSize::PerIteration, diff --git a/src/lib.rs b/src/lib.rs index 6697ff1b..811d2830 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; pub use system::database::{Database, IterableDatabase}; -pub use system::fuse_filesystem::FuseFS; +pub use system::fuse_filesystem::{FuseFS, IOC_GET_AVG_CHUNK_SIZE, IOC_GET_DEDUP_RATIO}; pub use system::scrub::{CopyScrubber, Scrub, ScrubMeasurements}; pub use system::storage::{Data, DataContainer}; pub use system::{create_cdc_filesystem, FileSystem}; diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 9f7e27a7..c94956e3 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -1,13 +1,14 @@ use crate::system::file_layer::FileHandle; use crate::{ - create_cdc_filesystem, ChunkHash, ChunkerRef, DataContainer, Database, FileSystem, Hasher, MB, + create_cdc_filesystem, ChunkHash, ChunkerRef, DataContainer, FileSystem, Hasher, + IterableDatabase, MB, }; use fuser::consts::FUSE_BIG_WRITES; use fuser::FileType::RegularFile; use fuser::TimeOrNow::Now; use fuser::{ FileAttr, FileType, Filesystem, KernelConfig, ReplyAttr, ReplyCreate, ReplyData, - ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyOpen, ReplyWrite, Request, TimeOrNow, + ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyIoctl, ReplyOpen, ReplyWrite, Request, TimeOrNow, }; use libc::c_int; use std::cmp::min; @@ -16,9 +17,6 @@ use std::ffi::OsStr; use std::io; use std::time::{Duration, SystemTime}; -type Inode = u64; -type Fh = u64; - /// The File is opened for execution. const FMODE_EXEC: i32 = 0x20; /// Total cache size of all files, after which all caches will be [`dropped and shrank`][FuseFS::drop_and_shrink_cache] to the [`underlying filesystem`][FileSystem]. @@ -26,6 +24,14 @@ const FILESYSTEM_CACHE_MAX_SIZE: usize = 25 * MB; /// Maximum cache size, after which it will drop [`dropped and shrank`][FuseFS::drop_and_shrink_cache] to the [`underlying filesystem`][FileSystem]. const FILE_CACHE_MAX_SIZE: usize = 5 * MB; +/// Command number for filesystem dedup ratio request via ioctl. +pub const IOC_GET_DEDUP_RATIO: u64 = nix::request_code_read!('S', 0, size_of::()); +/// Command number for filesystem average chunk size request via ioctl. +pub const IOC_GET_AVG_CHUNK_SIZE: u64 = nix::request_code_read!('S', 1, size_of::()); + +type Inode = u64; +type Fh = u64; + /// [`FuseFS`] file entity. #[derive(Clone)] struct FuseFile { @@ -58,7 +64,7 @@ struct FuseFileHandle { /// After creation, it should be passed to [`mount2`][fuser::mount2] or [`spawn_mount2`][fuser::spawn_mount2]. pub struct FuseFS where - B: Database>, + B: IterableDatabase>, Hash: ChunkHash, { /// Underlying [chunkfs][`FileSystem`]. @@ -79,7 +85,7 @@ where impl FuseFS where - B: Database>, + B: IterableDatabase>, Hash: ChunkHash, { /// Creates a file system with the given [`hasher`][Hasher], [`database`][Database] and [`chunker`][ChunkerRef]. @@ -233,7 +239,7 @@ fn check_access(file_attr: &FileAttr, req: &Request, access_mask: i32) -> bool { impl Filesystem for FuseFS where - B: Database>, + B: IterableDatabase>, Hash: ChunkHash, { fn init(&mut self, _req: &Request<'_>, config: &mut KernelConfig) -> Result<(), c_int> { @@ -511,13 +517,13 @@ where reply.error(libc::EACCES); return; } - + let cache_cap_before = file.cache.capacity(); file.cache.extend_from_slice(data); let cache_cap_after = file.cache.capacity(); self.total_cache += cache_cap_after - cache_cap_before; file.attr.size += data.len() as u64; - + if file.cache.len() > FILE_CACHE_MAX_SIZE && self.drop_cache(ino, fh).is_err() { reply.error(libc::EIO); return; @@ -711,4 +717,42 @@ where reply.created(&Duration::new(0, 0), &file.attr, 0, fh, flags as u32); } + + fn ioctl( + &mut self, + _req: &Request<'_>, + ino: u64, + fh: u64, + _flags: u32, + cmd: u32, + _in_data: &[u8], + _out_size: u32, + reply: ReplyIoctl, + ) { + let Some(file_handle) = self.file_handles.get_mut(&fh) else { + reply.error(libc::EBADF); + return; + }; + if file_handle.inode != ino { + reply.error(libc::ESTALE); + return; + } + if self.drop_and_shrink_caches().is_err() { + reply.error(libc::EIO); + return; + } + match cmd.into() { + IOC_GET_AVG_CHUNK_SIZE => { + let avg_chunk_size = self.underlying_fs.average_chunk_size(); + reply.ioctl(0, &avg_chunk_size.to_ne_bytes()); + } + IOC_GET_DEDUP_RATIO => { + let dedup_ratio = self.underlying_fs.cdc_dedup_ratio(); + reply.ioctl(0, &dedup_ratio.to_ne_bytes()); + } + _ => { + reply.error(libc::EINVAL); + } + } + } } diff --git a/tests/fuse_filesystem.rs b/tests/fuse_filesystem.rs index dc392f60..7a46d819 100644 --- a/tests/fuse_filesystem.rs +++ b/tests/fuse_filesystem.rs @@ -1,7 +1,7 @@ use cdc_chunkers::SizeParams; use chunkfs::chunkers::{LeapChunker, SuperChunker}; use chunkfs::hashers::Sha256Hasher; -use chunkfs::{ChunkerRef, FuseFS, MB}; +use chunkfs::{ChunkerRef, FuseFS, IOC_GET_AVG_CHUNK_SIZE, IOC_GET_DEDUP_RATIO, MB}; use filetime::FileTime; use fuser::BackgroundSession; use fuser::MountOption::AutoUnmount; @@ -11,6 +11,7 @@ use std::ffi::OsString; use std::fs; use std::fs::{File, OpenOptions, Permissions}; use std::io::{Read, Write}; +use std::os::fd::AsRawFd; use std::os::unix::fs::{FileExt, MetadataExt, OpenOptionsExt, PermissionsExt}; use std::path::{Path, PathBuf}; use std::time::SystemTime; @@ -839,3 +840,44 @@ fn concurrent_write_and_lookup() { handle.join().unwrap(); assert_eq!(file.metadata().unwrap().len(), 50 * MB as u64); } + +#[test] +fn ioctl() { + let fuse_fixture = FuseFixture::with_chunker(SuperChunker::new(SizeParams { + min: 1000, + avg: 1000, + max: 1000, + })); + let mount_point = Path::new(&fuse_fixture.mount_point); + + let file_path = mount_point.join("file"); + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .custom_flags(O_DIRECT) + .write(true) + .read(true) + .open(&file_path) + .unwrap(); + + file.write_all(&[0; 5000]).unwrap(); + + let mut dedup_ratio = [0u8; size_of::()]; + let ret = unsafe { libc::ioctl(file.as_raw_fd(), IOC_GET_DEDUP_RATIO, &mut dedup_ratio) }; + assert_eq!(ret, 0); + + let mut avg_chunk_size = [0u8; size_of::()]; + let ret = unsafe { + libc::ioctl( + file.as_raw_fd(), + IOC_GET_AVG_CHUNK_SIZE, + &mut avg_chunk_size, + ) + }; + assert_eq!(ret, 0); + + let dedup_ratio = f64::from_ne_bytes(dedup_ratio); + let avg_chunk_size = usize::from_ne_bytes(avg_chunk_size); + assert_eq!(dedup_ratio, 5.); + assert_eq!(avg_chunk_size, 1000); +} From de92078e9883e6986a40079afbfe5be018869c3e Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 8 May 2025 15:47:41 +0300 Subject: [PATCH 56/60] update Readme with native dependencies for FUSE Signed-off-by: Gleb Nasretdinov --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 477775c9..b9f21e7d 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,21 @@ To use provided chunkers and hashers, use the corresponding features: chunkfs = { version = "0.1", features = ["chunkers", "hashers"] } ``` +## FUSE native dependepcies + +To use FuseFS structure, mountable with FUSE, you need to install the following native dependencies: + +```bash +sudo apt update +sudo apt install -y fuse3 libfuse3-dev +``` + +To use the file system correctly, you need to allow other users to access the mounted file system. + +```bash +echo "user_allow_other" | sudo tee -a /etc/fuse.conf +``` + ## Examples Examples for chunkfs usage and benching are provided in [examples](examples) folder. From 6656e49da16f933f9ccb1eae1cc4e6b1449481a5 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 8 May 2025 15:48:00 +0300 Subject: [PATCH 57/60] fix global cache drop with concurrent file handles in FuseFS Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index c94956e3..730cbc50 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -191,6 +191,9 @@ where .files .get_mut(&handle.inode) .ok_or(io::ErrorKind::NotFound)?; + handle + .underlying_file_handle + .set_offset(file.attr.size as usize - file.cache.len()); self.underlying_fs .write_to_file(&mut handle.underlying_file_handle, &file.cache)?; From 6310a903706f47bfa818e1bfdc81be8b3cc17ac9 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 8 May 2025 16:11:50 +0300 Subject: [PATCH 58/60] change custom Dataset structure to bench::Dataset in fuse_write_read bench Signed-off-by: Gleb Nasretdinov --- benches/fuse_write_read.rs | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/benches/fuse_write_read.rs b/benches/fuse_write_read.rs index 6c3a3712..6fc09b45 100644 --- a/benches/fuse_write_read.rs +++ b/benches/fuse_write_read.rs @@ -1,4 +1,5 @@ use cdc_chunkers::SizeParams; +use chunkfs::bench::Dataset; use chunkfs::chunkers::{LeapChunker, RabinChunker, SuperChunker, UltraChunker}; use chunkfs::hashers::Sha256Hasher; use chunkfs::{ChunkerRef, FuseFS, IOC_GET_AVG_CHUNK_SIZE, IOC_GET_DEDUP_RATIO, MB}; @@ -55,23 +56,13 @@ fn get_default_sizes(algorithm: Algorithms) -> SizeParams { } } -struct Dataset { - filename: String, - size: u64, -} - pub fn bench(c: &mut Criterion) { - let dataset1_len = File::open("archX4.tar").unwrap().metadata().unwrap().len(); - let dataset1 = Dataset { - filename: "archX4.tar".to_string(), - size: dataset1_len, - }; - let datasets = vec![dataset1]; + let datasets = vec![Dataset::new("archX4.tar", "archX4").unwrap()]; for dataset in datasets { let mut group = c.benchmark_group("FuseChunkers"); group.sample_size(SAMPLE_SIZE); - group.throughput(Throughput::Bytes(dataset.size)); + group.throughput(Throughput::Bytes(dataset.size as u64)); for chunker in chunkers() { let params = get_default_sizes(chunker); @@ -109,7 +100,7 @@ fn dedup_ratio_and_avg_chunk(dataset: &Dataset, algorithm: Algorithms, params: S .open(&fuse_path) .unwrap(); - let mut source = File::open(&dataset.filename).unwrap(); + let mut source = File::open(&dataset.path).unwrap(); let mut buf = vec![0; 50 * MB]; loop { let bytes_read = source.read(&mut buf).unwrap(); @@ -148,7 +139,7 @@ fn bench_write( algorithm: Algorithms, params: SizeParams, ) { - let bench_name = dataset.filename.clone(); + let bench_name = dataset.path.clone(); let parameter = format!("write_fuse-{:?}-{}", algorithm, params); group.bench_function(BenchmarkId::new(bench_name, parameter), |b| { b.iter_batched( @@ -171,7 +162,7 @@ fn bench_write( .open(&fuse_path) .unwrap(); - let source = File::open(&dataset.filename).unwrap(); + let source = File::open(&dataset.path).unwrap(); (session, source, fuse_file) }, @@ -215,7 +206,7 @@ fn bench_read( .open(&fuse_path) .unwrap(); - let mut source = File::open(&dataset.filename).unwrap(); + let mut source = File::open(&dataset.path).unwrap(); let mut buf = vec![0; 50 * MB]; loop { @@ -228,7 +219,7 @@ fn bench_read( fuse_file.flush().unwrap(); drop(fuse_file); - let bench_name = dataset.filename.clone(); + let bench_name = dataset.path.clone(); let parameter = format!("read_fuse-{:?}-{}", algorithm, params); group.bench_function(BenchmarkId::new(bench_name, parameter), |b| { b.iter_batched( From b70e410c14f8ca4bdfd488135cd488faa64ed6b1 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 8 May 2025 18:17:08 +0300 Subject: [PATCH 59/60] fix benches: super and ultra were mix up Signed-off-by: Gleb Nasretdinov --- benches/chunker_sizes.rs | 4 ++-- benches/fuse_write_read.rs | 4 ++-- benches/write_read.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/benches/chunker_sizes.rs b/benches/chunker_sizes.rs index 0289f09a..4ee90588 100644 --- a/benches/chunker_sizes.rs +++ b/benches/chunker_sizes.rs @@ -54,8 +54,8 @@ fn get_chunker(algorithm: Algorithms, params: SizeParams) -> ChunkerRef { match algorithm { Algorithms::Rabin => RabinChunker::new(params).into(), Algorithms::Leap => LeapChunker::new(params).into(), - Algorithms::Super => UltraChunker::new(params).into(), - Algorithms::Ultra => SuperChunker::new(params).into(), + Algorithms::Super => SuperChunker::new(params).into(), + Algorithms::Ultra => UltraChunker::new(params).into(), } } diff --git a/benches/fuse_write_read.rs b/benches/fuse_write_read.rs index 6fc09b45..5633e193 100644 --- a/benches/fuse_write_read.rs +++ b/benches/fuse_write_read.rs @@ -41,8 +41,8 @@ fn get_chunker(algorithm: Algorithms, params: SizeParams) -> ChunkerRef { match algorithm { Algorithms::Rabin => RabinChunker::new(params).into(), Algorithms::Leap => LeapChunker::new(params).into(), - Algorithms::Super => UltraChunker::new(params).into(), - Algorithms::Ultra => SuperChunker::new(params).into(), + Algorithms::Super => SuperChunker::new(params).into(), + Algorithms::Ultra => UltraChunker::new(params).into(), } } diff --git a/benches/write_read.rs b/benches/write_read.rs index 2abab07f..48ef0799 100644 --- a/benches/write_read.rs +++ b/benches/write_read.rs @@ -33,8 +33,8 @@ fn get_chunker(algorithm: Algorithms) -> ChunkerRef { match algorithm { Algorithms::Rabin => RabinChunker::default().into(), Algorithms::Leap => LeapChunker::default().into(), - Algorithms::Super => UltraChunker::default().into(), - Algorithms::Ultra => SuperChunker::default().into(), + Algorithms::Super => SuperChunker::default().into(), + Algorithms::Ultra => UltraChunker::default().into(), } } From 213e1a5b4aa72a9f8fd0aad8fe213a7732ebe1ff Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 8 May 2025 21:43:23 +0300 Subject: [PATCH 60/60] increase FuseFS cache sizes Signed-off-by: Gleb Nasretdinov --- src/system/fuse_filesystem.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/system/fuse_filesystem.rs b/src/system/fuse_filesystem.rs index 730cbc50..581e619b 100644 --- a/src/system/fuse_filesystem.rs +++ b/src/system/fuse_filesystem.rs @@ -20,9 +20,9 @@ use std::time::{Duration, SystemTime}; /// The File is opened for execution. const FMODE_EXEC: i32 = 0x20; /// Total cache size of all files, after which all caches will be [`dropped and shrank`][FuseFS::drop_and_shrink_cache] to the [`underlying filesystem`][FileSystem]. -const FILESYSTEM_CACHE_MAX_SIZE: usize = 25 * MB; +const FILESYSTEM_CACHE_MAX_SIZE: usize = 250 * MB; /// Maximum cache size, after which it will drop [`dropped and shrank`][FuseFS::drop_and_shrink_cache] to the [`underlying filesystem`][FileSystem]. -const FILE_CACHE_MAX_SIZE: usize = 5 * MB; +const FILE_CACHE_MAX_SIZE: usize = 100 * MB; /// Command number for filesystem dedup ratio request via ioctl. pub const IOC_GET_DEDUP_RATIO: u64 = nix::request_code_read!('S', 0, size_of::());