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;
31use tokio_util::sync::CancellationToken;
32
33pub trait StreamReader: Stream<Item = StreamResult<Self::Output>> {
39 type Output;
41
42 type Info;
44
45 fn info(&self) -> &Self::Info;
47
48 fn progress(&self) -> impl Stream<Item = StreamProgress>;
51
52 fn read_all(self) -> impl std::future::Future<Output = StreamResult<Self::Output>> + Send;
58}
59
60pub struct ByteStreamReader {
62 info: ByteStreamInfo,
63 chunk_rx: UnboundedReceiver<StreamResult<Bytes>>,
64 progress_rx: watch::Receiver<StreamProgress>,
65 closer: StreamCloser,
66}
67
68pub struct TextStreamReader {
70 info: TextStreamInfo,
71 chunk_rx: UnboundedReceiver<StreamResult<Bytes>>,
72 progress_rx: watch::Receiver<StreamProgress>,
73 closer: StreamCloser,
74}
75
76#[derive(Clone, Debug)]
80pub struct StreamCloser {
81 pub(crate) token: CancellationToken,
83}
84
85impl StreamCloser {
86 #[cfg(any(test, feature = "test-utils"))]
88 fn detached() -> Self {
89 Self { token: CancellationToken::new() }
90 }
91
92 pub fn close(&self) {
101 self.token.cancel();
102 }
103}
104
105impl StreamReader for ByteStreamReader {
106 type Output = Bytes;
107 type Info = ByteStreamInfo;
108
109 fn info(&self) -> &ByteStreamInfo {
110 &self.info
111 }
112
113 fn progress(&self) -> impl Stream<Item = StreamProgress> {
114 self.progress_stream()
115 }
116
117 async fn read_all(mut self) -> StreamResult<Bytes> {
118 let mut buffer = BytesMut::new();
119 while let Some(result) = self.next().await {
120 match result {
121 Ok(bytes) => buffer.extend_from_slice(&bytes),
122 Err(e) => return Err(e),
123 }
124 }
125 Ok(buffer.freeze())
126 }
127}
128
129impl ByteStreamReader {
130 pub fn progress_stream(&self) -> impl Stream<Item = StreamProgress> + Send + Unpin + 'static {
133 WatchStream::new(self.progress_rx.clone())
134 }
135
136 pub fn closer(&self) -> StreamCloser {
138 self.closer.clone()
139 }
140
141 pub async fn write_to_file(
154 mut self,
155 directory: Option<impl AsRef<std::path::Path>>,
156 name_override: Option<&str>,
157 ) -> StreamResult<std::path::PathBuf> {
158 let directory =
159 directory.map(|d| d.as_ref().to_path_buf()).unwrap_or_else(std::env::temp_dir);
160 let name = name_override.unwrap_or_else(|| &self.info.name);
161 let mut components = std::path::Path::new(name).components();
164 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
165 || components.next().is_some()
166 {
167 return Err(StreamError::InvalidFileName);
168 }
169 let file_path = directory.join(name);
170
171 let mut file = tokio::fs::File::create(&file_path).await.map_err(StreamError::Io)?;
172
173 while let Some(result) = self.next().await {
174 let bytes = result?;
175 tokio::io::AsyncWriteExt::write_all(&mut file, &bytes)
176 .await
177 .map_err(StreamError::Io)?;
178 }
179 tokio::io::AsyncWriteExt::flush(&mut file).await.map_err(StreamError::Io)?;
180
181 Ok(file_path)
182 }
183}
184
185impl Stream for ByteStreamReader {
186 type Item = StreamResult<Bytes>;
187
188 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
189 let this = self.get_mut();
190 match Pin::new(&mut this.chunk_rx).poll_recv(cx) {
191 Poll::Ready(Some(Ok(chunk))) => Poll::Ready(Some(Ok(chunk))),
192 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
193 Poll::Ready(None) => Poll::Ready(None),
194 Poll::Pending => Poll::Pending,
195 }
196 }
197}
198
199impl TextStreamReader {
200 pub fn progress_stream(&self) -> impl Stream<Item = StreamProgress> + Send + Unpin + 'static {
203 WatchStream::new(self.progress_rx.clone())
204 }
205
206 pub fn closer(&self) -> StreamCloser {
208 self.closer.clone()
209 }
210}
211
212#[cfg(any(test, feature = "test-utils"))]
213impl TextStreamReader {
214 pub fn new_for_test(
219 info: TextStreamInfo,
220 chunk_rx: UnboundedReceiver<StreamResult<Bytes>>,
221 ) -> Self {
222 let (_, progress_rx) = watch::channel(StreamProgress::default());
225 Self { info, chunk_rx, progress_rx, closer: StreamCloser::detached() }
226 }
227}
228
229impl StreamReader for TextStreamReader {
230 type Output = String;
231 type Info = TextStreamInfo;
232
233 fn info(&self) -> &TextStreamInfo {
234 &self.info
235 }
236
237 fn progress(&self) -> impl Stream<Item = StreamProgress> {
238 self.progress_stream()
239 }
240
241 async fn read_all(mut self) -> StreamResult<String> {
242 let mut result = String::new();
243 while let Some(chunk) = self.next().await {
244 match chunk {
245 Ok(text) => result.push_str(&text),
246 Err(e) => return Err(e),
247 }
248 }
249 Ok(result)
250 }
251}
252
253impl Stream for TextStreamReader {
254 type Item = StreamResult<String>;
255
256 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
257 let this = self.get_mut();
258 match Pin::new(&mut this.chunk_rx).poll_recv(cx) {
259 Poll::Ready(Some(Ok(chunk))) => match String::from_utf8(chunk.into()) {
260 Ok(content) => Poll::Ready(Some(Ok(content))),
261 Err(e) => {
262 this.chunk_rx.close();
263 Poll::Ready(Some(Err(StreamError::from(e))))
264 }
265 },
266 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
267 Poll::Ready(None) => Poll::Ready(None),
268 Poll::Pending => Poll::Pending,
269 }
270 }
271}
272
273impl Debug for ByteStreamReader {
274 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 f.debug_struct("ByteStreamReader")
276 .field("id", &self.info.id())
277 .field("topic", &self.info.topic)
278 .finish()
279 }
280}
281
282impl Debug for TextStreamReader {
283 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284 f.debug_struct("TextStreamReader")
285 .field("id", &self.info.id())
286 .field("topic", &self.info.topic)
287 .finish()
288 }
289}
290
291pub enum AnyStreamReader {
292 Byte(ByteStreamReader),
293 Text(TextStreamReader),
294}
295
296impl AnyStreamReader {
297 pub(super) fn from(
304 info: AnyStreamInfo,
305 closer: StreamCloser,
306 ) -> (Self, UnboundedSender<StreamResult<Bytes>>, watch::Sender<StreamProgress>) {
307 let (chunk_tx, chunk_rx) = mpsc::unbounded_channel();
308 let (progress_tx, progress_rx) = watch::channel(StreamProgress {
309 bytes_total: info.total_length(),
310 ..Default::default()
311 });
312 let reader = match info {
313 AnyStreamInfo::Byte(info) => {
314 Self::Byte(ByteStreamReader { info, chunk_rx, progress_rx, closer })
315 }
316 AnyStreamInfo::Text(info) => {
317 Self::Text(TextStreamReader { info, chunk_rx, progress_rx, closer })
318 }
319 };
320 (reader, chunk_tx, progress_tx)
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use crate::types::{ByteHeader, CompressionType, Header, StreamId};
328 use std::collections::HashMap;
329
330 fn byte_reader(name: &str) -> (ByteStreamReader, UnboundedSender<StreamResult<Bytes>>) {
333 let header = Header {
334 stream_id: StreamId::from("stream-1"),
335 timestamp: 0,
336 topic: "topic".to_string(),
337 mime_type: "application/octet-stream".to_string(),
338 total_length: None,
339 attributes: HashMap::new(),
340 inline_content: None,
341 compression: CompressionType::None,
342 content_header: Some(ByteHeader { name: name.to_string() }.into()),
343 };
344 let AnyStreamInfo::Byte(info) = AnyStreamInfo::try_from(header).expect("valid header")
345 else {
346 panic!("expected a byte stream info");
347 };
348 let (chunk_tx, chunk_rx) = mpsc::unbounded_channel();
349 let (_, progress_rx) = watch::channel(StreamProgress::default());
350 (
351 ByteStreamReader { info, chunk_rx, progress_rx, closer: StreamCloser::detached() },
352 chunk_tx,
353 )
354 }
355
356 #[tokio::test]
357 async fn write_to_file_rejects_traversal_in_stream_name() {
358 let (reader, _chunk_tx) = byte_reader("../evil.txt");
359 let result = reader.write_to_file(None::<&std::path::Path>, None).await;
360 assert!(matches!(result, Err(StreamError::InvalidFileName)));
361 }
362
363 #[tokio::test]
364 async fn write_to_file_rejects_traversal_in_name_override() {
365 let (reader, _chunk_tx) = byte_reader("safe.txt");
366 let result = reader.write_to_file(None::<&std::path::Path>, Some("../evil.txt")).await;
367 assert!(matches!(result, Err(StreamError::InvalidFileName)));
368 }
369
370 #[tokio::test]
371 async fn write_to_file_rejects_absolute_path_in_stream_name() {
372 let (reader, _chunk_tx) = byte_reader("/etc/evil.txt");
373 let result = reader.write_to_file(None::<&std::path::Path>, None).await;
374 assert!(matches!(result, Err(StreamError::InvalidFileName)));
375 }
376
377 #[tokio::test]
378 async fn write_to_file_rejects_nested_path_in_stream_name() {
379 let (reader, _chunk_tx) = byte_reader("nested/evil.txt");
380 let result = reader.write_to_file(None::<&std::path::Path>, None).await;
381 assert!(matches!(result, Err(StreamError::InvalidFileName)));
382 }
383
384 #[tokio::test]
385 async fn write_to_file_rejects_empty_stream_name() {
386 let (reader, _chunk_tx) = byte_reader("");
387 let result = reader.write_to_file(None::<&std::path::Path>, None).await;
388 assert!(matches!(result, Err(StreamError::InvalidFileName)));
389 }
390
391 #[tokio::test]
392 async fn write_to_file_accepts_plain_name() {
393 let directory =
394 std::env::temp_dir().join(format!("lk-stream-test-{}", uuid::Uuid::new_v4()));
395 tokio::fs::create_dir_all(&directory).await.expect("failed to create test directory");
396
397 let (reader, chunk_tx) = byte_reader("file.txt");
398 chunk_tx.send(Ok(Bytes::from_static(b"hello"))).expect("failed to send chunk");
399 drop(chunk_tx);
400
401 let path = reader
402 .write_to_file(Some(&directory), None)
403 .await
404 .expect("write_to_file should succeed for a plain file name");
405 assert_eq!(path, directory.join("file.txt"));
406 assert_eq!(tokio::fs::read(&path).await.expect("failed to read file"), b"hello");
407
408 tokio::fs::remove_dir_all(&directory).await.expect("failed to clean up test directory");
409 }
410}