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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ build: check-deps check-pip
fi; \
if [ -n "$$MATURIN_CMD" ]; then \
TMPDIR=$$(mktemp -d); \
cd crates/ragfs-python && $$MATURIN_CMD build --release --out "$$TMPDIR" 2>&1; \
cd crates/ragfs-python && $$MATURIN_CMD build --release --features s3 --out "$$TMPDIR" 2>&1; \
cd ../..; \
mkdir -p openviking/lib; \
echo "import zipfile, glob, shutil, os, sys" > /tmp/extract_ragfs.py; \
Expand Down Expand Up @@ -136,4 +136,4 @@ clean:
done
@find . -name "*.pyc" -delete
@find . -name "__pycache__" -type d -exec rm -rf {} +
@echo "Cleanup completed."
@echo "Cleanup completed."
4 changes: 4 additions & 0 deletions crates/ragfs-python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ publish = false
name = "ragfs_python"
crate-type = ["cdylib"]

[features]
default = ["s3"]
s3 = ["ragfs/s3"]

[dependencies]
ragfs = { path = "../ragfs" }
pyo3 = { version = "0.27", features = ["extension-module"] }
Expand Down
152 changes: 93 additions & 59 deletions crates/ragfs-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ use std::sync::Arc;
use std::time::UNIX_EPOCH;

use ragfs::core::{ConfigValue, FileInfo, FileSystem, MountableFS, PluginConfig, WriteFlag};
use ragfs::plugins::{KVFSPlugin, LocalFSPlugin, MemFSPlugin, QueueFSPlugin, ServerInfoFSPlugin, SQLFSPlugin};
#[cfg(feature = "s3")]
use ragfs::plugins::S3FSPlugin;
use ragfs::plugins::{
KVFSPlugin, LocalFSPlugin, MemFSPlugin, QueueFSPlugin, SQLFSPlugin, ServerInfoFSPlugin,
};

/// Convert a ragfs error into a Python RuntimeError
fn to_py_err(e: ragfs::core::Error) -> PyErr {
Expand Down Expand Up @@ -125,6 +129,8 @@ impl RAGFSBindingClient {
fs.register_plugin(SQLFSPlugin::new()).await;
fs.register_plugin(LocalFSPlugin::new()).await;
fs.register_plugin(ServerInfoFSPlugin::new()).await;
#[cfg(feature = "s3")]
fs.register_plugin(S3FSPlugin::new()).await;
});

