Skip to content

Commit

Permalink
feat: Add download started and download completed callbacks (#530)
Browse files Browse the repository at this point in the history
* 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
3 people authored Oct 19, 2022
1 parent 3183e93 commit 3691c4f
Show file tree
Hide file tree
Showing 10 changed files with 822 additions and 39 deletions.
5 changes: 5 additions & 0 deletions .changes/download.md
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.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ http = "0.2.8"
anyhow = "1.0"
tempfile = "3.3.0"
http-range = "0.1.5"
normpath = "0.3.2"
dirs = "4.0.0"
base64 = "0.13.0"

[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
webkit2gtk = { version = "0.18.2", features = [ "v2_36" ] }
Expand All @@ -61,6 +64,7 @@ soup2 = "0.2"
[target."cfg(target_os = \"windows\")".dependencies]
webview2-com = "0.19.1"
windows-implement = "0.39.0"
dunce = "1.0.2"

[target."cfg(target_os = \"windows\")".dependencies.windows]
version = "0.39.0"
Expand Down
198 changes: 198 additions & 0 deletions examples/blob_download.rs
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;
}
}
}
},
_ => (),
}
});
}
132 changes: 132 additions & 0 deletions examples/download_event.rs
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)
}
_ => (),
}
});
}
Loading

0 comments on commit 3691c4f

Please sign in to comment.