Skip to content

Commit e403c76

Browse files
committed
Add rename() FUSE operation and integration tests
Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent 48eab58 commit e403c76

4 files changed

Lines changed: 721 additions & 0 deletions

File tree

src/fs.rs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,179 @@ impl Filesystem for BranchFs {
11211121
self.unlink(_req, parent, name, reply);
11221122
}
11231123

1124+
fn rename(
1125+
&mut self,
1126+
_req: &Request,
1127+
parent: u64,
1128+
name: &OsStr,
1129+
newparent: u64,
1130+
newname: &OsStr,
1131+
flags: u32,
1132+
reply: ReplyEmpty,
1133+
) {
1134+
if flags & libc::RENAME_EXCHANGE != 0 {
1135+
reply.error(libc::EINVAL);
1136+
return;
1137+
}
1138+
1139+
let parent_path = match self.inodes.get_path(parent) {
1140+
Some(p) => p,
1141+
None => {
1142+
reply.error(libc::ENOENT);
1143+
return;
1144+
}
1145+
};
1146+
let newparent_path = match self.inodes.get_path(newparent) {
1147+
Some(p) => p,
1148+
None => {
1149+
reply.error(libc::ENOENT);
1150+
return;
1151+
}
1152+
};
1153+
1154+
let name_str = name.to_string_lossy();
1155+
let newname_str = newname.to_string_lossy();
1156+
1157+
// Normalize both parents to (branch, parent_rel, inode_prefix, is_root_path).
1158+
let resolve_parent = |path: &str| -> Option<(String, String, String, bool)> {
1159+
match classify_path(path) {
1160+
PathContext::BranchDir(b) => {
1161+
Some((b.clone(), "/".into(), format!("/@{}", b), false))
1162+
}
1163+
PathContext::BranchPath(b, rel) => {
1164+
Some((b.clone(), rel, format!("/@{}", b), false))
1165+
}
1166+
PathContext::RootPath(rp) => Some((String::new(), rp, String::new(), true)),
1167+
_ => None,
1168+
}
1169+
};
1170+
1171+
let (src_branch, src_parent_rel, src_prefix, src_is_root) =
1172+
match resolve_parent(&parent_path) {
1173+
Some(t) => t,
1174+
None => {
1175+
reply.error(libc::EPERM);
1176+
return;
1177+
}
1178+
};
1179+
let (dst_branch, dst_parent_rel, dst_prefix, dst_is_root) =
1180+
match resolve_parent(&newparent_path) {
1181+
Some(t) => t,
1182+
None => {
1183+
reply.error(libc::EPERM);
1184+
return;
1185+
}
1186+
};
1187+
1188+
// Both must be the same kind (both root or both same branch)
1189+
if src_is_root != dst_is_root || (!src_is_root && src_branch != dst_branch) {
1190+
reply.error(libc::EXDEV);
1191+
return;
1192+
}
1193+
1194+
let branch = if src_is_root {
1195+
self.get_branch_name()
1196+
} else {
1197+
if !self.manager.is_branch_valid(&src_branch) {
1198+
reply.error(libc::ENOENT);
1199+
return;
1200+
}
1201+
src_branch
1202+
};
1203+
1204+
let join_rel = |parent_rel: &str, child: &str| -> String {
1205+
if parent_rel == "/" {
1206+
format!("/{}", child)
1207+
} else {
1208+
format!("{}/{}", parent_rel, child)
1209+
}
1210+
};
1211+
let src_rel = join_rel(&src_parent_rel, &name_str);
1212+
let dst_rel = join_rel(&dst_parent_rel, &newname_str);
1213+
1214+
// Check source exists
1215+
if self.resolve_for_branch(&branch, &src_rel).is_none() {
1216+
reply.error(libc::ENOENT);
1217+
return;
1218+
}
1219+
1220+
// RENAME_NOREPLACE
1221+
if flags & libc::RENAME_NOREPLACE != 0
1222+
&& self.resolve_for_branch(&branch, &dst_rel).is_some()
1223+
{
1224+
reply.error(libc::EEXIST);
1225+
return;
1226+
}
1227+
1228+
// COW source into delta
1229+
let src_delta = match self.ensure_cow_for_branch(&branch, &src_rel) {
1230+
Ok(p) => p,
1231+
Err(_) => {
1232+
reply.error(libc::EIO);
1233+
return;
1234+
}
1235+
};
1236+
1237+
let dst_delta = self.get_delta_path_for_branch(&branch, &dst_rel);
1238+
if storage::ensure_parent_dirs(&dst_delta).is_err() {
1239+
reply.error(libc::EIO);
1240+
return;
1241+
}
1242+
1243+
// If destination already exists, remove its delta so rename can overwrite
1244+
let dst_existed = self.resolve_for_branch(&branch, &dst_rel).is_some();
1245+
if dst_existed {
1246+
let _ = self.manager.with_branch(&branch, |b| {
1247+
let d = b.delta_path(&dst_rel);
1248+
if d.exists() {
1249+
if d.is_dir() {
1250+
let _ = std::fs::remove_dir_all(&d);
1251+
} else {
1252+
let _ = std::fs::remove_file(&d);
1253+
}
1254+
}
1255+
Ok(())
1256+
});
1257+
}
1258+
1259+
// Move within the delta layer (same filesystem, always succeeds)
1260+
if std::fs::rename(&src_delta, &dst_delta).is_err() {
1261+
reply.error(libc::EIO);
1262+
return;
1263+
}
1264+
1265+
// Update tombstones: mark src deleted, revive dst, tombstone old dst
1266+
let result = self.manager.with_branch(&branch, |b| {
1267+
b.add_tombstone(&src_rel)?;
1268+
if dst_existed {
1269+
b.add_tombstone(&dst_rel)?;
1270+
}
1271+
b.remove_tombstone(&dst_rel);
1272+
Ok(())
1273+
});
1274+
if result.is_err() {
1275+
reply.error(libc::EIO);
1276+
return;
1277+
}
1278+
1279+
if src_is_root && self.is_stale() {
1280+
reply.error(libc::ESTALE);
1281+
return;
1282+
}
1283+
1284+
// Update inode cache
1285+
let src_inode_path = format!("{}{}", src_prefix, src_rel);
1286+
let dst_inode_path = format!("{}{}", dst_prefix, dst_rel);
1287+
let is_dir = dst_delta.is_dir();
1288+
self.inodes.remove(&src_inode_path);
1289+
self.inodes.remove(&dst_inode_path);
1290+
let new_ino = self.inodes.get_or_create(&dst_inode_path, is_dir);
1291+
self.open_cache.invalidate_ino(new_ino);
1292+
self.write_cache.invalidate_ino(new_ino);
1293+
1294+
reply.ok();
1295+
}
1296+
11241297
fn open(&mut self, _req: &Request, ino: u64, flags: i32, reply: ReplyOpen) {
11251298
// Control file is always openable (no epoch check)
11261299
if ino == CTL_INO {

tests/run_all_tests.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,21 @@ else
9696
fi
9797
echo ""
9898

99+
# Run filesystem integration tests
100+
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
101+
echo -e "${BLUE} test_integration (Rust integration)${NC}"
102+
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
103+
TOTAL_TESTS=$((TOTAL_TESTS + 1))
104+
if (cd "$PROJECT_ROOT" && cargo test --test test_integration -- --ignored 2>&1); then
105+
PASSED_SUITES=$((PASSED_SUITES + 1))
106+
echo -e "${GREEN}Suite test_integration: PASSED${NC}"
107+
else
108+
FAILED_SUITES=$((FAILED_SUITES + 1))
109+
FAILED_SUITE_NAMES+=("test_integration")
110+
echo -e "${RED}Suite test_integration: FAILED${NC}"
111+
fi
112+
echo ""
113+
99114
# Final summary
100115
echo -e "${BLUE}========================================${NC}"
101116
echo -e "${BLUE} Final Summary${NC}"

0 commit comments

Comments
 (0)