livekit_data_stream/incoming/
stream_reader.rs1use crate::{
16 info::{AnyStreamInfo, ByteStreamInfo, TextStreamInfo},
17 utils::{StreamError, StreamProgress, StreamResult},
18};
19use bytes::{Bytes, BytesMut};
20use futures_util::{Stream, StreamExt};
21use std::{
22 fmt::Debug,
23 pin::Pin,
24 task::{Context, Poll},
25};
26use tokio::sync::{
27 mpsc::{self, UnboundedReceiver, UnboundedSender},
28 watch,
29};
30use tokio_stream::wrappers::WatchStream;
31
32pub trait StreamReader: Stream<Item = StreamResult<Self::Output>> {
38 type Output;
40
41 type Info;
43
44 fn info(&self) -> &Self::Info;
46
47 fn progress(&self) -> impl Stream<Item = StreamProgress>;
50
51 fn read_all(self) -> impl std::future::Future<Output = StreamResult<Self::Output>> + Send;
57}
58
59pub struct ByteStreamReader {
61 info: ByteStreamInfo,
62 chunk_rx: UnboundedReceiver<StreamResult<Bytes>>,
63 progress_rx: watch::Receiver<StreamProgress>,
64}
65
66pub struct TextStreamReader {
68 info: TextStreamInfo,
69 chunk_rx: UnboundedReceiver<StreamResult<Bytes>>,
70 progress_rx: watch::Receiver<StreamProgress>,
71}
72
73impl StreamReader for ByteStreamReader {
74 type Output = Bytes;
75 type Info = ByteStreamInfo;
76
77 fn info(&self) -> &ByteStreamInfo {
78 &self.info
79 }
80
81 fn progress(&self) -> impl Stream<Item = StreamProgress> {
82 WatchStream::new(self.progress_rx.clone())
83 }
84
85 async fn read_all(mut self) -> StreamResult<Bytes> {
86 let mut buffer = BytesMut::new();
87 while let Some(result) = self.next().await {
88 match result {
89 Ok(bytes) => buffer.extend_from_slice(&bytes),
90 Err(e) => return Err(e),
91 }
92 }
93 Ok(buffer.freeze())
94 }
95}
96
97impl ByteStreamReader {
98 pub async fn write_to_file(
111 mut self,
112 directory: Option<impl AsRef<std::path::Path>>,
113 name_override: Option<&str>,
114 ) -> StreamResult<std::path::PathBuf> {
115 let directory =
116 directory.map(|d| d.as_ref().to_path_buf()).unwrap_or_else(|| std::env::temp_dir());
117 let name = name_override.unwrap_or_else(|| &self.info.name);
118 let mut components = std::path::Path::new(name).components();
121 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
122 || components.next().is_some()
123 {
124 return Err(StreamError::InvalidFileName);
125 }
126 let file_path = directory.join(name);
127
128 let mut file = tokio::fs::File::create(&file_path).await.map_err(StreamError::Io)?;
129
130 while let Some(result) = self.next().await {
131 let bytes = result?;
132 tokio::io::AsyncWriteExt::write_all(&mut file, &bytes)
133 .await
134 .map_err(StreamError::Io)?;
135 }
136 tokio::io::AsyncWriteExt::flush(&mut file).await.map_err(StreamError::Io)?;
137
138 Ok(file_path)
139 }
140}
141
142impl Stream for ByteStreamReader {
143 type Item = StreamResult<Bytes>;
144
145 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
146 let this = self.get_mut();
147 match Pin::new(&mut this.chunk_rx).poll_recv(cx) {
148 Poll::Ready(Some(Ok(chunk))) => Poll::Ready(Some(Ok(chunk))),
149 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
150 Poll::Ready(None) => Poll::Ready(None),
151 Poll::Pending => Poll::Pending,
152 }
153 }
154}
155
156#[cfg(any(test, feature = "test-utils"))]
157impl TextStreamReader {
158 pub fn new_for_test(
163 info: TextStreamInfo,
164 chunk_rx: UnboundedReceiver<StreamResult<Bytes>>,
165 ) -> Self {
166 let (_, progress_rx) = watch::channel(StreamProgress::default());
169 Self { info, chunk_rx, progress_rx }
170 }
171}
172
173impl StreamReader for TextStreamReader {
174 type Output = String;
175 type Info = TextStreamInfo;
176
177 fn info(&self) -> &TextStreamInfo {
178 &self.info
179 }
180
181 fn progress(&self) -> impl Stream<Item = StreamProgress> {
182 WatchStream::new(self.progress_rx.clone())
183 }
184
185 async fn read_all(mut self) -> StreamResult<String> {
186 let mut result = String::new();
187 while let Some(chunk) = self.next().await {
188 match chunk {
189 Ok(text) => result.push_str(&text),
190 Err(e) => return Err(e),
191 }
192 }
193 Ok(result)
194 }
195}
196
197impl Stream for TextStreamReader {
198 type Item = StreamResult<String>;
199
200 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
201 let this = self.get_mut();
202 match Pin::new(&mut this.chunk_rx).poll_recv(cx) {
203 Poll::Ready(Some(Ok(chunk))) => match String::from_utf8(chunk.into()) {
204 Ok(content) => Poll::Ready(Some(Ok(content))),
205 Err(e) => {
206 this.chunk_rx.close();
207 Poll::Ready(Some(Err(StreamError::from(e))))
208 }
209 },
210 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
211 Poll::Ready(None) => Poll::Ready(None),
212 Poll::Pending => Poll::Pending,
213 }
214 }
215}
216
217impl Debug for ByteStreamReader {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 f.debug_struct("ByteStreamReader")
220 .field("id", &self.info.id())
221 .field("topic", &self.info.topic)
222 .finish()
223 }
224}
225
226impl Debug for TextStreamReader {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 f.debug_struct("TextStreamReader")
229 .field("id", &self.info.id())
230 .field("topic", &self.info.topic)
231 .finish()
232 }
233}
234
235pub enum AnyStreamReader {
236 Byte(ByteStreamReader),
237 Text(TextStreamReader),
238}
239
240impl AnyStreamReader {
241 pub(super) fn from(
248 info: AnyStreamInfo,
249 ) -> (Self, UnboundedSender<StreamResult<Bytes>>, watch::Sender<StreamProgress>) {
250 let (chunk_tx, chunk_rx) = mpsc::unbounded_channel();
251 let (progress_tx, progress_rx) = watch::channel(StreamProgress {
252 bytes_total: info.total_length(),
253 ..Default::default()
254 });
255 let reader = match info {
256 AnyStreamInfo::Byte(info) => {
257 Self::Byte(ByteStreamReader { info, chunk_rx, progress_rx })
258 }
259 AnyStreamInfo::Text(info) => {
260 Self::Text(TextStreamReader { info, chunk_rx, progress_rx })
261 }
262 };
263 return (reader, chunk_tx, progress_tx);
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use crate::types::{ByteHeader, CompressionType, Header, StreamId};
271 use std::collections::HashMap;
272
273 fn byte_reader(name: &str) -> (ByteStreamReader, UnboundedSender<StreamResult<Bytes>>) {
276 let header = Header {
277 stream_id: StreamId::from("stream-1"),
278 timestamp: 0,
279 topic: "topic".to_string(),
280 mime_type: "application/octet-stream".to_string(),
281 total_length: None,
282 attributes: HashMap::new(),
283 inline_content: None,
284 compression: CompressionType::None,
285 content_header: Some(ByteHeader { name: name.to_string() }.into()),
286 };
287 let AnyStreamInfo::Byte(info) = AnyStreamInfo::try_from(header).expect("valid header")
288 else {
289 panic!("expected a byte stream info");
290 };
291 let (chunk_tx, chunk_rx) = mpsc::unbounded_channel();
292 let (_, progress_rx) = watch::channel(StreamProgress::default());
293 (ByteStreamReader { info, chunk_rx, progress_rx }, chunk_tx)
294 }
295
296 #[tokio::test]
297 async fn write_to_file_rejects_traversal_in_stream_name() {
298 let (reader, _chunk_tx) = byte_reader("../evil.txt");
299 let result = reader.write_to_file(None::<&std::path::Path>, None).await;
300 assert!(matches!(result, Err(StreamError::InvalidFileName)));
301 }
302
303 #[tokio::test]
304 async fn write_to_file_rejects_traversal_in_name_override() {
305 let (reader, _chunk_tx) = byte_reader("safe.txt");
306 let result = reader.write_to_file(None::<&std::path::Path>, Some("../evil.txt")).await;
307 assert!(matches!(result, Err(StreamError::InvalidFileName)));
308 }
309
310 #[tokio::test]
311 async fn write_to_file_rejects_absolute_path_in_stream_name() {
312 let (reader, _chunk_tx) = byte_reader("/etc/evil.txt");
313 let result = reader.write_to_file(None::<&std::path::Path>, None).await;
314 assert!(matches!(result, Err(StreamError::InvalidFileName)));
315 }
316
317 #[tokio::test]
318 async fn write_to_file_rejects_nested_path_in_stream_name() {
319 let (reader, _chunk_tx) = byte_reader("nested/evil.txt");
320 let result = reader.write_to_file(None::<&std::path::Path>, None).await;
321 assert!(matches!(result, Err(StreamError::InvalidFileName)));
322 }
323
324 #[tokio::test]
325 async fn write_to_file_rejects_empty_stream_name() {
326 let (reader, _chunk_tx) = byte_reader("");
327 let result = reader.write_to_file(None::<&std::path::Path>, None).await;
328 assert!(matches!(result, Err(StreamError::InvalidFileName)));
329 }
330
331 #[tokio::test]
332 async fn write_to_file_accepts_plain_name() {
333 let directory =
334 std::env::temp_dir().join(format!("lk-stream-test-{}", uuid::Uuid::new_v4()));
335 tokio::fs::create_dir_all(&directory).await.expect("failed to create test directory");
336
337 let (reader, chunk_tx) = byte_reader("file.txt");
338 chunk_tx.send(Ok(Bytes::from_static(b"hello"))).expect("failed to send chunk");
339 drop(chunk_tx);
340
341 let path = reader
342 .write_to_file(Some(&directory), None)
343 .await
344 .expect("write_to_file should succeed for a plain file name");
345 assert_eq!(path, directory.join("file.txt"));
346 assert_eq!(tokio::fs::read(&path).await.expect("failed to read file"), b"hello");
347
348 tokio::fs::remove_dir_all(&directory).await.expect("failed to clean up test directory");
349 }
350}