rx.download accepts data
arg as either str or bytes (#2493)
* initial attempt that works for dataframe and text downloads * changes for masens comments * Instead of using blob, just send a data: URL from the backend * Enable rx.download directly with Var If the Var is string-like and starts with `data:`, then no special processing occurs. Otherwise, the value is passed to JSON.stringify and downloaded as text/plain. * event: update docstring and comments on rx.download Raise ValueError when URL and data are both provided, or the data provided is not one of the expected types. --------- Co-authored-by: Tom Gotsman <tomgotsman@toms-mbp.lan> Co-authored-by: Masen Furer <m_github@0x26.net>
This commit is contained in:
parent
6b6eea4d7d
commit
dec777485f
@ -152,12 +152,12 @@ export const applyEvent = async (event, socket) => {
|
|||||||
navigator.clipboard.writeText(content);
|
navigator.clipboard.writeText(content);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.name == "_download") {
|
if (event.name == "_download") {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.hidden = true;
|
a.hidden = true;
|
||||||
a.href = event.payload.url;
|
a.href = event.payload.url
|
||||||
if (event.payload.filename)
|
a.download = event.payload.filename;
|
||||||
a.download = event.payload.filename;
|
|
||||||
a.click();
|
a.click();
|
||||||
a.remove();
|
a.remove();
|
||||||
return false;
|
return false;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
from base64 import b64encode
|
||||||
from types import FunctionType
|
from types import FunctionType
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
@ -552,21 +553,26 @@ def set_clipboard(content: str) -> EventSpec:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def download(url: str | Var, filename: Optional[str | Var] = None) -> EventSpec:
|
def download(
|
||||||
"""Download the file at a given path.
|
url: str | Var | None = None,
|
||||||
|
filename: Optional[str | Var] = None,
|
||||||
|
data: str | bytes | Var | None = None,
|
||||||
|
) -> EventSpec:
|
||||||
|
"""Download the file at a given path or with the specified data.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url : The URL to the file to download.
|
url: The URL to the file to download.
|
||||||
filename : The name that the file should be saved as after download.
|
filename: The name that the file should be saved as after download.
|
||||||
|
data: The data to download.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If the URL provided is invalid.
|
ValueError: If the URL provided is invalid, both URL and data are provided,
|
||||||
|
or the data is not an expected type.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
EventSpec: An event to download the associated file.
|
EventSpec: An event to download the associated file.
|
||||||
"""
|
"""
|
||||||
if isinstance(url, Var) and filename is None:
|
from reflex.components.core.cond import cond
|
||||||
filename = ""
|
|
||||||
|
|
||||||
if isinstance(url, str):
|
if isinstance(url, str):
|
||||||
if not url.startswith("/"):
|
if not url.startswith("/"):
|
||||||
@ -576,6 +582,42 @@ def download(url: str | Var, filename: Optional[str | Var] = None) -> EventSpec:
|
|||||||
if filename is None:
|
if filename is None:
|
||||||
filename = url.rpartition("/")[-1]
|
filename = url.rpartition("/")[-1]
|
||||||
|
|
||||||
|
if filename is None:
|
||||||
|
filename = ""
|
||||||
|
|
||||||
|
if data is not None:
|
||||||
|
if url is not None:
|
||||||
|
raise ValueError("Cannot provide both URL and data to download.")
|
||||||
|
|
||||||
|
if isinstance(data, str):
|
||||||
|
# Caller provided a plain text string to download.
|
||||||
|
url = "data:text/plain," + data
|
||||||
|
elif isinstance(data, Var):
|
||||||
|
# Need to check on the frontend if the Var already looks like a data: URI.
|
||||||
|
is_data_url = data._replace(
|
||||||
|
_var_name=(
|
||||||
|
f"typeof {data._var_full_name} == 'string' && "
|
||||||
|
f"{data._var_full_name}.startsWith('data:')"
|
||||||
|
),
|
||||||
|
_var_type=bool,
|
||||||
|
_var_is_string=False,
|
||||||
|
_var_full_name_needs_state_prefix=False,
|
||||||
|
)
|
||||||
|
# If it's a data: URI, use it as is, otherwise convert the Var to JSON in a data: URI.
|
||||||
|
url = cond( # type: ignore
|
||||||
|
is_data_url,
|
||||||
|
data,
|
||||||
|
"data:text/plain," + data.to_string(), # type: ignore
|
||||||
|
)
|
||||||
|
elif isinstance(data, bytes):
|
||||||
|
# Caller provided bytes, so base64 encode it as a data: URI.
|
||||||
|
b64_data = b64encode(data).decode("utf-8")
|
||||||
|
url = "data:application/octet-stream;base64," + b64_data
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid data type {type(data)} for download. Use `str` or `bytes`."
|
||||||
|
)
|
||||||
|
|
||||||
return server_side(
|
return server_side(
|
||||||
"_download",
|
"_download",
|
||||||
get_fn_signature(download),
|
get_fn_signature(download),
|
||||||
|
Loading…
Reference in New Issue
Block a user