Skip to content
Merged
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
183 changes: 160 additions & 23 deletions cli/src/fuse.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use agentfs_sdk::{FileSystem, FsError, Stats};
use fuser::{
consts::FUSE_WRITEBACK_CACHE, FileAttr, FileType, Filesystem, KernelConfig, MountOption,
ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyOpen,
ReplyStatfs, ReplyWrite, Request,
ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyDirectoryPlus, ReplyEmpty, ReplyEntry,
ReplyOpen, ReplyStatfs, ReplyWrite, Request,
};
use parking_lot::Mutex;
use std::{
Expand Down Expand Up @@ -187,6 +187,9 @@ impl Filesystem for AgentFSFuse {
///
/// Returns "." and ".." entries followed by the directory contents.
/// Each entry's inode is cached for subsequent lookups.
///
/// Uses readdir_plus to fetch entries with stats in a single query,
/// avoiding N+1 database queries.
fn readdir(
&mut self,
_req: &Request,
Expand All @@ -202,7 +205,7 @@ impl Filesystem for AgentFSFuse {

let fs = self.fs.clone();
let (entries_result, path) = self.runtime.block_on(async move {
let result = fs.readdir(&path).await;
let result = fs.readdir_plus(&path).await;
(result, path)
});

Expand Down Expand Up @@ -253,31 +256,24 @@ impl Filesystem for AgentFSFuse {
(parent_ino, FileType::Directory, ".."),
];

for entry_name in &entries {
// Process entries with stats already available (no N+1 queries!)
for entry in &entries {
let entry_path = if path == "/" {
format!("/{}", entry_name)
format!("/{}", entry.name)
} else {
format!("{}/{}", path, entry_name)
format!("{}/{}", path, entry.name)
};

let fs = self.fs.clone();
let (stats_result, entry_path) = self.runtime.block_on(async move {
let result = fs.stat(&entry_path).await;
(result, entry_path)
});

if let Ok(Some(stats)) = stats_result {
let kind = if stats.is_directory() {
FileType::Directory
} else if stats.is_symlink() {
FileType::Symlink
} else {
FileType::RegularFile
};
let kind = if entry.stats.is_directory() {
FileType::Directory
} else if entry.stats.is_symlink() {
FileType::Symlink
} else {
FileType::RegularFile
};

self.add_path(stats.ino as u64, entry_path);
all_entries.push((stats.ino as u64, kind, entry_name.as_str()));
}
self.add_path(entry.stats.ino as u64, entry_path);
all_entries.push((entry.stats.ino as u64, kind, entry.name.as_str()));
}

for (i, entry) in all_entries.iter().enumerate().skip(offset as usize) {
Expand All @@ -288,6 +284,147 @@ impl Filesystem for AgentFSFuse {
reply.ok();
}

/// Reads directory entries with full attributes for the given inode.
///
/// This is an optimized version that returns both directory entries and
/// their attributes in a single call, reducing kernel/userspace round trips.
/// Uses readdir_plus to fetch entries with stats in a single database query.
fn readdirplus(
&mut self,
_req: &Request,
ino: u64,
_fh: u64,
offset: i64,
mut reply: ReplyDirectoryPlus,
) {
let Some(path) = self.get_path(ino) else {
reply.error(libc::ENOENT);
return;
};

let fs = self.fs.clone();
let (entries_result, path) = self.runtime.block_on(async move {
let result = fs.readdir_plus(&path).await;
(result, path)
});

let entries = match entries_result {
Ok(Some(entries)) => entries,
Ok(None) => {
reply.error(libc::ENOENT);
return;
}
Err(_) => {
reply.error(libc::EIO);
return;
}
};

// Get current directory stats for "."
let fs = self.fs.clone();
let path_for_stat = path.clone();
let dir_stats = self
.runtime
.block_on(async move { fs.stat(&path_for_stat).await })
.ok()
.flatten();

// Determine parent inode and stats for ".." entry
let (parent_ino, parent_stats) = if ino == 1 {
(1u64, dir_stats.clone()) // Root's parent is itself
} else {
let parent_path = Path::new(&path)
.parent()
.map(|p| {
let s = p.to_string_lossy().to_string();
if s.is_empty() {
"/".to_string()
} else {
s
}
})
.unwrap_or_else(|| "/".to_string());

if parent_path == "/" {
let fs = self.fs.clone();
let parent_stats = self
.runtime
.block_on(async move { fs.stat(&parent_path).await })
.ok()
.flatten();
(1u64, parent_stats)
} else {
let fs = self.fs.clone();
let parent_stats = self
.runtime
.block_on(async move { fs.stat(&parent_path).await })
.ok()
.flatten();
let parent_ino = parent_stats.as_ref().map(|s| s.ino as u64).unwrap_or(1);
(parent_ino, parent_stats)
}
};

// Build the entries list with full attributes
let uid = self.uid;
let gid = self.gid;

let mut offset_counter = 0i64;

// Add "." entry
if offset <= offset_counter {
if let Some(ref stats) = dir_stats {
let attr = fillattr(stats, uid, gid);
if reply.add(ino, offset_counter + 1, ".", &TTL, &attr, 0) {
reply.ok();
return;
}
}
}
offset_counter += 1;

// Add ".." entry
if offset <= offset_counter {
if let Some(ref stats) = parent_stats {
let attr = fillattr(stats, uid, gid);
if reply.add(parent_ino, offset_counter + 1, "..", &TTL, &attr, 0) {
reply.ok();
return;
}
}
}
offset_counter += 1;

// Add directory entries with their attributes
for entry in &entries {
if offset <= offset_counter {
let entry_path = if path == "/" {
format!("/{}", entry.name)
} else {
format!("{}/{}", path, entry.name)
};

let attr = fillattr(&entry.stats, uid, gid);
self.add_path(entry.stats.ino as u64, entry_path);

if reply.add(
entry.stats.ino as u64,
offset_counter + 1,
&entry.name,
&TTL,
&attr,
0,
) {
reply.ok();
return;
}
}
offset_counter += 1;
}

reply.ok();
}

/// Creates a new directory.
///
/// Creates a directory at `name` under `parent`, then stats it to return
Expand Down
107 changes: 105 additions & 2 deletions sdk/rust/src/filesystem/agentfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use std::time::{SystemTime, UNIX_EPOCH};
use turso::{Builder, Connection, Value};

use super::{
FileSystem, FilesystemStats, FsError, Stats, DEFAULT_DIR_MODE, DEFAULT_FILE_MODE, S_IFLNK,
S_IFMT,
DirEntry, FileSystem, FilesystemStats, FsError, Stats, DEFAULT_DIR_MODE, DEFAULT_FILE_MODE,
S_IFLNK, S_IFMT,
};

const ROOT_INO: i64 = 1;
Expand Down Expand Up @@ -1053,6 +1053,105 @@ impl AgentFS {
Ok(Some(entries))
}

/// List directory contents with full statistics (optimized batch query)
///
/// Returns entries with their stats in a single JOIN query, avoiding N+1 queries.
pub async fn readdir_plus(&self, path: &str) -> Result<Option<Vec<DirEntry>>> {
let ino = match self.resolve_path(path).await? {
Some(ino) => ino,
None => return Ok(None),
};

// Single JOIN query to get all entry names and their stats (including link count)
let mut rows = self
.conn
.query(
"SELECT d.name, i.ino, i.mode, i.uid, i.gid, i.size, i.atime, i.mtime, i.ctime,
(SELECT COUNT(*) FROM fs_dentry WHERE ino = i.ino) as nlink
FROM fs_dentry d
JOIN fs_inode i ON d.ino = i.ino
WHERE d.parent_ino = ?
ORDER BY d.name",
(ino,),
)
.await?;

let mut entries = Vec::new();
while let Some(row) = rows.next().await? {
let name = row
.get_value(0)
.ok()
.and_then(|v| {
if let Value::Text(s) = v {
Some(s.clone())
} else {
None
}
})
.unwrap_or_default();

if name.is_empty() {
continue;
}

let entry_ino = row
.get_value(1)
.ok()
.and_then(|v| v.as_integer().copied())
.unwrap_or(0);

let nlink = row
.get_value(9)
.ok()
.and_then(|v| v.as_integer().copied())
.unwrap_or(1) as u32;

let stats = Stats {
ino: entry_ino,
mode: row
.get_value(2)
.ok()
.and_then(|v| v.as_integer().copied())
.unwrap_or(0) as u32,
nlink,
uid: row
.get_value(3)
.ok()
.and_then(|v| v.as_integer().copied())
.unwrap_or(0) as u32,
gid: row
.get_value(4)
.ok()
.and_then(|v| v.as_integer().copied())
.unwrap_or(0) as u32,
size: row
.get_value(5)
.ok()
.and_then(|v| v.as_integer().copied())
.unwrap_or(0),
atime: row
.get_value(6)
.ok()
.and_then(|v| v.as_integer().copied())
.unwrap_or(0),
mtime: row
.get_value(7)
.ok()
.and_then(|v| v.as_integer().copied())
.unwrap_or(0),
ctime: row
.get_value(8)
.ok()
.and_then(|v| v.as_integer().copied())
.unwrap_or(0),
};

entries.push(DirEntry { name, stats });
}

Ok(Some(entries))
}

/// Create a symbolic link
pub async fn symlink(&self, target: &str, linkpath: &str) -> Result<()> {
let linkpath = self.normalize_path(linkpath);
Expand Down Expand Up @@ -1531,6 +1630,10 @@ impl FileSystem for AgentFS {
AgentFS::readdir(self, path).await
}

async fn readdir_plus(&self, path: &str) -> Result<Option<Vec<DirEntry>>> {
AgentFS::readdir_plus(self, path).await
}

async fn mkdir(&self, path: &str) -> Result<()> {
AgentFS::mkdir(self, path).await
}
Expand Down
Loading