diff --git a/.changes/asset-protocol-streaming-mime-type.md b/.changes/asset-protocol-streaming-mime-type.md new file mode 100644 index 00000000000..43c99d2c9be --- /dev/null +++ b/.changes/asset-protocol-streaming-mime-type.md @@ -0,0 +1,5 @@ +--- +"tauri": "patch" +--- + +Set the correct mimetype when streaming files through `asset:` protocol diff --git a/core/tauri-codegen/src/context.rs b/core/tauri-codegen/src/context.rs index b2a61ae96c6..ef68689a936 100644 --- a/core/tauri-codegen/src/context.rs +++ b/core/tauri-codegen/src/context.rs @@ -57,7 +57,7 @@ fn map_core_assets( let mut hasher = Sha256::new(); hasher.update(&script); let hash = hasher.finalize(); - scripts.push(format!("'sha256-{}'", base64::encode(&hash))); + scripts.push(format!("'sha256-{}'", base64::encode(hash))); } csp_hashes .inline_scripts @@ -76,7 +76,7 @@ fn map_core_assets( let hash = hasher.finalize(); csp_hashes .styles - .push(format!("'sha256-{}'", base64::encode(&hash))); + .push(format!("'sha256-{}'", base64::encode(hash))); } } @@ -457,7 +457,7 @@ fn ico_icon>( path: P, ) -> Result { let path = path.as_ref(); - let bytes = std::fs::read(&path) + let bytes = std::fs::read(path) .unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e)) .to_vec(); let icon_dir = ico::IconDir::read(std::io::Cursor::new(bytes)) @@ -485,7 +485,7 @@ fn ico_icon>( fn raw_icon>(out_dir: &Path, path: P) -> Result { let path = path.as_ref(); - let bytes = std::fs::read(&path) + let bytes = std::fs::read(path) .unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e)) .to_vec(); @@ -507,7 +507,7 @@ fn png_icon>( path: P, ) -> Result { let path = path.as_ref(); - let bytes = std::fs::read(&path) + let bytes = std::fs::read(path) .unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e)) .to_vec(); let decoder = png::Decoder::new(std::io::Cursor::new(bytes)); @@ -537,13 +537,13 @@ fn write_if_changed(out_path: &Path, data: &[u8]) -> std::io::Result<()> { use std::fs::File; use std::io::Write; - if let Ok(curr) = std::fs::read(&out_path) { + if let Ok(curr) = std::fs::read(out_path) { if curr == data { return Ok(()); } } - let mut out_file = File::create(&out_path)?; + let mut out_file = File::create(out_path)?; out_file.write_all(data) } diff --git a/core/tauri-utils/src/config.rs b/core/tauri-utils/src/config.rs index aab3b813ec3..b877769cc95 100644 --- a/core/tauri-utils/src/config.rs +++ b/core/tauri-utils/src/config.rs @@ -2981,7 +2981,7 @@ mod build { tokens.append_all(match self { Self::App(path) => { - let path = path_buf_lit(&path); + let path = path_buf_lit(path); quote! { #prefix::App(#path) } } Self::External(url) => { @@ -3211,7 +3211,7 @@ mod build { quote! { #prefix::OfflineInstaller { silent: #silent } } } Self::FixedRuntime { path } => { - let path = path_buf_lit(&path); + let path = path_buf_lit(path); quote! { #prefix::FixedRuntime { path: #path } } } }) diff --git a/core/tauri/src/api/file/extract.rs b/core/tauri/src/api/file/extract.rs index d92f7019747..28e2ad9a472 100644 --- a/core/tauri/src/api/file/extract.rs +++ b/core/tauri/src/api/file/extract.rs @@ -15,7 +15,7 @@ pub enum ArchiveReader { /// A plain reader. Plain(R), /// A GZ- compressed reader (decoder). - GzCompressed(flate2::read::GzDecoder), + GzCompressed(Box>), } impl Read for ArchiveReader { @@ -161,7 +161,9 @@ impl<'a, R: Read + Seek> Extract<'a, R> { }; Extract { reader: match compression { - Some(Compression::Gz) => ArchiveReader::GzCompressed(flate2::read::GzDecoder::new(reader)), + Some(Compression::Gz) => { + ArchiveReader::GzCompressed(Box::new(flate2::read::GzDecoder::new(reader))) + } _ => ArchiveReader::Plain(reader), }, archive_format, @@ -248,7 +250,7 @@ impl<'a, R: Read + Seek> Extract<'a, R> { fs::create_dir_all(&out_path)?; } else { if let Some(out_path_parent) = out_path.parent() { - fs::create_dir_all(&out_path_parent)?; + fs::create_dir_all(out_path_parent)?; } let mut out_file = fs::File::create(&out_path)?; io::copy(&mut file, &mut out_file)?; diff --git a/core/tauri/src/api/file/file_move.rs b/core/tauri/src/api/file/file_move.rs index fea87ef7db6..5e782c67a97 100644 --- a/core/tauri/src/api/file/file_move.rs +++ b/core/tauri/src/api/file/file_move.rs @@ -98,7 +98,7 @@ fn walkdir_and_copy(source: &path::Path, dest: &path::Path) -> crate::api::Resul let element = entry?; let metadata = element.metadata()?; - let destination = dest.join(element.path().strip_prefix(&source)?); + let destination = dest.join(element.path().strip_prefix(source)?); // we make sure it's a directory and destination doesnt exist if metadata.is_dir() && !&destination.exists() { diff --git a/core/tauri/src/api/process/command.rs b/core/tauri/src/api/process/command.rs index 5b0cb9fa3cf..8f7d2ca1728 100644 --- a/core/tauri/src/api/process/command.rs +++ b/core/tauri/src/api/process/command.rs @@ -436,7 +436,7 @@ mod test { #[test] fn test_cmd_output() { // create a command to run cat. - let cmd = Command::new("cat").args(&["test/api/test.txt"]); + let cmd = Command::new("cat").args(["test/api/test.txt"]); let (mut rx, _) = cmd.spawn().unwrap(); crate::async_runtime::block_on(async move { @@ -458,7 +458,7 @@ mod test { #[test] // test the failure case fn test_cmd_fail() { - let cmd = Command::new("cat").args(&["test/api/"]); + let cmd = Command::new("cat").args(["test/api/"]); let (mut rx, _) = cmd.spawn().unwrap(); crate::async_runtime::block_on(async move { diff --git a/core/tauri/src/endpoints/http.rs b/core/tauri/src/endpoints/http.rs index 73ec11ade15..0f1acbdd618 100644 --- a/core/tauri/src/endpoints/http.rs +++ b/core/tauri/src/endpoints/http.rs @@ -103,7 +103,7 @@ impl Cmd { } = value { if crate::api::file::SafePathBuf::new(path.clone()).is_err() - || !scopes.fs.is_allowed(&path) + || !scopes.fs.is_allowed(path) { return Err(crate::Error::PathNotAllowed(path.clone()).into_anyhow()); } diff --git a/core/tauri/src/manager.rs b/core/tauri/src/manager.rs index 4ee026b5e49..f6c25830a32 100644 --- a/core/tauri/src/manager.rs +++ b/core/tauri/src/manager.rs @@ -541,23 +541,39 @@ impl WindowManager { .get("range") .and_then(|r| r.to_str().map(|r| r.to_string()).ok()) { - let (headers, status_code, data) = crate::async_runtime::safe_block_on(async move { - let mut headers = HashMap::new(); - let mut buf = Vec::new(); + #[derive(Default)] + struct RangeMetadata { + file: Option, + range: Option, + metadata: Option, + headers: HashMap<&'static str, String>, + status_code: u16, + body: Vec, + } + + let mut range_metadata = crate::async_runtime::safe_block_on(async move { + let mut data = RangeMetadata::default(); // open the file let mut file = match tokio::fs::File::open(path_.clone()).await { Ok(file) => file, Err(e) => { debug_eprintln!("Failed to open asset: {}", e); - return (headers, 404, buf); + data.status_code = 404; + return data; } }; // Get the file size let file_size = match file.metadata().await { - Ok(metadata) => metadata.len(), + Ok(metadata) => { + let len = metadata.len(); + data.metadata.replace(metadata); + len + } Err(e) => { debug_eprintln!("Failed to read asset metadata: {}", e); - return (headers, 404, buf); + data.file.replace(file); + data.status_code = 404; + return data; } }; // parse the range @@ -572,13 +588,16 @@ impl WindowManager { Ok(r) => r, Err(e) => { debug_eprintln!("Failed to parse range {}: {:?}", range, e); - return (headers, 400, buf); + data.file.replace(file); + data.status_code = 400; + return data; } }; // FIXME: Support multiple ranges // let support only 1 range for now - let status_code = if let Some(range) = range.first() { + if let Some(range) = range.first() { + data.range.replace(*range); let mut real_length = range.length; // prevent max_length; // specially on webview2 @@ -592,38 +611,84 @@ impl WindowManager { // who should be skipped on the header let last_byte = range.start + real_length - 1; - headers.insert("Connection", "Keep-Alive".into()); - headers.insert("Accept-Ranges", "bytes".into()); - headers.insert("Content-Length", real_length.to_string()); - headers.insert( + data.headers.insert("Connection", "Keep-Alive".into()); + data.headers.insert("Accept-Ranges", "bytes".into()); + data + .headers + .insert("Content-Length", real_length.to_string()); + data.headers.insert( "Content-Range", format!("bytes {}-{}/{}", range.start, last_byte, file_size), ); if let Err(e) = file.seek(std::io::SeekFrom::Start(range.start)).await { debug_eprintln!("Failed to seek file to {}: {}", range.start, e); - return (headers, 422, buf); + data.file.replace(file); + data.status_code = 422; + return data; } - if let Err(e) = file.take(real_length).read_to_end(&mut buf).await { + let mut f = file.take(real_length); + let r = f.read_to_end(&mut data.body).await; + file = f.into_inner(); + data.file.replace(file); + + if let Err(e) = r { debug_eprintln!("Failed read file: {}", e); - return (headers, 422, buf); + data.status_code = 422; + return data; } // partial content - 206 + data.status_code = 206; } else { - 200 - }; + data.status_code = 200; + } - (headers, status_code, buf) + data }); - for (k, v) in headers { + for (k, v) in range_metadata.headers { response = response.header(k, v); } - let mime_type = MimeType::parse(&data, &path); - response.mimetype(&mime_type).status(status_code).body(data) + let mime_type = if let (Some(mut file), Some(metadata), Some(range)) = ( + range_metadata.file, + range_metadata.metadata, + range_metadata.range, + ) { + // if we're already reading the beginning of the file, we do not need to re-read it + if range.start == 0 { + MimeType::parse(&range_metadata.body, &path) + } else { + let (status, bytes) = crate::async_runtime::safe_block_on(async move { + let mut status = None; + if let Err(e) = file.rewind().await { + debug_eprintln!("Failed to rewind file: {}", e); + status.replace(422); + (status, Vec::with_capacity(0)) + } else { + // taken from https://docs.rs/infer/0.9.0/src/infer/lib.rs.html#240-251 + let limit = std::cmp::min(metadata.len(), 8192) as usize + 1; + let mut bytes = Vec::with_capacity(limit); + if let Err(e) = file.take(8192).read_to_end(&mut bytes).await { + debug_eprintln!("Failed read file: {}", e); + status.replace(422); + } + (status, bytes) + } + }); + if let Some(s) = status { + range_metadata.status_code = s; + } + MimeType::parse(&bytes, &path) + } + } else { + MimeType::parse(&range_metadata.body, &path) + }; + response + .mimetype(&mime_type) + .status(range_metadata.status_code) + .body(range_metadata.body) } else { match crate::async_runtime::safe_block_on(async move { tokio::fs::read(path_).await }) { Ok(data) => { @@ -1066,7 +1131,7 @@ impl WindowManager { // ignore "index.html" just to simplify the url if path.to_str() != Some("index.html") { url - .join(&*path.to_string_lossy()) + .join(&path.to_string_lossy()) .map_err(crate::Error::InvalidUrl) // this will never fail .unwrap() diff --git a/core/tauri/src/scope/fs.rs b/core/tauri/src/scope/fs.rs index 66d8188c748..32d92652a94 100644 --- a/core/tauri/src/scope/fs.rs +++ b/core/tauri/src/scope/fs.rs @@ -148,9 +148,9 @@ impl Scope { let mut list = self.allowed_patterns.lock().unwrap(); // allow the directory to be read - push_pattern(&mut list, &path, escaped_pattern)?; + push_pattern(&mut list, path, escaped_pattern)?; // allow its files and subdirectories to be read - push_pattern(&mut list, &path, |p| { + push_pattern(&mut list, path, |p| { escaped_pattern_with(p, if recursive { "**" } else { "*" }) })?; } @@ -165,7 +165,7 @@ impl Scope { let path = path.as_ref(); push_pattern( &mut self.allowed_patterns.lock().unwrap(), - &path, + path, escaped_pattern, )?; self.trigger(Event::PathAllowed(path.to_path_buf())); @@ -181,9 +181,9 @@ impl Scope { let mut list = self.forbidden_patterns.lock().unwrap(); // allow the directory to be read - push_pattern(&mut list, &path, escaped_pattern)?; + push_pattern(&mut list, path, escaped_pattern)?; // allow its files and subdirectories to be read - push_pattern(&mut list, &path, |p| { + push_pattern(&mut list, path, |p| { escaped_pattern_with(p, if recursive { "**" } else { "*" }) })?; } @@ -198,7 +198,7 @@ impl Scope { let path = path.as_ref(); push_pattern( &mut self.forbidden_patterns.lock().unwrap(), - &path, + path, escaped_pattern, )?; self.trigger(Event::PathForbidden(path.to_path_buf())); diff --git a/core/tauri/src/scope/shell.rs b/core/tauri/src/scope/shell.rs index bf6766f364c..26f0a3853f7 100644 --- a/core/tauri/src/scope/shell.rs +++ b/core/tauri/src/scope/shell.rs @@ -306,8 +306,8 @@ impl Scope { // The prevention of argument escaping is handled by the usage of std::process::Command::arg by // the `open` dependency. This behavior should be re-confirmed during upgrades of `open`. match with.map(Program::name) { - Some(program) => ::open::with(&path, program), - None => ::open::that(&path), + Some(program) => ::open::with(path, program), + None => ::open::that(path), } .map_err(Into::into) } diff --git a/examples/commands/main.rs b/examples/commands/main.rs index f04e579ab6b..5821eacfad4 100644 --- a/examples/commands/main.rs +++ b/examples/commands/main.rs @@ -177,7 +177,7 @@ async fn async_stateful_command_with_result( state: State<'_, MyState>, ) -> Result { println!("{:?} {:?}", the_argument, state.inner()); - Ok(the_argument.unwrap_or_else(|| "".to_string())) + Ok(the_argument.unwrap_or_default()) } // Non-Ident command function arguments diff --git a/examples/streaming/README.md b/examples/streaming/README.md index 2ece2f01dde..68fa3514b64 100644 --- a/examples/streaming/README.md +++ b/examples/streaming/README.md @@ -3,3 +3,5 @@ A simple Tauri Application showcase the streaming functionality. To execute run the following on the root directory of the repository: `cargo run --example streaming`. + +By default the example uses a custom URI scheme protocol. To use the builtin `asset` protocol, run `cargo run --example streaming --features protocol-asset`. diff --git a/examples/streaming/index.html b/examples/streaming/index.html index f46b8bdf399..83c9dc7f93e 100644 --- a/examples/streaming/index.html +++ b/examples/streaming/index.html @@ -1,28 +1,32 @@ - - - - - - - - - - + }) + + + + \ No newline at end of file diff --git a/examples/streaming/main.rs b/examples/streaming/main.rs index a448c944972..d05f8567d02 100644 --- a/examples/streaming/main.rs +++ b/examples/streaming/main.rs @@ -39,6 +39,7 @@ fn main() { } tauri::Builder::default() + .invoke_handler(tauri::generate_handler![video_uri]) .register_uri_scheme_protocol("stream", move |_app, request| { // prepare our response let mut response = ResponseBuilder::new(); @@ -46,7 +47,7 @@ fn main() { #[cfg(target_os = "windows")] let path = request.uri().strip_prefix("stream://localhost/").unwrap(); #[cfg(not(target_os = "windows"))] - let path = request.uri().strip_prefix("stream://").unwrap(); + let path = request.uri().strip_prefix("stream://localhost/").unwrap(); let path = percent_encoding::percent_decode(path.as_bytes()) .decode_utf8_lossy() .to_string(); @@ -117,3 +118,18 @@ fn main() { )) .expect("error while running tauri application"); } + +// returns the scheme and the path of the video file +// we're using this just to allow using the custom `stream` protocol or tauri built-in `asset` protocol +#[tauri::command] +fn video_uri() -> (&'static str, std::path::PathBuf) { + #[cfg(feature = "protocol-asset")] + { + let mut path = std::env::current_dir().unwrap(); + path.push("test_video.mp4"); + ("asset", path) + } + + #[cfg(not(feature = "protocol-asset"))] + ("stream", "example/test_video.mp4".into()) +} diff --git a/examples/streaming/tauri.conf.json b/examples/streaming/tauri.conf.json index 4dcad7297cb..b8baf642d4c 100644 --- a/examples/streaming/tauri.conf.json +++ b/examples/streaming/tauri.conf.json @@ -38,7 +38,10 @@ } }, "allowlist": { - "all": false + "all": false, + "protocol": { + "assetScope": ["**/test_video.mp4"] + } }, "windows": [ { @@ -50,7 +53,7 @@ } ], "security": { - "csp": "default-src 'self'; media-src stream: https://stream.localhost" + "csp": "default-src 'self'; media-src stream: https://stream.localhost asset: https://asset.localhost" }, "updater": { "active": false