Ctrl K
ComunidadGuías…

sdk/connector-sdk/examples/file_system.rs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//! Example: FileSystemConnector
//!
//! Syncs all `.txt` and `.pdf` files from a local directory to GCTRL.
//!
//! Run with:
//!   cargo run --example file_system --manifest-path sdk/connector-sdk/Cargo.toml
//!
//! Set GCTRL_API_URL and GCTRL_API_KEY environment variables, or edit the
//! constants at the bottom of this file.

use std::{
    path::{Path, PathBuf},
    time::UNIX_EPOCH,
};

use async_trait::async_trait;
use chrono::{DateTime, Utc};
use gctrl_connector_sdk::{
    AuthTokens, ConnectorContent, ConnectorContentType, ConnectorFile, ConnectorResult,
    GctrlClient, GctrlConnector,
};

// ---------------------------------------------------------------------------
// FileSystemConnector
// ---------------------------------------------------------------------------

/// A simple connector that reads files from a local directory.
///
/// Only `.txt` and `.pdf` extensions are synced; everything else is skipped.
struct FileSystemConnector {
    root_path: PathBuf,
}

impl FileSystemConnector {
    fn new(root_path: impl Into<PathBuf>) -> Self {
        Self { root_path: root_path.into() }
    }

    /// Determine MIME type from file extension (minimal, no external deps).
    fn mime_for_path(path: &Path) -> &'static str {
        match path.extension().and_then(|e| e.to_str()).unwrap_or("") {
            "txt" | "text" | "md" => "text/plain",
            "pdf" => "application/pdf",
            "html" | "htm" => "text/html",
            "json" => "application/json",
            "csv" => "text/csv",
            _ => "application/octet-stream",
        }
    }

    /// Check whether a file extension is one we want to sync.
    fn is_supported(path: &Path) -> bool {
        matches!(
            path.extension().and_then(|e| e.to_str()).unwrap_or(""),
            "txt" | "text" | "md" | "pdf" | "html" | "htm" | "json" | "csv"
        )
    }
}

#[async_trait]
impl GctrlConnector for FileSystemConnector {
    fn name(&self) -> &str {
        "FileSystem Connector"
    }

    fn provider(&self) -> &str {
        "filesystem"
    }

    async fn list_files(&self, folder_id: Option<&str>) -> ConnectorResult<Vec<ConnectorFile>> {
        // If a sub-folder is requested, resolve it relative to root; otherwise use root.
        let base: PathBuf = match folder_id {
            Some(rel) => self.root_path.join(rel),
            None => self.root_path.clone(),
        };

        if !base.exists() {
            return Err(gctrl_connector_sdk::ConnectorError::NotFound(
                base.display().to_string(),
            ));
        }

        let mut entries = Vec::new();

        let read_dir = std::fs::read_dir(&base)
            .map_err(|e| gctrl_connector_sdk::ConnectorError::Io(e.to_string()))?;

        for entry in read_dir.flatten() {
            let path = entry.path();
            let meta = match entry.metadata() {
                Ok(m) => m,
                Err(_) => continue,
            };

            let is_folder = meta.is_dir();

            // Skip unsupported file types (still include folders so callers can recurse)
            if !is_folder && !Self::is_supported(&path) {
                continue;
            }

            // Use the path relative to root as the stable ID
            let id = path
                .strip_prefix(&self.root_path)
                .unwrap_or(&path)
                .display()
                .to_string()
                // Normalise separators for portability
                .replace('\\', "/");

            let name = path
                .file_name()
                .map(|n| n.to_string_lossy().into_owned())
                .unwrap_or_else(|| id.clone());

            let size_bytes = if meta.is_file() { Some(meta.len()) } else { None };

            let modified_at: Option<DateTime<Utc>> = meta
                .modified()
                .ok()
                .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
                .map(|d| DateTime::from_timestamp(d.as_secs() as i64, 0).unwrap_or_default());

            // Parent is the folder_id that was passed in, or None for root items.
            let parent_id = folder_id.map(str::to_owned);

            let mime_type = if is_folder {
                "inode/directory".to_owned()
            } else {
                Self::mime_for_path(&path).to_owned()
            };

            entries.push(ConnectorFile {
                id,
                name,
                mime_type,
                size_bytes,
                modified_at,
                is_folder,
                parent_id,
            });
        }

        Ok(entries)
    }

    async fn fetch_content(&self, file_id: &str) -> ConnectorResult<ConnectorContent> {
        let path = self.root_path.join(file_id.replace('/', std::path::MAIN_SEPARATOR_STR));

        if !path.exists() {
            return Err(gctrl_connector_sdk::ConnectorError::NotFound(
                path.display().to_string(),
            ));
        }

        let file_name = path
            .file_name()
            .map(|n| n.to_string_lossy().into_owned())
            .unwrap_or_else(|| file_id.to_owned());

        let mime_type = Self::mime_for_path(&path).to_owned();

        // Read content: try UTF-8 first for text types; fall back to raw bytes.
        let content = if mime_type.starts_with("text/") || mime_type == "application/json" {
            let text = std::fs::read_to_string(&path)
                .map_err(|e| gctrl_connector_sdk::ConnectorError::Io(e.to_string()))?;
            ConnectorContentType::Text(text)
        } else {
            let bytes = std::fs::read(&path)
                .map_err(|e| gctrl_connector_sdk::ConnectorError::Io(e.to_string()))?;
            ConnectorContentType::Bytes(bytes)
        };

        Ok(ConnectorContent {
            file_id: file_id.to_owned(),
            file_name,
            content,
            mime_type,
        })
    }

    /// No-op: filesystem connector doesn't use OAuth.
    async fn on_auth_complete(&self, _tokens: &AuthTokens) -> ConnectorResult<()> {
        Ok(())
    }
}

// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------

#[tokio::main]
async fn main() {
    // Initialise basic logging so tracing output is visible.
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::from_default_env()
                .add_directive("gctrl_connector_sdk=debug".parse().unwrap()),
        )
        .init();

    // Configuration — override via env vars or edit these constants.
    let api_url = std::env::var("GCTRL_API_URL")
        .unwrap_or_else(|_| "http://localhost:4000".to_string());
    let api_key = std::env::var("GCTRL_API_KEY")
        .unwrap_or_else(|_| "your-api-key".to_string());
    let docs_dir = std::env::var("GCTRL_SYNC_DIR")
        .unwrap_or_else(|_| "./docs".to_string());
    let compilation_id = std::env::var("GCTRL_COMPILATION_ID").ok();

    // Create connector and client
    let connector = FileSystemConnector::new(&docs_dir);
    let client = GctrlClient::new(&api_url, &api_key);

    println!("GCTRL FileSystem Connector");
    println!("  Source dir    : {}", docs_dir);
    println!("  GCTRL API     : {}", api_url);
    println!(
        "  Compilation   : {}",
        compilation_id.as_deref().unwrap_or("(none — transient)")
    );
    println!();

    match client
        .sync_connector(&connector, None, compilation_id.as_deref())
        .await
    {
        Ok(result) => {
            println!("Sync complete:");
            println!("  Total files : {}", result.total);
            println!("  Synced      : {}", result.synced);
            println!("  Failed      : {}", result.failed);
            if !result.errors.is_empty() {
                println!("  Errors:");
                for e in &result.errors {
                    println!("    - {}", e);
                }
            }
        }
        Err(e) => {
            eprintln!("Sync failed: {}", e);
            std::process::exit(1);
        }
    }
}