Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion project/slayerfs/examples/persistence_s3_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use slayerfs::chuck::chunk::ChunkLayout;
use slayerfs::chuck::store::ObjectBlockStore;
use slayerfs::fuse::mount::mount_vfs_unprivileged;
use slayerfs::meta::factory::MetaStoreFactory;
use slayerfs::meta::stores::DatabaseMetaStore;
use slayerfs::vfs::fs::VFS;
use std::path::PathBuf;
use tokio::signal;
Expand Down Expand Up @@ -205,7 +206,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

let config = slayerfs::meta::config::Config::from_file(&target_config_path)
.map_err(|e| format!("Failed to load config file: {}", e))?;
let meta = MetaStoreFactory::create_from_config(config)
let meta = MetaStoreFactory::<DatabaseMetaStore>::create_from_config(config)
.await
.map_err(|e| format!("Failed to initialize metadata storage: {}", e))?;
let meta_store = meta.store();
Expand Down
24 changes: 8 additions & 16 deletions project/slayerfs/src/fuse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -745,15 +745,6 @@ where
return Err(libc::ENOTDIR.into());
}

// Ensure destination does not already exist
if self
.child_of(new_parent as i64, new_name.as_ref())
.await
.is_some()
{
return Err(libc::EEXIST.into());
}

// Build full paths and perform the rename
let Some(mut oldp) = self.path_of(parent as i64).await else {
return Err(libc::ENOENT.into());
Expand All @@ -769,13 +760,14 @@ where
newp.push('/');
}
newp.push_str(&new_name);
VFS::rename(self, &oldp, &newp).await.map_err(|e| {
let code = match e.as_str() {
"target exists" => libc::EEXIST,
_ => libc::EIO,
};
code.into()
})
VFS::rename(self, &oldp, &newp)
.await
.map_err(|e| match e.as_str() {
"target exists" => libc::EEXIST.into(),
"directory not empty" => libc::ENOTEMPTY.into(),
"not a directory" => libc::ENOTDIR.into(),
_ => libc::EIO.into(),
})
}

// ===== Resource release & sync: stateless implementation, return success =====
Expand Down
4 changes: 3 additions & 1 deletion project/slayerfs/src/meta/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,9 @@ impl<T: MetaStore + 'static> MetaLayer for MetaClient<T> {

info!("MetaClient: Inode cache MISS for readdir inode {}", inode);

let entries = self.store.readdir(inode).await?;
let mut entries = self.store.readdir(inode).await?;
// Sort once before caching so readops always return stable ordering by name.
entries.sort_by(|a, b| a.name.cmp(&b.name));

