Skip to content

Commit 45330e3

Browse files
fix(core): rewrite asset protocol streaming, closes #6375 (#6390)
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
1 parent 17da87d commit 45330e3

File tree

5 files changed

+358
-252
lines changed

5 files changed

+358
-252
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'tauri': 'patch:enhance'
3+
---
4+
5+
Enhance the `asset` protocol to support streaming of large files.

core/tauri/src/asset_protocol.rs

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2+
// SPDX-License-Identifier: Apache-2.0
3+
// SPDX-License-Identifier: MIT
4+
5+
#![cfg(protocol_asset)]
6+
7+
use crate::api::file::SafePathBuf;
8+
use crate::scope::FsScope;
9+
use rand::RngCore;
10+
use std::io::SeekFrom;
11+
use tauri_runtime::http::HttpRange;
12+
use tauri_runtime::http::{
13+
header::*, status::StatusCode, MimeType, Request, Response, ResponseBuilder,
14+
};
15+
use tauri_utils::debug_eprintln;
16+
use tokio::fs::File;
17+
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
18+
use url::Position;
19+
use url::Url;
20+
21+
pub fn asset_protocol_handler(
22+
request: &Request,
23+
scope: FsScope,
24+
window_origin: String,
25+
) -> Result<Response, Box<dyn std::error::Error>> {
26+
let parsed_path = Url::parse(request.uri())?;
27+
let filtered_path = &parsed_path[..Position::AfterPath];
28+
let path = filtered_path
29+
.strip_prefix("asset://localhost/")
30+
// the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows
31+
// where `$P` is not `localhost/*`
32+
.unwrap_or("");
33+
let path = percent_encoding::percent_decode(path.as_bytes())
34+
.decode_utf8_lossy()
35+
.to_string();
36+
37+
if let Err(e) = SafePathBuf::new(path.clone().into()) {
38+
debug_eprintln!("asset protocol path \"{}\" is not valid: {}", path, e);
39+
return ResponseBuilder::new().status(403).body(Vec::new());
40+
}
41+
42+
if !scope.is_allowed(&path) {
43+
debug_eprintln!("asset protocol not configured to allow the path: {}", path);
44+
return ResponseBuilder::new().status(403).body(Vec::new());
45+
}
46+
47+
let mut resp = ResponseBuilder::new().header("Access-Control-Allow-Origin", &window_origin);
48+
49+
crate::async_runtime::block_on(async move {
50+
let mut file = File::open(&path).await?;
51+
52+
// get file length
53+
let len = {
54+
let old_pos = file.stream_position().await?;
55+
let len = file.seek(SeekFrom::End(0)).await?;
56+
file.seek(SeekFrom::Start(old_pos)).await?;
57+
len
58+
};
59+
60+
// get file mime type
61+
let (mime_type, read_bytes) = {
62+
let nbytes = len.min(8192);
63+
let mut magic_buf = Vec::with_capacity(nbytes as usize);
64+
let old_pos = file.stream_position().await?;
65+
(&mut file).take(nbytes).read_to_end(&mut magic_buf).await?;
66+
file.seek(SeekFrom::Start(old_pos)).await?;
67+
(
68+
MimeType::parse(&magic_buf, &path),
69+
// return the `magic_bytes` if we read the whole file
70+
// to avoid reading it again later if this is not a range request
71+
if len < 8192 { Some(magic_buf) } else { None },
72+
)
73+
};
74+
75+
resp = resp.header(CONTENT_TYPE, &mime_type);
76+
77+
// handle 206 (partial range) http requests
78+
let response = if let Some(range_header) = request
79+
.headers()
80+
.get("range")
81+
.and_then(|r| r.to_str().map(|r| r.to_string()).ok())
82+
{
83+
resp = resp.header(ACCEPT_RANGES, "bytes");
84+
85+
let not_satisfiable = || {
86+
ResponseBuilder::new()
87+
.status(StatusCode::RANGE_NOT_SATISFIABLE)
88+
.header(CONTENT_RANGE, format!("bytes */{len}"))
89+
.body(vec![])
90+
};
91+
92+
// parse range header
93+
let ranges = if let Ok(ranges) = HttpRange::parse(&range_header, len) {
94+
ranges
95+
.iter()
96+
// map the output to spec range <start-end>, example: 0-499
97+
.map(|r| (r.start, r.start + r.length - 1))
98+
.collect::<Vec<_>>()
99+
} else {
100+
return not_satisfiable();
101+
};
102+
103+
/// The Maximum bytes we send in one range
104+
const MAX_LEN: u64 = 1000 * 1024;
105+
106+
// single-part range header
107+
if ranges.len() == 1 {
108+
let &(start, mut end) = ranges.first().unwrap();
109+
110+
// check if a range is not satisfiable
111+
//
112+
// this should be already taken care of by the range parsing library
113+
// but checking here again for extra assurance
114+
if start >= len || end >= len || end < start {
115+
return not_satisfiable();
116+
}
117+
118+
// adjust end byte for MAX_LEN
119+
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
120+
121+
// calculate number of bytes needed to be read
122+
let nbytes = end + 1 - start;
123+
124+
let mut buf = Vec::with_capacity(nbytes as usize);
125+
file.seek(SeekFrom::Start(start)).await?;
126+
file.take(nbytes).read_to_end(&mut buf).await?;
127+
128+
resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}"));
129+
resp = resp.header(CONTENT_LENGTH, end + 1 - start);
130+
resp = resp.status(StatusCode::PARTIAL_CONTENT);
131+
resp.body(buf)
132+
} else {
133+
// multi-part range header
134+
let mut buf = Vec::new();
135+
let ranges = ranges
136+
.iter()
137+
.filter_map(|&(start, mut end)| {
138+
// filter out unsatisfiable ranges
139+
//
140+
// this should be already taken care of by the range parsing library
141+
// but checking here again for extra assurance
142+
if start >= len || end >= len || end < start {
143+
None
144+
} else {
145+
// adjust end byte for MAX_LEN
146+
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
147+
Some((start, end))
148+
}
149+
})
150+
.collect::<Vec<_>>();
151+
152+
let boundary = random_boundary();
153+
let boundary_sep = format!("\r\n--{boundary}\r\n");
154+
let boundary_closer = format!("\r\n--{boundary}\r\n");
155+
156+
resp = resp.header(
157+
CONTENT_TYPE,
158+
format!("multipart/byteranges; boundary={boundary}"),
159+
);
160+
161+
for (end, start) in ranges {
162+
// a new range is being written, write the range boundary
163+
buf.write_all(boundary_sep.as_bytes()).await?;
164+
165+
// write the needed headers `Content-Type` and `Content-Range`
166+
buf
167+
.write_all(format!("{CONTENT_TYPE}: {mime_type}\r\n").as_bytes())
168+
.await?;
169+
buf
170+
.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())
171+
.await?;
172+
173+
// write the separator to indicate the start of the range body
174+
buf.write_all("\r\n".as_bytes()).await?;
175+
176+
// calculate number of bytes needed to be read
177+
let nbytes = end + 1 - start;
178+
179+
let mut local_buf = Vec::with_capacity(nbytes as usize);
180+
file.seek(SeekFrom::Start(start)).await?;
181+
(&mut file).take(nbytes).read_to_end(&mut local_buf).await?;
182+
buf.extend_from_slice(&local_buf);
183+
}
184+
// all ranges have been written, write the closing boundary
185+
buf.write_all(boundary_closer.as_bytes()).await?;
186+
187+
resp.body(buf)
188+
}
189+
} else {
190+
// avoid reading the file if we already read it
191+
// as part of mime type detection
192+
let buf = if let Some(b) = read_bytes {
193+
b
194+
} else {
195+
let mut local_buf = Vec::with_capacity(len as usize);
196+
file.read_to_end(&mut local_buf).await?;
197+
local_buf
198+
};
199+
resp = resp.header(CONTENT_LENGTH, len);
200+
resp.body(buf)
201+
};
202+
203+
response
204+
})
205+
}
206+
207+
fn random_boundary() -> String {
208+
let mut x = [0_u8; 30];
209+
rand::thread_rng().fill_bytes(&mut x);
210+
(x[..])
211+
.iter()
212+
.map(|&x| format!("{x:x}"))
213+
.fold(String::new(), |mut a, x| {
214+
a.push_str(x.as_str());
215+
a
216+
})
217+
}

core/tauri/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,14 @@ mod pattern;
184184
pub mod plugin;
185185
pub mod window;
186186
use tauri_runtime as runtime;
187+
#[cfg(protocol_asset)]
188+
mod asset_protocol;
187189
/// The allowlist scopes.
188190
pub mod scope;
189191
mod state;
190192
#[cfg(updater)]
191193
#[cfg_attr(doc_cfg, doc(cfg(feature = "updater")))]
192194
pub mod updater;
193-
194195
pub use tauri_utils as utils;
195196

196197
/// A Tauri [`Runtime`] wrapper around wry.

0 commit comments

Comments
 (0)