Skip to main content

st/formatters/
sse.rs

1//! Server-Sent Events (SSE) formatter
2//!
3//! Streams directory changes and updates as SSE events
4
5use crate::scanner::{FileNode, TreeStats};
6use anyhow::Result;
7use serde_json;
8use std::io::Write;
9use std::path::Path;
10
11use super::{Formatter, StreamingFormatter};
12
13pub struct SseFormatter {
14    event_id: u64,
15}
16
17impl Default for SseFormatter {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl SseFormatter {
24    pub fn new() -> Self {
25        Self { event_id: 0 }
26    }
27
28    fn next_event_id(&mut self) -> u64 {
29        self.event_id += 1;
30        self.event_id
31    }
32
33    fn write_event(
34        &self,
35        writer: &mut dyn Write,
36        event_type: &str,
37        data: &serde_json::Value,
38        id: u64,
39    ) -> Result<()> {
40        writeln!(writer, "id: {}", id)?;
41        writeln!(writer, "event: {}", event_type)?;
42        writeln!(writer, "data: {}", serde_json::to_string(data)?)?;
43        writeln!(writer)?; // Empty line to end the event
44        writer.flush()?;
45        Ok(())
46    }
47}
48
49impl Formatter for SseFormatter {
50    fn format(
51        &self,
52        writer: &mut dyn Write,
53        nodes: &[FileNode],
54        stats: &TreeStats,
55        root_path: &Path,
56    ) -> Result<()> {
57        let mut formatter = SseFormatter::new();
58
59        // Send initial scan event
60        let scan_event = serde_json::json!({
61            "type": "scan_complete",
62            "path": root_path.display().to_string(),
63            "stats": {
64                "total_files": stats.total_files,
65                "total_dirs": stats.total_dirs,
66                "total_size": stats.total_size,
67            }
68        });
69        let id = formatter.next_event_id();
70        formatter.write_event(writer, "scan", &scan_event, id)?;
71
72        // Send node events
73        for node in nodes {
74            let node_event = serde_json::json!({
75                "type": "node",
76                "node": {
77                    "name": node.path.file_name().unwrap_or(node.path.as_os_str()).to_string_lossy(),
78                    "path": node.path.display().to_string(),
79                    "is_dir": node.is_dir,
80                    "size": node.size,
81                    "depth": node.depth,
82                }
83            });
84            let id = formatter.next_event_id();
85            formatter.write_event(writer, "node", &node_event, id)?;
86        }
87
88        // Send completion event
89        let complete_event = serde_json::json!({
90            "type": "format_complete",
91            "node_count": nodes.len(),
92        });
93        let id = formatter.next_event_id();
94        formatter.write_event(writer, "complete", &complete_event, id)?;
95
96        Ok(())
97    }
98}
99
100impl StreamingFormatter for SseFormatter {
101    fn start_stream(&self, writer: &mut dyn Write, root_path: &Path) -> Result<()> {
102        // Send HTTP headers for SSE
103        writeln!(writer, "HTTP/1.1 200 OK")?;
104        writeln!(writer, "Content-Type: text/event-stream")?;
105        writeln!(writer, "Cache-Control: no-cache")?;
106        writeln!(writer, "Connection: keep-alive")?;
107        writeln!(writer, "Access-Control-Allow-Origin: *")?;
108        writeln!(writer)?; // Empty line to end headers
109
110        // Send initial connection event
111        let mut formatter = SseFormatter::new();
112        let init_event = serde_json::json!({
113            "type": "stream_start",
114            "path": root_path.display().to_string(),
115            "timestamp": chrono::Utc::now().to_rfc3339(),
116        });
117        let id = formatter.next_event_id();
118        formatter.write_event(writer, "init", &init_event, id)?;
119
120        Ok(())
121    }
122
123    fn format_node(
124        &self,
125        writer: &mut dyn Write,
126        node: &FileNode,
127        _root_path: &Path,
128    ) -> Result<()> {
129        let mut formatter = SseFormatter::new();
130
131        let node_event = serde_json::json!({
132            "type": "node_discovered",
133            "node": {
134                "name": node.path.file_name().unwrap_or(node.path.as_os_str()).to_string_lossy(),
135                "path": node.path.display().to_string(),
136                "is_dir": node.is_dir,
137                "size": node.size,
138                "depth": node.depth,
139                "permissions": format!("{:o}", node.permissions),
140                "modified": chrono::DateTime::<chrono::Utc>::from(node.modified).to_rfc3339(),
141            }
142        });
143
144        let id = formatter.next_event_id();
145        formatter.write_event(writer, "node", &node_event, id)?;
146        Ok(())
147    }
148
149    fn end_stream(
150        &self,
151        writer: &mut dyn Write,
152        stats: &TreeStats,
153        root_path: &Path,
154    ) -> Result<()> {
155        let mut formatter = SseFormatter::new();
156
157        // Send final statistics
158        let stats_event = serde_json::json!({
159            "type": "stream_complete",
160            "path": root_path.display().to_string(),
161            "stats": {
162                "total_files": stats.total_files,
163                "total_dirs": stats.total_dirs,
164                "total_size": stats.total_size,
165            },
166            "timestamp": chrono::Utc::now().to_rfc3339(),
167        });
168
169        let id = formatter.next_event_id();
170        formatter.write_event(writer, "complete", &stats_event, id)?;
171
172        // Send close event
173        let close_event = serde_json::json!({
174            "type": "stream_close",
175            "reason": "scan_complete",
176        });
177        let id = formatter.next_event_id();
178        formatter.write_event(writer, "close", &close_event, id)?;
179
180        Ok(())
181    }
182}