info!(
"MetaClient: Caching readdir result for inode {} ({} entries)",
Expand Down
85 changes: 73 additions & 12 deletions project/slayerfs/src/vfs/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -792,18 +792,7 @@ where
let (old_dir, old_name) = Self::split_dir_file(&old);
let (new_dir, new_name) = Self::split_dir_file(&new);

if self
.core
.meta_layer
.lookup_path(&new)
.await
.ok()
.flatten()
.is_some()
{
return Err("target exists".into());
}

// Resolve old parent and source inode/attributes first
let old_parent_ino = if &old_dir == "/" {
self.core.root
} else {
Expand All @@ -816,8 +805,80 @@ where
.0
};

let src_ino = self
.core
.meta_layer
.lookup(old_parent_ino, &old_name)
.await
.map_err(|e| e.to_string())?
.ok_or_else(|| "not found".to_string())?;

let src_attr = self
.core
.meta_layer
.stat(src_ino)
.await
.map_err(|e| e.to_string())?
.ok_or_else(|| "not found".to_string())?;

// If destination exists, apply replace semantics:
// - If dest is file/symlink: unlink it
// - If dest is dir: source must be dir and dest must be empty; rmdir it
if let Ok(Some((dest_ino, dest_kind))) = self.core.meta_layer.lookup_path(&new).await {
// resolve parent directory ino for destination
let new_dir_ino = if &new_dir == "/" {
self.core.root
} else {
// parent must exist when destination exists
self.core
.meta_layer
.lookup_path(&new_dir)
.await
.map_err(|e| e.to_string())?
.ok_or_else(|| "parent not found".to_string())?
.0
};

if dest_kind == FileType::Dir {
// source must be directory
if src_attr.kind != FileType::Dir {
return Err("not a directory".into());
}

// ensure destination dir is empty
let children = self
.core
.meta_layer
.readdir(dest_ino)
.await
.map_err(|e| e.to_string())?;
if !children.is_empty() {
return Err("directory not empty".into());
}

// remove the empty destination directory
self.core
.meta_layer
.rmdir(new_dir_ino, &new_name)
.await
.map_err(|e| e.to_string())?;
} else {
if src_attr.kind == FileType::Dir {
return Err("not a directory".into());
}
// dest is a file or symlink: unlink it to allow replace
self.core
.meta_layer
.unlink(new_dir_ino, &new_name)
.await
.map_err(|e| e.to_string())?;
}
}

// Ensure destination parent exists (create as needed)
let new_dir_ino = self.mkdir_p(&new_dir).await?;

// Perform rename
self.core
.meta_layer
.rename(old_parent_ino, &old_name, new_dir_ino, new_name)
Expand Down
85 changes: 85 additions & 0 deletions project/slayerfs/tests/scripts/xfstests_slayer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/bin/bash

set -euo pipefail

current_dir=$(dirname "$(realpath "$0")")
workspace_dir=$(realpath "$current_dir/../../..")
redis_config="$workspace_dir/slayerfs/slayerfs-sqlite.yml"
backend_dir=/tmp/data
mount_dir=/tmp/mount
log_file=/tmp/slayerfs.log
persistence_bin="$workspace_dir/target/release/examples/persistence_demo"

if [[ -z "$persistence_bin" ]]; then
echo "Cannot find slayerfs persistence_demo binary."
echo "Please run: cargo build -p slayerfs --example persistence_demo --release"
exit 1
fi

sudo rm -rf "$backend_dir"
while mount | grep -q "$mount_dir"; do
sudo umount -f "$mount_dir" || sleep 1
done
sudo rm -rf "$mount_dir"
sudo rm -rf /tmp/xfstests-dev
sudo mkdir -p "$backend_dir" "$mount_dir"
sudo rm -f "$log_file"

sudo apt-get update
sudo apt-get install acl attr automake bc dbench dump e2fsprogs fio gawk \
gcc git indent libacl1-dev libaio-dev libcap-dev libgdbm-dev libtool \
libtool-bin liburing-dev libuuid1 lvm2 make psmisc python3 quota sed \
uuid-dev uuid-runtime xfsprogs linux-headers-$(uname -r) sqlite3 \
fuse3
sudo apt-get install exfatprogs f2fs-tools ocfs2-tools udftools xfsdump \
xfslibs-dev

# clone xfstests and install.
cd /tmp/
git clone -b v2023.12.10 git://git.kernel.org/pub/scm/fs/xfs/xfstests-dev.git
cd xfstests-dev
make
sudo make install

# overwrite local config.
cat >local.config <<EOF
export TEST_DEV=slayerfs
export TEST_DIR=$mount_dir
#export SCRATCH_DEV=slayerfs
#export SCRATCH_MNT=/tmp/test2/merged
export FSTYP=fuse
export FUSE_SUBTYP=.slayerfs

#Deleting the following command will result in an error: TEST_DEV=slayerfs is mounted but not a type fuse filesystem.
export DF_PROG="df -T -P -a"
EOF

# create fuse mount script for slayerfs.
sudo cat >/usr/sbin/mount.fuse.slayerfs <<EOF
#!/bin/bash
set -euo pipefail

export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:\$PATH"

ulimit -n 1048576
CONFIG_PATH="$redis_config"
LOG_FILE="$log_file"
PERSISTENCE_BIN="$persistence_bin"

BACKEND_DIR="$backend_dir"
MOUNT_DIR="$mount_dir"

mkdir -p "\$BACKEND_DIR" "\$MOUNT_DIR"

"\$PERSISTENCE_BIN" \
-c "\$CONFIG_PATH" \
-s "\$BACKEND_DIR" \
-m "\$MOUNT_DIR" >>"\$LOG_FILE" 2>&1 &

EOF
sudo chmod +x /usr/sbin/mount.fuse.slayerfs

echo "====> Start to run xfstests."
# run tests.
cd /tmp/xfstests-dev
# sudo LC_ALL=C ./check -fuse