1use std::collections::BTreeMap;
11use std::fmt;
12use std::fs::File;
13use std::io::{Read, Seek, SeekFrom};
14use std::panic::{catch_unwind, AssertUnwindSafe};
15use std::path::Path;
16use std::sync::Arc;
17
18use tauri::http::response::Builder;
19use tauri::http::{header, Method, Request, Response, StatusCode};
20
21use crate::broker::PathBroker;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum StreamEventKind {
26 Resolved { size: u64 },
29 Head {
32 size: u64,
33 range: Option<(u64, u64)>,
34 },
35 RangeServed { start: u64, end: u64, size: u64 },
37 FullServed { size: u64 },
39 RangeRejected { size: u64 },
41 BytesRead { bytes: u64, ranged: bool },
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct StreamEvent {
49 pub mime: &'static str,
52 pub kind: StreamEventKind,
53}
54
55#[derive(Clone)]
59pub struct StreamObserver(Arc<dyn Fn(&StreamEvent) + Send + Sync>);
60
61impl StreamObserver {
62 pub fn new(f: impl Fn(&StreamEvent) + Send + Sync + 'static) -> Self {
63 Self(Arc::new(f))
64 }
65
66 fn emit(&self, mime: &'static str, kind: StreamEventKind) {
67 let event = StreamEvent { mime, kind };
68 let _ = catch_unwind(AssertUnwindSafe(|| (self.0)(&event)));
69 }
70}
71
72impl fmt::Debug for StreamObserver {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 f.write_str("StreamObserver(..)")
75 }
76}
77
78fn emit(observer: &Option<StreamObserver>, mime: &'static str, kind: StreamEventKind) {
79 if let Some(o) = observer {
80 o.emit(mime, kind);
81 }
82}
83
84#[derive(Debug, Clone)]
86pub struct StreamPolicy {
87 pub max_bytes: u64,
89 pub extensions: Option<Vec<String>>,
91 pub max_bytes_by_kind: BTreeMap<String, u64>,
96 pub observer: Option<StreamObserver>,
99}
100
101impl Default for StreamPolicy {
102 fn default() -> Self {
103 Self {
104 max_bytes: 150 * 1024 * 1024,
105 extensions: None,
106 max_bytes_by_kind: BTreeMap::new(),
107 observer: None,
108 }
109 }
110}
111
112impl StreamPolicy {
113 pub fn with_kind_cap(mut self, kind: &str, max_bytes: u64) -> Self {
116 self.max_bytes_by_kind
117 .insert(normalize_kind(kind), max_bytes);
118 self
119 }
120
121 pub fn observer(mut self, f: impl Fn(&StreamEvent) + Send + Sync + 'static) -> Self {
124 self.observer = Some(StreamObserver::new(f));
125 self
126 }
127
128 pub fn max_bytes_for(&self, path: &Path) -> u64 {
130 if self.max_bytes_by_kind.is_empty() {
131 return self.max_bytes;
132 }
133 let ext = path
134 .extension()
135 .and_then(|e| e.to_str())
136 .map(|e| e.to_ascii_lowercase());
137 if let Some(cap) = ext.as_deref().and_then(|e| self.max_bytes_by_kind.get(e)) {
138 return *cap;
139 }
140 let Some(mime) = mime_for(path) else {
141 return self.max_bytes;
142 };
143 let mime = mime.split(';').next().unwrap_or(mime).trim();
144 if let Some(cap) = self.max_bytes_by_kind.get(mime) {
145 return *cap;
146 }
147 let family = mime.split('/').next().unwrap_or(mime);
148 self.max_bytes_by_kind
149 .get(family)
150 .copied()
151 .unwrap_or(self.max_bytes)
152 }
153}
154
155fn normalize_kind(kind: &str) -> String {
156 kind.trim()
157 .trim_start_matches('.')
158 .trim_end_matches("/*")
159 .trim_end_matches('/')
160 .to_ascii_lowercase()
161}
162
163pub fn mime_for(path: &Path) -> Option<&'static str> {
165 let ext = path.extension()?.to_str()?.to_ascii_lowercase();
166 Some(match ext.as_str() {
167 "pdf" => "application/pdf",
168 "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
169 "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
170 "png" => "image/png",
171 "jpg" | "jpeg" => "image/jpeg",
172 "gif" => "image/gif",
173 "webp" => "image/webp",
174 "avif" => "image/avif",
175 "svg" => "image/svg+xml",
176 "bmp" => "image/bmp",
177 "ico" => "image/x-icon",
178 "tif" | "tiff" => "image/tiff",
179 "mp3" => "audio/mpeg",
180 "wav" => "audio/wav",
181 "m4a" => "audio/mp4",
182 "flac" => "audio/flac",
183 "ogg" | "oga" => "audio/ogg",
184 "mp4" | "m4v" => "video/mp4",
185 "mov" => "video/quicktime",
186 "webm" => "video/webm",
187 "txt" | "log" => "text/plain; charset=utf-8",
188 "md" => "text/markdown; charset=utf-8",
189 "json" => "application/json",
190 "csv" => "text/csv; charset=utf-8",
191 "bin" | "dat" => "application/octet-stream",
192 _ => return None,
193 })
194}
195
196pub(crate) fn internal_error() -> Response<Vec<u8>> {
199 let mut r = Response::new(Vec::new());
200 *r.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
201 r
202}
203
204pub(crate) fn finish(b: Builder, body: Vec<u8>) -> Response<Vec<u8>> {
206 b.body(body).unwrap_or_else(|_| internal_error())
207}
208
209pub(crate) fn cors(status: StatusCode) -> Builder {
211 Response::builder()
212 .status(status)
213 .header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*")
214 .header(header::ACCESS_CONTROL_ALLOW_METHODS, "GET, HEAD, OPTIONS")
215 .header(header::ACCESS_CONTROL_ALLOW_HEADERS, "Range")
216 .header(
217 header::ACCESS_CONTROL_EXPOSE_HEADERS,
218 "Accept-Ranges, Content-Length, Content-Range",
219 )
220}
221
222pub(crate) fn ranged(status: StatusCode, mime: &str, len: u64) -> Builder {
224 Response::builder()
225 .status(status)
226 .header(header::CONTENT_TYPE, mime)
227 .header(header::ACCEPT_RANGES, "bytes")
228 .header(header::CONTENT_LENGTH, len.to_string())
229 .header(header::CACHE_CONTROL, "no-store")
230 .header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*")
231 .header(
232 header::ACCESS_CONTROL_EXPOSE_HEADERS,
233 "Accept-Ranges, Content-Length, Content-Range",
234 )
235}
236
237pub(crate) fn empty(status: StatusCode) -> Response<Vec<u8>> {
239 finish(cors(status), Vec::new())
240}
241
242pub(crate) fn method_gate_status(method: &Method) -> Option<StatusCode> {
245 if method == Method::OPTIONS {
246 return Some(StatusCode::NO_CONTENT);
247 }
248 if method != Method::GET && method != Method::HEAD {
249 return Some(StatusCode::METHOD_NOT_ALLOWED);
250 }
251 None
252}
253
254pub fn parse_range(value: &str, len: u64) -> Option<(u64, u64)> {
257 let spec = value.strip_prefix("bytes=")?;
258 if spec.contains(',') || len == 0 {
259 return None;
260 }
261 let (a, b) = spec.split_once('-')?;
262 if a.is_empty() {
263 let suffix = b.parse::<u64>().ok()?;
264 if suffix == 0 {
265 return None;
266 }
267 return Some((len.saturating_sub(suffix), len - 1));
268 }
269 let start = a.parse::<u64>().ok()?;
270 if start >= len {
271 return None;
272 }
273 let end = if b.is_empty() {
274 len - 1
275 } else {
276 b.parse::<u64>().ok()?.min(len - 1)
277 };
278 (start <= end).then_some((start, end))
279}
280
281pub(crate) fn read_range(path: &Path, start: u64, end: u64) -> std::io::Result<Vec<u8>> {
283 let mut f = File::open(path)?;
284 f.seek(SeekFrom::Start(start))?;
285 let mut buf = vec![0; (end - start + 1) as usize];
286 f.read_exact(&mut buf)?;
287 Ok(buf)
288}
289
290pub fn serve<F>(broker: &PathBroker, policy: &StreamPolicy, request: &Request<Vec<u8>>, respond: F)
293where
294 F: FnOnce(Response<Vec<u8>>) + Send + 'static,
295{
296 if let Some(status) = method_gate_status(request.method()) {
297 return respond(empty(status));
298 }
299 let id = request.uri().path().trim_start_matches('/').trim();
300 if id.is_empty() {
301 return respond(empty(StatusCode::BAD_REQUEST));
302 }
303 let Ok(target) = broker.resolve_stream(id) else {
304 return respond(empty(StatusCode::FORBIDDEN));
305 };
306 let path = target.path;
307 let allowed_ext = policy.extensions.as_ref().is_none_or(|exts| {
308 path.extension()
309 .and_then(|e| e.to_str())
310 .is_some_and(|e| exts.iter().any(|x| x.eq_ignore_ascii_case(e)))
311 });
312 let mime = match mime_for(&path) {
313 Some(m) if allowed_ext => m,
314 _ => return respond(empty(StatusCode::UNSUPPORTED_MEDIA_TYPE)),
315 };
316 let Ok(meta) = std::fs::metadata(&path) else {
317 return respond(empty(StatusCode::NOT_FOUND));
318 };
319 if !meta.is_file() || meta.len() > policy.max_bytes_for(&path) {
320 return respond(empty(StatusCode::PAYLOAD_TOO_LARGE));
321 }
322 let total = meta.len();
323 let head = method_is_head(request);
324 let observer = &policy.observer;
325 emit(observer, mime, StreamEventKind::Resolved { size: total });
326
327 if let Some(range) = request.headers().get(header::RANGE) {
328 let parsed = range.to_str().ok().and_then(|r| parse_range(r, total));
329 let Some((start, end)) = parsed else {
330 emit(
331 observer,
332 mime,
333 StreamEventKind::RangeRejected { size: total },
334 );
335 return respond(finish(
336 cors(StatusCode::RANGE_NOT_SATISFIABLE)
337 .header(header::CONTENT_RANGE, format!("bytes */{total}")),
338 Vec::new(),
339 ));
340 };
341 let builder = ranged(StatusCode::PARTIAL_CONTENT, mime, end - start + 1).header(
342 header::CONTENT_RANGE,
343 format!("bytes {start}-{end}/{total}"),
344 );
345 if head {
346 emit(
347 observer,
348 mime,
349 StreamEventKind::Head {
350 size: total,
351 range: Some((start, end)),
352 },
353 );
354 return respond(finish(builder, Vec::new()));
355 }
356 emit(
357 observer,
358 mime,
359 StreamEventKind::RangeServed {
360 start,
361 end,
362 size: total,
363 },
364 );
365 let observer = observer.clone();
366 std::thread::spawn(move || {
367 respond(match read_range(&path, start, end) {
368 Ok(bytes) => {
369 emit(
370 &observer,
371 mime,
372 StreamEventKind::BytesRead {
373 bytes: bytes.len() as u64,
374 ranged: true,
375 },
376 );
377 finish(builder, bytes)
378 }
379 Err(_) => empty(StatusCode::NOT_FOUND),
380 })
381 });
382 return;
383 }
384 if head {
385 emit(
386 observer,
387 mime,
388 StreamEventKind::Head {
389 size: total,
390 range: None,
391 },
392 );
393 return respond(finish(ranged(StatusCode::OK, mime, total), Vec::new()));
394 }
395 emit(observer, mime, StreamEventKind::FullServed { size: total });
396 let observer = observer.clone();
397 std::thread::spawn(move || {
398 respond(match std::fs::read(&path) {
399 Ok(bytes) => {
400 emit(
401 &observer,
402 mime,
403 StreamEventKind::BytesRead {
404 bytes: bytes.len() as u64,
405 ranged: false,
406 },
407 );
408 finish(ranged(StatusCode::OK, mime, bytes.len() as u64), bytes)
409 }
410 Err(_) => empty(StatusCode::NOT_FOUND),
411 })
412 });
413}
414
415fn method_is_head<B>(request: &Request<B>) -> bool {
416 request.method() == Method::HEAD
417}