Ok(Self { fs, rt })
Expand All @@ -141,9 +147,24 @@ impl RAGFSBindingClient {
fn get_capabilities(&self) -> PyResult<HashMap<String, Py<PyAny>>> {
Python::attach(|py| {
let mut m = HashMap::new();
m.insert("version".to_string(), "ragfs-python".into_pyobject(py)?.into_any().unbind());
let features = vec!["memfs", "kvfs", "queuefs", "sqlfs"];
m.insert("features".to_string(), features.into_pyobject(py)?.into_any().unbind());
m.insert(
"version".to_string(),
"ragfs-python".into_pyobject(py)?.into_any().unbind(),
);
let mut features = vec![
"memfs",
"kvfs",
"queuefs",
"sqlfs",
"localfs",
"serverinfofs",
];
#[cfg(feature = "s3")]
features.push("s3fs");
m.insert(
"features".to_string(),
features.into_pyobject(py)?.into_any().unbind(),
);
Ok(m)
})
}
Expand All @@ -154,9 +175,10 @@ impl RAGFSBindingClient {
/// name, size, mode, modTime, isDir
fn ls(&self, path: String) -> PyResult<Py<PyAny>> {
let fs = self.fs.clone();
let entries = self.rt.block_on(async move {
fs.read_dir(&path).await
}).map_err(to_py_err)?;
let entries = self
.rt
.block_on(async move { fs.read_dir(&path).await })
.map_err(to_py_err)?;

Python::attach(|py| {
let list = PyList::empty(py);
Expand Down Expand Up @@ -187,13 +209,12 @@ impl RAGFSBindingClient {
let off = if offset < 0 { 0u64 } else { offset as u64 };
let sz = if size < 0 { 0u64 } else { size as u64 };

let data = self.rt.block_on(async move {
fs.read(&path, off, sz).await
}).map_err(to_py_err)?;
let data = self
.rt
.block_on(async move { fs.read(&path, off, sz).await })
.map_err(to_py_err)?;

Python::attach(|py| {
Ok(PyBytes::new(py, &data).into())
})
Python::attach(|py| Ok(PyBytes::new(py, &data).into()))
}

/// Read file content (alias for read).
Expand All @@ -212,19 +233,19 @@ impl RAGFSBindingClient {
let _ = max_retries; // not applicable for local binding
let fs = self.fs.clone();
let len = data.len();
self.rt.block_on(async move {
fs.write(&path, &data, 0, WriteFlag::Create).await
}).map_err(to_py_err)?;
self.rt
.block_on(async move { fs.write(&path, &data, 0, WriteFlag::Create).await })
.map_err(to_py_err)?;

Ok(format!("Written {} bytes", len))
}

/// Create a new empty file.
fn create(&self, path: String) -> PyResult<HashMap<String, String>> {
let fs = self.fs.clone();
self.rt.block_on(async move {
fs.create(&path).await
}).map_err(to_py_err)?;
self.rt
.block_on(async move { fs.create(&path).await })
.map_err(to_py_err)?;

let mut m = HashMap::new();
m.insert("message".to_string(), "created".to_string());
Expand All @@ -238,9 +259,9 @@ impl RAGFSBindingClient {
.map_err(|e| PyRuntimeError::new_err(format!("Invalid mode '{}': {}", mode, e)))?;

let fs = self.fs.clone();
self.rt.block_on(async move {
fs.mkdir(&path, mode_int).await
}).map_err(to_py_err)?;
self.rt
.block_on(async move { fs.mkdir(&path, mode_int).await })
.map_err(to_py_err)?;

let mut m = HashMap::new();
m.insert("message".to_string(), "created".to_string());
Expand All @@ -251,13 +272,15 @@ impl RAGFSBindingClient {
#[pyo3(signature = (path, recursive=false))]
fn rm(&self, path: String, recursive: bool) -> PyResult<HashMap<String, String>> {
let fs = self.fs.clone();
self.rt.block_on(async move {
if recursive {
fs.remove_all(&path).await
} else {
fs.remove(&path).await
}
}).map_err(to_py_err)?;
self.rt
.block_on(async move {
if recursive {
fs.remove_all(&path).await
} else {
fs.remove(&path).await
}
})
.map_err(to_py_err)?;

let mut m = HashMap::new();
m.insert("message".to_string(), "deleted".to_string());
Expand All @@ -267,9 +290,10 @@ impl RAGFSBindingClient {
/// Get file/directory information.
fn stat(&self, path: String) -> PyResult<Py<PyAny>> {
let fs = self.fs.clone();
let info = self.rt.block_on(async move {
fs.stat(&path).await
}).map_err(to_py_err)?;
let info = self
.rt
.block_on(async move { fs.stat(&path).await })
.map_err(to_py_err)?;

Python::attach(|py| {
let dict = file_info_to_py_dict(py, &info)?;
Expand All @@ -280,9 +304,9 @@ impl RAGFSBindingClient {
/// Rename/move a file or directory.
fn mv(&self, old_path: String, new_path: String) -> PyResult<HashMap<String, String>> {
let fs = self.fs.clone();
self.rt.block_on(async move {
fs.rename(&old_path, &new_path).await
}).map_err(to_py_err)?;
self.rt
.block_on(async move { fs.rename(&old_path, &new_path).await })
.map_err(to_py_err)?;

let mut m = HashMap::new();
m.insert("message".to_string(), "renamed".to_string());
Expand All @@ -292,9 +316,9 @@ impl RAGFSBindingClient {
/// Change file permissions.
fn chmod(&self, path: String, mode: u32) -> PyResult<HashMap<String, String>> {
let fs = self.fs.clone();
self.rt.block_on(async move {
fs.chmod(&path, mode).await
}).map_err(to_py_err)?;
self.rt
.block_on(async move { fs.chmod(&path, mode).await })
.map_err(to_py_err)?;

let mut m = HashMap::new();
m.insert("message".to_string(), "chmod ok".to_string());
Expand All @@ -304,16 +328,18 @@ impl RAGFSBindingClient {
/// Touch a file (create if not exists, or update timestamp).
fn touch(&self, path: String) -> PyResult<HashMap<String, String>> {
let fs = self.fs.clone();
self.rt.block_on(async move {
// Try create; if already exists, write empty to update mtime
match fs.create(&path).await {
Ok(_) => Ok(()),
Err(_) => {
// File exists, write empty bytes to update timestamp
fs.write(&path, &[], 0, WriteFlag::None).await.map(|_| ())
self.rt
.block_on(async move {
// Try create; if already exists, write empty to update mtime
match fs.create(&path).await {
Ok(_) => Ok(()),
Err(_) => {
// File exists, write empty bytes to update timestamp
fs.write(&path, &[], 0, WriteFlag::None).await.map(|_| ())
}
}
}
}).map_err(to_py_err)?;
})
.map_err(to_py_err)?;

let mut m = HashMap::new();
m.insert("message".to_string(), "touched".to_string());
Expand All @@ -323,9 +349,7 @@ impl RAGFSBindingClient {
/// List all mounted plugins.
fn mounts(&self) -> PyResult<Vec<HashMap<String, String>>> {
let fs = self.fs.clone();
let mount_list = self.rt.block_on(async move {
fs.list_mounts().await
});
let mount_list = self.rt.block_on(async move { fs.list_mounts().await });

let result: Vec<HashMap<String, String>> = mount_list
.into_iter()
Expand Down Expand Up @@ -365,9 +389,9 @@ impl RAGFSBindingClient {
};

let fs = self.fs.clone();
self.rt.block_on(async move {
fs.mount(plugin_config).await
}).map_err(to_py_err)?;
self.rt
.block_on(async move { fs.mount(plugin_config).await })
.map_err(to_py_err)?;

let mut m = HashMap::new();
m.insert(
Expand All @@ -381,9 +405,9 @@ impl RAGFSBindingClient {
fn unmount(&self, path: String) -> PyResult<HashMap<String, String>> {
let fs = self.fs.clone();
let path_clone = path.clone();
self.rt.block_on(async move {
fs.unmount(&path_clone).await
}).map_err(to_py_err)?;
self.rt
.block_on(async move { fs.unmount(&path_clone).await })
.map_err(to_py_err)?;

let mut m = HashMap::new();
m.insert("message".to_string(), format!("unmounted {}", path));
Expand All @@ -393,14 +417,17 @@ impl RAGFSBindingClient {
/// List all registered plugin names.
fn list_plugins(&self) -> PyResult<Vec<String>> {
// Return names of built-in plugins
Ok(vec![
let mut plugins = vec![
"memfs".to_string(),
"kvfs".to_string(),
"queuefs".to_string(),
"sqlfs".to_string(),
"localfs".to_string(),
"serverinfofs".to_string(),
])
];
#[cfg(feature = "s3")]
plugins.push("s3fs".to_string());
Ok(plugins)
}

/// Get detailed plugin information.
Expand Down Expand Up @@ -433,7 +460,14 @@ impl RAGFSBindingClient {
stream: bool,
node_limit: Option<i32>,
) -> PyResult<Py<PyAny>> {
let _ = (path, pattern, recursive, case_insensitive, stream, node_limit);
let _ = (
path,
pattern,
recursive,
case_insensitive,
stream,
node_limit,
);
Err(PyRuntimeError::new_err(
"grep not yet implemented in ragfs-python",
))
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ def build_ragfs_python_artifact(self):
"maturin",
"build",
"--release",
"--features",
"s3",
"--out",
tmpdir,
]
Expand Down
2 changes: 1 addition & 1 deletion tests/agfs/test_fs_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from openviking_cli.utils.config.agfs_config import AGFSConfig

# Direct configuration for testing
AGFS_CONF = AGFSConfig(path="/tmp/ov-test", backend="local", mode="binding-client")
AGFS_CONF = AGFSConfig(path="/tmp/ov-test", backend="local")

# clean up test directory if it exists
if os.path.exists(AGFS_CONF.path):
Expand Down
Loading