-
-
Notifications
You must be signed in to change notification settings - Fork 284
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add download started and download completed callbacks (#530)
* Add download handler to webview attributes * Potentially implement download event for windows * Add download event example * Implement download event for webkitgtk * Attempt to write example writing to tempdir * Add download compete handler, fix example * Update doc * Fix webkitgtk implementation for download handlers * Attempt to implement download events on macOS * Use more reliable URLs * Improve gtk implementation * Add more details to example * Attempt to write tempfile to documents * Fix download delegate funcs to implement on navdelegate on macOS Also adds the complete and failed callbacks. * dummy commit * Update webkit2gtk * Match changes on dev * Split download handlers Now has handler for download start/deny and download completed callback * Propagate split of download handlers to win impl * Switch to mutable ref PathBuf instead of string * Wrap download_completed callback in Rc This avoids the indirection of the closure builder pattern whilst still solving the lifetime issues. * Windows formatting * Fix merge in linux implementation Downloads still don't actually complete though * Fix macOS implementation + refactor * Rework example Now holds temp_dir as long as necessary to prevent premature drop (and thus deleting our example file). Requires the use of the Rc<RefCell> pattern for interior mutability across a shared reference - this (should) be safe, as the entire example is single threaded, and any mutable borrows should be dropped by the time a borrow ocurrs. * Ignore unused parameter * Improve download example handling of empty path * Formatting * Attempt to improve linux behaviour * Attempt to fix Windows compile errors * Separate download complete handler from download started handler * Formatting * Take closure by mutable reference (windows) * Workaround mutable borrow issues on windows * Separate download started handler from finished on linux * Potentially improve setting output path on linux * Add original download's url as parameter to download completed handler * Formatting * Standardise terminology (replace `callback` with `handler`) * Use dunce to attempt to remove UNC prefixes on windows * Fix incorrect function signature on macOS * Improve docs * Enable devtools in example * Include blob download example macOS implementation works, but may rely on an incorrect assumption - clicking the download link seems to return a random blob URL, so we just grab the first saved blob URL * Formatting * Address comment regarding passing pathbuf to completion handler * Separate download completed from started handler on macOS * Formatting * Move download ffi function to download module * Add change file * Correct the name of download start method Co-authored-by: Iain Laird <[email protected]> Co-authored-by: Wu Wayne <[email protected]>
- Loading branch information
1 parent
3183e93
commit 3691c4f
Showing
10 changed files
with
822 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"wry": patch | ||
--- | ||
|
||
On Desktop, add `download_started_handler` and `download_completed_handler`. See `blob_download` and `download_event` example for their usages. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// SPDX-License-Identifier: MIT | ||
|
||
use std::{ | ||
fs::File, | ||
io::{Read, Write}, | ||
path::PathBuf, | ||
}; | ||
|
||
use base64::decode; | ||
use tempfile::tempdir; | ||
|
||
fn main() -> wry::Result<()> { | ||
use wry::{ | ||
application::{ | ||
event::{Event, StartCause, WindowEvent}, | ||
event_loop::{ControlFlow, EventLoop}, | ||
window::WindowBuilder, | ||
}, | ||
webview::WebViewBuilder, | ||
}; | ||
|
||
let html = r#" | ||
<body> | ||
<div> | ||
<a download="hello.txt" href='#' id="link">Download</a> | ||
<script> | ||
const example = new Blob(["Hello, world!"], {type: 'text/plain'}); | ||
link.href = URL.createObjectURL(example); | ||
</script> | ||
</div> | ||
</body> | ||
"#; | ||
|
||
enum UserEvent { | ||
BlobReceived(String), | ||
BlobChunk(Option<String>), | ||
} | ||
|
||
let init_script = r" | ||
// Adds an URL.getFromObjectURL( <blob:// URI> ) method | ||
// returns the original object (<Blob> or <MediaSource>) the URI points to or null | ||
(() => { | ||
// overrides URL methods to be able to retrieve the original blobs later on | ||
const old_create = URL.createObjectURL; | ||
const old_revoke = URL.revokeObjectURL; | ||
Object.defineProperty(URL, 'createObjectURL', { | ||
get: () => storeAndCreate | ||
}); | ||
Object.defineProperty(URL, 'revokeObjectURL', { | ||
get: () => forgetAndRevoke | ||
}); | ||
Object.defineProperty(URL, 'getFromObjectURL', { | ||
get: () => getBlob | ||
}); | ||
Object.defineProperty(URL, 'getObjectURLDict', { | ||
get: () => getDict | ||
}); | ||
Object.defineProperty(URL, 'clearURLDict', { | ||
get: () => clearDict | ||
}); | ||
const dict = {}; | ||
|
||
function storeAndCreate(blob) { | ||
const url = old_create(blob); // let it throw if it has to | ||
dict[url] = blob; | ||
console.log(url) | ||
console.log(blob) | ||
return url | ||
} | ||
|
||
function forgetAndRevoke(url) { | ||
console.log(`revoke ${url}`) | ||
old_revoke(url); | ||
} | ||
|
||
function getBlob(url) { | ||
return dict[url] || null; | ||
} | ||
|
||
function getDict() { | ||
return dict; | ||
} | ||
|
||
function clearDict() { | ||
dict = {}; | ||
} | ||
})(); | ||
"; | ||
|
||
let event_loop: EventLoop<UserEvent> = EventLoop::with_user_event(); | ||
let proxy = event_loop.create_proxy(); | ||
let window = WindowBuilder::new() | ||
.with_title("Hello World") | ||
.build(&event_loop)?; | ||
let webview = WebViewBuilder::new(window)? | ||
.with_html(html)? | ||
.with_initialization_script(init_script) | ||
.with_download_started_handler({ | ||
let proxy = proxy.clone(); | ||
move |uri: String, _: &mut PathBuf| { | ||
if uri.starts_with("blob:") { | ||
let _ = proxy.send_event(UserEvent::BlobReceived(dbg!(uri))); | ||
} | ||
|
||
false | ||
} | ||
}) | ||
.with_ipc_handler({ | ||
let proxy = proxy.clone(); | ||
move |_, string| match string.as_str() { | ||
_ if string.starts_with("data:") => { | ||
let _ = proxy.send_event(UserEvent::BlobChunk(Some(string))); | ||
} | ||
"#EOF" => { | ||
let _ = proxy.send_event(UserEvent::BlobChunk(None)); | ||
} | ||
_ => {} | ||
} | ||
}) | ||
.with_devtools(true) | ||
.build()?; | ||
|
||
#[cfg(debug_assertions)] | ||
webview.open_devtools(); | ||
|
||
let mut blob_file = None; | ||
event_loop.run(move |event, _, control_flow| { | ||
*control_flow = ControlFlow::Wait; | ||
|
||
match event { | ||
Event::NewEvents(StartCause::Init) => println!("Wry has started!"), | ||
Event::WindowEvent { | ||
event: WindowEvent::CloseRequested, | ||
.. | ||
} => *control_flow = ControlFlow::Exit, | ||
Event::UserEvent(UserEvent::BlobReceived(uri)) => { | ||
let temp_dir = tempdir().expect("Create temp dir"); | ||
blob_file = Some((File::create(&temp_dir.path().join("blob.txt")).expect("Create file"), temp_dir)); | ||
webview.evaluate_script(&format!(r#" | ||
(() => {{ | ||
/** | ||
* @type Blob | ||
*/ | ||
let blob = URL.getObjectURLDict()['{}'] | ||
|| Object.values(URL.getObjectURLDict())[0] // For some reason macOS returns a completely random blob URL? Just grab the first one | ||
|
||
var increment = 1024; | ||
var index = 0; | ||
var reader = new FileReader(); | ||
let func = function() {{ | ||
let res = reader.result; | ||
window.ipc.postMessage(`${{res}}`); | ||
index += increment; | ||
if (index < blob.size) {{ | ||
let slice = blob.slice(index, index + increment); | ||
reader = new FileReader(); | ||
reader.onloadend = func; | ||
reader.readAsDataURL(slice); | ||
}} else {{ | ||
window.ipc.postMessage('#EOF'); | ||
}} | ||
}}; | ||
reader.onloadend = func; | ||
reader.readAsDataURL(blob.slice(index, increment)) | ||
}})(); | ||
"#, uri)).expect("Eval script"); | ||
}, | ||
Event::UserEvent(UserEvent::BlobChunk(chunk)) => { | ||
if let Some((file, path)) = blob_file.as_mut() { | ||
match chunk { | ||
Some(chunk) => { | ||
let split = chunk.split(',').nth(1); | ||
println!("{:?}", chunk.split(',').next()); | ||
if let Some(split) = split { | ||
if let Ok(decoded) = decode(split) { | ||
if file.write(&decoded).is_err() { | ||
eprintln!("Failed to write bytes to temp file") | ||
} | ||
} | ||
} | ||
}, | ||
None => { | ||
let mut file = File::open(&path.path().join("blob.txt")).expect("Open temp file"); | ||
let mut content = String::new(); | ||
file.read_to_string(&mut content).expect("Read contents of file"); | ||
println!("Contents of file:"); | ||
println!("{}", content); | ||
blob_file = None; | ||
} | ||
} | ||
} | ||
}, | ||
_ => (), | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// SPDX-License-Identifier: MIT | ||
|
||
use std::{cell::RefCell, path::PathBuf, rc::Rc}; | ||
|
||
use normpath::PathExt; | ||
use tempfile::tempdir; | ||
|
||
fn main() -> wry::Result<()> { | ||
use wry::{ | ||
application::{ | ||
event::{Event, StartCause, WindowEvent}, | ||
event_loop::{ControlFlow, EventLoop}, | ||
window::WindowBuilder, | ||
}, | ||
webview::WebViewBuilder, | ||
}; | ||
|
||
let html = r#" | ||
<body> | ||
<div> | ||
<p> WRYYYYYYYYYYYYYYYYYYYYYY! </p> | ||
<a download="allow.zip" href='https://github.com/tauri-apps/wry/archive/refs/tags/wry-v0.13.3.zip' id="link">Allowed Download</a> | ||
<a download="deny.zip" href='https://github.com/tauri-apps/wry/archive/refs/tags/wry-v0.13.2.zip' id="link">Denied Download</a> | ||
</div> | ||
</body> | ||
"#; | ||
|
||
enum UserEvent { | ||
DownloadStarted(String, String), | ||
DownloadComplete(Option<PathBuf>, bool), | ||
Rejected(String), | ||
} | ||
|
||
let temp_dir = Rc::new(RefCell::new(None)); | ||
let event_loop: EventLoop<UserEvent> = EventLoop::with_user_event(); | ||
let proxy = event_loop.create_proxy(); | ||
let window = WindowBuilder::new() | ||
.with_title("Hello World") | ||
.build(&event_loop)?; | ||
let webview = WebViewBuilder::new(window)? | ||
.with_html(html)? | ||
.with_download_started_handler({ | ||
let proxy = proxy.clone(); | ||
let tempdir_cell = temp_dir.clone(); | ||
move |uri: String, default_path: &mut PathBuf| { | ||
if uri.contains("wry-v0.13.3") { | ||
if let Ok(tempdir) = tempdir() { | ||
if let Ok(path) = tempdir.path().normalize() { | ||
tempdir_cell.borrow_mut().replace(tempdir); | ||
|
||
let path = path.join("example.zip").as_path().to_path_buf(); | ||
|
||
*default_path = path.clone(); | ||
|
||
let submitted = proxy | ||
.send_event(UserEvent::DownloadStarted( | ||
uri.clone(), | ||
path.display().to_string(), | ||
)) | ||
.is_ok(); | ||
|
||
return submitted; | ||
} | ||
} | ||
} | ||
|
||
let _ = proxy.send_event(UserEvent::Rejected(uri.clone())); | ||
|
||
false | ||
} | ||
}) | ||
.with_download_completed_handler({ | ||
let proxy = proxy.clone(); | ||
move |_uri, path, success| { | ||
let _ = proxy.send_event(UserEvent::DownloadComplete(path, success)); | ||
} | ||
}) | ||
.with_devtools(true) | ||
.build()?; | ||
|
||
#[cfg(debug_assertions)] | ||
webview.open_devtools(); | ||
|
||
event_loop.run(move |event, _, control_flow| { | ||
*control_flow = ControlFlow::Wait; | ||
|
||
match event { | ||
Event::NewEvents(StartCause::Init) => println!("Wry has started!"), | ||
Event::WindowEvent { | ||
event: WindowEvent::CloseRequested, | ||
.. | ||
} => *control_flow = ControlFlow::Exit, | ||
Event::UserEvent(UserEvent::DownloadStarted(uri, temp_dir)) => { | ||
println!("Download: {}", uri); | ||
println!("Will write to: {:?}", temp_dir); | ||
} | ||
Event::UserEvent(UserEvent::DownloadComplete(mut path, success)) => { | ||
let _temp_dir_guard = if path.is_none() && success { | ||
let temp_dir = temp_dir.borrow_mut().take(); | ||
path = Some( | ||
temp_dir | ||
.as_ref() | ||
.expect("Stored temp dir") | ||
.path() | ||
.join("example.zip"), | ||
); | ||
temp_dir | ||
} else { | ||
None | ||
}; | ||
println!("Succeeded: {}", success); | ||
if let Some(path) = path { | ||
let metadata = path.metadata(); | ||
println!("Path: {}", path.to_string_lossy()); | ||
if let Ok(metadata) = metadata { | ||
println!("Size of {}Mb", (metadata.len() / 1024) / 1024) | ||
} else { | ||
println!("Failed to retrieve file metadata - does it exist?") | ||
} | ||
} else { | ||
println!("No output path") | ||
} | ||
} | ||
Event::UserEvent(UserEvent::Rejected(uri)) => { | ||
println!("Rejected download from: {}", uri) | ||
} | ||
_ => (), | ||
} | ||
}); | ||
} |
Oops, something went wrong.