browsing 0.1.4

Lightweight MCP/API for browser automation: navigate, get content (text), screenshot. Parallelism via RwLock.
Documentation
//! File operation action handlers
//!
//! Handlers for write_file and read_file actions.

use super::Handler;
use crate::agent::views::ActionResult;
use crate::error::{BrowsingError, Result};
use crate::tools::views::{ActionContext, ActionParams};
use async_trait::async_trait;
use std::path::Path;
use tokio::fs;
use tracing::info;

/// Handler for file operations
pub struct FileHandler;

#[async_trait]
impl Handler for FileHandler {
    async fn handle(
        &self,
        params: &ActionParams<'_>,
        _context: &mut ActionContext<'_>,
    ) -> Result<ActionResult> {
        let action_type = params.get_action_type().unwrap_or("unknown");

        match action_type {
            "write_file" => self.write_file(params).await,
            "read_file" => self.read_file(params).await,
            "replace_file" => self.replace_file(params).await,
            _ => Err(BrowsingError::Tool(format!(
                "Unknown file action: {action_type}"
            ))),
        }
    }
}

impl FileHandler {
    /// Write content to a file on disk
    async fn write_file(&self, params: &ActionParams<'_>) -> Result<ActionResult> {
        let path = params.get_required_str("path")?;
        let content = params.get_required_str("content")?;

        if path.contains("..") || path.contains('~') {
            return Err(BrowsingError::Tool(
                "Invalid file path: path traversal not allowed".into(),
            ));
        }

        let path = Path::new(path);

        // Ensure parent directory exists
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).await.map_err(|e| {
                BrowsingError::Tool(format!("Failed to create directory: {e}"))
            })?;
        }

        fs::write(path, content).await.map_err(|e| {
            BrowsingError::Tool(format!("Failed to write file: {e}"))
        })?;

        let memory = format!("Wrote {} bytes to file {}", content.len(), path.display());
        info!("[file] {}", memory);
        Ok(ActionResult {
            extracted_content: Some(format!(
                "Successfully wrote {} bytes to {}",
                content.len(),
                path.display()
            )),
            long_term_memory: Some(memory),
            ..Default::default()
        })
    }

    /// Read content from a file on disk
    async fn read_file(&self, params: &ActionParams<'_>) -> Result<ActionResult> {
        let path = params.get_required_str("path")?;

        if path.contains("..") || path.contains('~') {
            return Err(BrowsingError::Tool(
                "Invalid file path: path traversal not allowed".into(),
            ));
        }

        let path = Path::new(path);

        if !path.exists() {
            return Err(BrowsingError::Tool(format!(
                "File {} does not exist",
                path.display()
            )));
        }

        if !path.is_file() {
            return Err(BrowsingError::Tool(format!(
                "Path {} is not a file",
                path.display()
            )));
        }

        let content = fs::read_to_string(path).await.map_err(|e| {
            BrowsingError::Tool(format!("Failed to read file: {e}"))
        })?;

        let memory = format!(
            "Read {} bytes from file {}",
            content.len(),
            path.display()
        );
        info!("[file] {}", memory);
        Ok(ActionResult {
            extracted_content: Some(content.clone()),
            long_term_memory: Some(memory),
            ..Default::default()
        })
    }

    /// Replace text within a file on disk
    async fn replace_file(&self, params: &ActionParams<'_>) -> Result<ActionResult> {
        let path = params.get_required_str("path")?;
        let old_text = params.get_required_str("old_text")?;
        let new_text = params.get_required_str("new_text")?;

        if path.contains("..") || path.contains('~') {
            return Err(BrowsingError::Tool(
                "Invalid file path: path traversal not allowed".into(),
            ));
        }

        let path = Path::new(path);

        if !path.exists() {
            return Err(BrowsingError::Tool(format!(
                "File {} does not exist",
                path.display()
            )));
        }

        if !path.is_file() {
            return Err(BrowsingError::Tool(format!(
                "Path {} is not a file",
                path.display()
            )));
        }

        let content = fs::read_to_string(path).await.map_err(|e| {
            BrowsingError::Tool(format!("Failed to read file for replacement: {e}"))
        })?;

        let updated = content.replace(old_text, new_text);
        let replacements = content.matches(old_text).count();

        fs::write(path, updated).await.map_err(|e| {
            BrowsingError::Tool(format!("Failed to write replaced content: {e}"))
        })?;

        let memory = format!(
            "Replaced {} occurrence(s) of '{}' with '{}' in {}",
            replacements,
            old_text,
            new_text,
            path.display()
        );
        info!("[file] {}", memory);
        Ok(ActionResult {
            extracted_content: Some(format!("Replaced {replacements} occurrence(s)")),
            long_term_memory: Some(memory),
            ..Default::default()
        })
    }
}