chore: initial commit for phase04
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::error::DaemonError;
|
||||
|
||||
/// Filesystem operations with path jail enforcement.
|
||||
pub struct FileSystem {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl FileSystem {
|
||||
pub fn new(root: PathBuf) -> Self {
|
||||
Self { root }
|
||||
}
|
||||
|
||||
/// Resolve a relative path within the jail. Prevents path traversal.
|
||||
fn resolve(&self, relative: &str) -> Result<PathBuf, DaemonError> {
|
||||
let clean = relative.trim_start_matches('/');
|
||||
let resolved = self.root.join(clean);
|
||||
|
||||
// Canonicalize both to compare (handle .. and symlinks)
|
||||
// For non-existent paths, check the parent
|
||||
let check_path = if resolved.exists() {
|
||||
resolved.canonicalize().map_err(DaemonError::Io)?
|
||||
} else {
|
||||
let parent = resolved
|
||||
.parent()
|
||||
.ok_or_else(|| DaemonError::PathTraversal(relative.to_string()))?;
|
||||
if !parent.exists() {
|
||||
// Parent doesn't exist either — check the root prefix
|
||||
let normalized = self.root.join(clean);
|
||||
if !normalized.starts_with(&self.root) {
|
||||
return Err(DaemonError::PathTraversal(relative.to_string()));
|
||||
}
|
||||
return Ok(normalized);
|
||||
}
|
||||
let canonical_parent = parent.canonicalize().map_err(DaemonError::Io)?;
|
||||
canonical_parent.join(resolved.file_name().unwrap_or_default())
|
||||
};
|
||||
|
||||
let canonical_root = self.root.canonicalize().unwrap_or_else(|_| self.root.clone());
|
||||
if !check_path.starts_with(&canonical_root) {
|
||||
return Err(DaemonError::PathTraversal(relative.to_string()));
|
||||
}
|
||||
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
/// List files in a directory.
|
||||
pub async fn list_files(&self, path: &str) -> Result<Vec<FileEntry>, DaemonError> {
|
||||
let resolved = self.resolve(path)?;
|
||||
let mut entries = Vec::new();
|
||||
|
||||
let mut reader = fs::read_dir(&resolved).await.map_err(DaemonError::Io)?;
|
||||
while let Some(entry) = reader.next_entry().await.map_err(DaemonError::Io)? {
|
||||
let metadata = entry.metadata().await.map_err(DaemonError::Io)?;
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
let relative_path = format!(
|
||||
"{}/{}",
|
||||
path.trim_end_matches('/'),
|
||||
&name
|
||||
);
|
||||
|
||||
entries.push(FileEntry {
|
||||
name,
|
||||
path: relative_path,
|
||||
is_directory: metadata.is_dir(),
|
||||
size: metadata.len() as i64,
|
||||
modified_at: metadata
|
||||
.modified()
|
||||
.ok()
|
||||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0),
|
||||
});
|
||||
}
|
||||
|
||||
entries.sort_by(|a, b| {
|
||||
// Directories first, then by name
|
||||
b.is_directory.cmp(&a.is_directory).then(a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Read file contents.
|
||||
pub async fn read_file(&self, path: &str) -> Result<Vec<u8>, DaemonError> {
|
||||
let resolved = self.resolve(path)?;
|
||||
debug!(path = %resolved.display(), "Reading file");
|
||||
fs::read(&resolved).await.map_err(DaemonError::Io)
|
||||
}
|
||||
|
||||
/// Write file contents.
|
||||
pub async fn write_file(&self, path: &str, data: &[u8]) -> Result<(), DaemonError> {
|
||||
let resolved = self.resolve(path)?;
|
||||
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = resolved.parent() {
|
||||
fs::create_dir_all(parent).await.map_err(DaemonError::Io)?;
|
||||
}
|
||||
|
||||
debug!(path = %resolved.display(), "Writing file");
|
||||
fs::write(&resolved, data).await.map_err(DaemonError::Io)
|
||||
}
|
||||
|
||||
/// Delete files or directories.
|
||||
pub async fn delete_paths(&self, paths: &[String]) -> Result<(), DaemonError> {
|
||||
for path in paths {
|
||||
let resolved = self.resolve(path)?;
|
||||
if resolved.is_dir() {
|
||||
fs::remove_dir_all(&resolved).await.map_err(DaemonError::Io)?;
|
||||
} else {
|
||||
fs::remove_file(&resolved).await.map_err(DaemonError::Io)?;
|
||||
}
|
||||
debug!(path = %resolved.display(), "Deleted");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileEntry {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub is_directory: bool,
|
||||
pub size: i64,
|
||||
pub modified_at: i64,
|
||||
}
|
||||
Reference in New Issue
Block a user