Przeglądaj źródła

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>
Tom Gotsman 1 rok temu
rodzic
commit
dec777485f
2 zmienionych plików z 52 dodań i 10 usunięć
  1. 3 3
      reflex/.templates/web/utils/state.js
  2. 49 7
      reflex/event.py

+ 3 - 3
reflex/.templates/web/utils/state.js

@@ -152,12 +152,12 @@ export const applyEvent = async (event, socket) => {
     navigator.clipboard.writeText(content);
     return false;
   }
+
   if (event.name == "_download") {
     const a = document.createElement('a');
     a.hidden = true;
-    a.href = event.payload.url;
-    if (event.payload.filename)
-      a.download = event.payload.filename;
+    a.href = event.payload.url
+    a.download = event.payload.filename;
     a.click();
     a.remove();
     return false;

+ 49 - 7
reflex/event.py

@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 import inspect
+from base64 import b64encode
 from types import FunctionType
 from typing import (
     TYPE_CHECKING,
@@ -552,21 +553,26 @@ def set_clipboard(content: str) -> EventSpec:
     )
 
 
-def download(url: str | Var, filename: Optional[str | Var] = None) -> EventSpec:
-    """Download the file at a given path.
+def download(
+    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:
-        url : The URL to the file to download.
-        filename : The name that the file should be saved as after download.
+        url: The URL to the file to download.
+        filename: The name that the file should be saved as after download.
+        data: The data to download.
 
     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:
         EventSpec: An event to download the associated file.
     """
-    if isinstance(url, Var) and filename is None:
-        filename = ""
+    from reflex.components.core.cond import cond
 
     if isinstance(url, str):
         if not url.startswith("/"):
@@ -576,6 +582,42 @@ def download(url: str | Var, filename: Optional[str | Var] = None) -> EventSpec:
         if filename is None:
             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(
         "_download",
         get_fn_signature(download),