Skip to content

Commit 5750b3f

Browse files
committed
Add symlink support across FUSE layer, COW, and branch operations
Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent 2077030 commit 5750b3f

6 files changed

Lines changed: 658 additions & 20 deletions

File tree

src/branch.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use parking_lot::{Mutex, RwLock};
1111

1212
use crate::error::{BranchError, Result};
1313
use crate::inode::ROOT_INO;
14+
use crate::storage;
1415

1516
pub struct Branch {
1617
pub name: String,
@@ -104,7 +105,7 @@ impl Branch {
104105
}
105106

106107
pub fn has_delta(&self, rel_path: &str) -> bool {
107-
self.delta_path(rel_path).exists()
108+
self.delta_path(rel_path).symlink_metadata().is_ok()
108109
}
109110
}
110111

@@ -419,7 +420,7 @@ impl BranchManager {
419420
}
420421

421422
let base = self.base_path.join(rel_path.trim_start_matches('/'));
422-
if base.exists() {
423+
if base.symlink_metadata().is_ok() {
423424
Ok(Some(base))
424425
} else {
425426
Ok(None)
@@ -476,8 +477,12 @@ impl BranchManager {
476477
// Apply tombstones as deletions
477478
for path in &child_tombstones {
478479
let full_path = self.base_path.join(path.trim_start_matches('/'));
479-
if full_path.exists() {
480-
if full_path.is_dir() {
480+
if full_path.symlink_metadata().is_ok() {
481+
if full_path
482+
.symlink_metadata()
483+
.map(|m| m.file_type().is_dir())
484+
.unwrap_or(false)
485+
{
481486
fs::remove_dir_all(&full_path)?;
482487
} else {
483488
fs::remove_file(&full_path)?;
@@ -493,10 +498,10 @@ impl BranchManager {
493498
if let Some(parent_dir) = dest.parent() {
494499
let _ = fs::create_dir_all(parent_dir);
495500
}
496-
if let Ok(meta) = src_path.metadata() {
501+
if let Ok(meta) = src_path.symlink_metadata() {
497502
total_bytes += meta.len();
498503
}
499-
let _ = fs::copy(src_path, &dest);
504+
let _ = storage::copy_entry(src_path, &dest);
500505
num_files += 1;
501506
})?;
502507

@@ -557,7 +562,7 @@ impl BranchManager {
557562
if let Some(parent_dir) = dest.parent() {
558563
let _ = fs::create_dir_all(parent_dir);
559564
}
560-
let _ = fs::copy(src_path, &dest);
565+
let _ = storage::copy_entry(src_path, &dest);
561566
copied_paths.push(rel_path.to_string());
562567
})?;
563568

@@ -661,7 +666,11 @@ impl BranchManager {
661666
format!("{}/{}", prefix, name)
662667
};
663668

664-
if path.is_dir() {
669+
let is_dir = path
670+
.symlink_metadata()
671+
.map(|m| m.file_type().is_dir())
672+
.unwrap_or(false);
673+
if is_dir {
665674
self.walk_files(&path, &rel_path, f)?;
666675
} else {
667676
f(&rel_path, &path);

src/fs.rs

Lines changed: 141 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,10 @@ impl Filesystem for BranchFs {
377377
return;
378378
}
379379
};
380-
let is_dir = resolved.is_dir();
380+
let is_dir = resolved
381+
.symlink_metadata()
382+
.map(|m| m.is_dir())
383+
.unwrap_or(false);
381384
let ino = self.inodes.get_or_create(&path, is_dir);
382385
match self.make_attr(ino, &resolved) {
383386
Some(attr) => reply.entry(&TTL, &attr, 0),
@@ -428,7 +431,10 @@ impl Filesystem for BranchFs {
428431
};
429432

430433
let inode_path = format!("/@{}{}", branch, child_rel);
431-
let is_dir = resolved.is_dir();
434+
let is_dir = resolved
435+
.symlink_metadata()
436+
.map(|m| m.is_dir())
437+
.unwrap_or(false);
432438
let ino = self.inodes.get_or_create(&inode_path, is_dir);
433439
match self.make_attr(ino, &resolved) {
434440
Some(attr) => reply.entry(&TTL, &attr, 0),
@@ -449,7 +455,10 @@ impl Filesystem for BranchFs {
449455
return;
450456
}
451457
};
452-
let is_dir = resolved.is_dir();
458+
let is_dir = resolved
459+
.symlink_metadata()
460+
.map(|m| m.is_dir())
461+
.unwrap_or(false);
453462
let ino = self.inodes.get_or_create(&path, is_dir);
454463
match self.make_attr(ino, &resolved) {
455464
Some(attr) => reply.entry(&TTL, &attr, 0),
@@ -1679,4 +1688,133 @@ impl Filesystem for BranchFs {
16791688
}
16801689
}
16811690
}
1691+
1692+
fn readlink(&mut self, _req: &Request, ino: u64, reply: ReplyData) {
1693+
let resolved = match self.classify_ino(ino) {
1694+
Some(PathContext::BranchPath(branch, rel_path)) => {
1695+
if !self.manager.is_branch_valid(&branch) {
1696+
reply.error(libc::ENOENT);
1697+
return;
1698+
}
1699+
match self.resolve_for_branch(&branch, &rel_path) {
1700+
Some(p) => p,
1701+
None => {
1702+
reply.error(libc::ENOENT);
1703+
return;
1704+
}
1705+
}
1706+
}
1707+
Some(PathContext::RootPath(ref rp)) => {
1708+
if self.is_stale() {
1709+
reply.error(libc::ESTALE);
1710+
return;
1711+
}
1712+
match self.resolve(rp) {
1713+
Some(p) => p,
1714+
None => {
1715+
reply.error(libc::ENOENT);
1716+
return;
1717+
}
1718+
}
1719+
}
1720+
_ => {
1721+
reply.error(libc::EINVAL);
1722+
return;
1723+
}
1724+
};
1725+
1726+
match std::fs::read_link(&resolved) {
1727+
Ok(target) => reply.data(target.as_os_str().as_encoded_bytes()),
1728+
Err(_) => reply.error(libc::EINVAL),
1729+
}
1730+
}
1731+
1732+
fn symlink(
1733+
&mut self,
1734+
_req: &Request,
1735+
parent: u64,
1736+
link_name: &OsStr,
1737+
target: &Path,
1738+
reply: ReplyEntry,
1739+
) {
1740+
let parent_path = match self.inodes.get_path(parent) {
1741+
Some(p) => p,
1742+
None => {
1743+
reply.error(libc::ENOENT);
1744+
return;
1745+
}
1746+
};
1747+
1748+
let name_str = link_name.to_string_lossy();
1749+
1750+
let branch_ctx = match classify_path(&parent_path) {
1751+
PathContext::BranchDir(b) => Some((b, "/".to_string())),
1752+
PathContext::BranchPath(b, rel) => Some((b, rel)),
1753+
_ => None,
1754+
};
1755+
1756+
if let Some((branch, parent_rel)) = branch_ctx {
1757+
if !self.manager.is_branch_valid(&branch) {
1758+
reply.error(libc::ENOENT);
1759+
return;
1760+
}
1761+
let rel_path = if parent_rel == "/" {
1762+
format!("/{}", name_str)
1763+
} else {
1764+
format!("{}/{}", parent_rel, name_str)
1765+
};
1766+
let delta = self.get_delta_path_for_branch(&branch, &rel_path);
1767+
if storage::ensure_parent_dirs(&delta).is_err() {
1768+
reply.error(libc::EIO);
1769+
return;
1770+
}
1771+
match std::os::unix::fs::symlink(target, &delta) {
1772+
Ok(()) => {
1773+
let inode_path = format!("/@{}{}", branch, rel_path);
1774+
let ino = self.inodes.get_or_create(&inode_path, false);
1775+
match self.make_attr(ino, &delta) {
1776+
Some(attr) => reply.entry(&TTL, &attr, 0),
1777+
None => reply.error(libc::EIO),
1778+
}
1779+
}
1780+
Err(_) => reply.error(libc::EIO),
1781+
}
1782+
} else {
1783+
match classify_path(&parent_path) {
1784+
PathContext::BranchCtl(_) | PathContext::RootCtl => {
1785+
reply.error(libc::EPERM);
1786+
}
1787+
PathContext::RootPath(rp) => {
1788+
let path = if rp == "/" {
1789+
format!("/{}", name_str)
1790+
} else {
1791+
format!("{}/{}", rp, name_str)
1792+
};
1793+
let delta = self.get_delta_path(&path);
1794+
if storage::ensure_parent_dirs(&delta).is_err() {
1795+
reply.error(libc::EIO);
1796+
return;
1797+
}
1798+
match std::os::unix::fs::symlink(target, &delta) {
1799+
Ok(()) => {
1800+
if self.is_stale() {
1801+
let _ = std::fs::remove_file(&delta);
1802+
reply.error(libc::ESTALE);
1803+
return;
1804+
}
1805+
let ino = self.inodes.get_or_create(&path, false);
1806+
match self.make_attr(ino, &delta) {
1807+
Some(attr) => reply.entry(&TTL, &attr, 0),
1808+
None => reply.error(libc::EIO),
1809+
}
1810+
}
1811+
Err(_) => reply.error(libc::EIO),
1812+
}
1813+
}
1814+
_ => {
1815+
reply.error(libc::ENOENT);
1816+
}
1817+
}
1818+
}
1819+
}
16821820
}

src/fs_helpers.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@ impl BranchFs {
5050
) -> std::io::Result<std::path::PathBuf> {
5151
let delta = self.get_delta_path_for_branch(branch, rel_path);
5252

53-
if !delta.exists() {
53+
if delta.symlink_metadata().is_err() {
5454
if let Some(src) = self.resolve_for_branch(branch, rel_path) {
55-
if src.exists() && src.is_file() {
56-
storage::copy_file(&src, &delta)
57-
.map_err(|e| std::io::Error::other(e.to_string()))?;
55+
if let Ok(meta) = src.symlink_metadata() {
56+
if meta.file_type().is_symlink() || meta.file_type().is_file() {
57+
storage::copy_entry(&src, &delta)
58+
.map_err(|e| std::io::Error::other(e.to_string()))?;
59+
}
5860
}
5961
}
6062
}
@@ -65,7 +67,7 @@ impl BranchFs {
6567
}
6668

6769
pub(crate) fn make_attr(&self, ino: u64, path: &Path) -> Option<FileAttr> {
68-
let meta = std::fs::metadata(path).ok()?;
70+
let meta = std::fs::symlink_metadata(path).ok()?;
6971
let kind = if meta.is_dir() {
7072
FileType::Directory
7173
} else if meta.is_symlink() {
@@ -172,9 +174,13 @@ impl BranchFs {
172174
format!("{}/{}", rel_path, name)
173175
};
174176
let inode_path = format!("{}{}", inode_prefix, child_rel);
175-
let is_dir = entry.path().is_dir();
177+
let ft = entry.file_type();
178+
let is_symlink = ft.as_ref().map(|t| t.is_symlink()).unwrap_or(false);
179+
let is_dir = !is_symlink && ft.as_ref().map(|t| t.is_dir()).unwrap_or(false);
176180
let child_ino = self.inodes.get_or_create(&inode_path, is_dir);
177-
let kind = if is_dir {
181+
let kind = if is_symlink {
182+
FileType::Symlink
183+
} else if is_dir {
178184
FileType::Directory
179185
} else {
180186
FileType::RegularFile
@@ -197,9 +203,14 @@ impl BranchFs {
197203
format!("{}/{}", rel_path, name)
198204
};
199205
let inode_path = format!("{}{}", inode_prefix, child_rel);
200-
let is_dir = entry.path().is_dir();
206+
let ft = entry.file_type();
207+
let is_symlink = ft.as_ref().map(|t| t.is_symlink()).unwrap_or(false);
208+
let is_dir =
209+
!is_symlink && ft.as_ref().map(|t| t.is_dir()).unwrap_or(false);
201210
let child_ino = self.inodes.get_or_create(&inode_path, is_dir);
202-
let kind = if is_dir {
211+
let kind = if is_symlink {
212+
FileType::Symlink
213+
} else if is_dir {
203214
FileType::Directory
204215
} else {
205216
FileType::RegularFile

src/storage.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@ pub fn copy_file(src: &Path, dst: &Path) -> Result<()> {
1717
Ok(())
1818
}
1919

20+
/// Symlink-aware copy: if `src` is a symlink, recreate the symlink at `dst`;
21+
/// otherwise fall back to a regular file copy.
22+
pub fn copy_entry(src: &Path, dst: &Path) -> Result<()> {
23+
ensure_parent_dirs(dst)?;
24+
let meta = src.symlink_metadata()?;
25+
if meta.file_type().is_symlink() {
26+
let target = fs::read_link(src)?;
27+
// Remove any pre-existing entry at dst so symlink() won't fail
28+
let _ = fs::remove_file(dst);
29+
std::os::unix::fs::symlink(&target, dst)?;
30+
} else {
31+
fs::copy(src, dst)?;
32+
}
33+
Ok(())
34+
}
35+
2036
pub fn read_file(path: &Path) -> Result<Vec<u8>> {
2137
let mut file = File::open(path)?;
2238
let mut buf = Vec::new();

0 commit comments

Comments
 (0)