How to use the downloader components
Use the PlainDownloader
component
Dash File Cache provides a customized component that can fire a downloading event by a callback.
downloader = PlainDownloader(id: str)
The following codes show a comparison between the implementation with PlainDownloader
and without PlainDownloader
. They are implementations with the equivalent
functionalities.
- With Downloader
- Without Downloader
import io
from typing import Optional
import dash
from dash import html
from dash import Output, Input
import dash_file_cache as dfc
app = dash.Dash("demo")
service = dfc.ServiceData(dfc.CachePlain(1))
service.serve(app)
app.layout = html.Div(
(
html.Div(html.Button(id="btn", children="Download")),
dfc.PlainDownloader(id="downloader")
)
)
@app.callback(Output("downloader", "url"), Input("btn", "n_clicks"))
def a_callback_creating_data(n_clicks: Optional[int]) -> str:
if not n_clicks:
return dash.no_update
address = service.register(
fobj=io.StringIO("test file data..."),
file_name="test.txt",
mime_type="text/plain",
one_time_service=True,
download=True,
)
return address
if __name__ == "__main__":
app.run()
import io
from typing import Optional
import dash
from dash import html
from dash import Output, Input
import dash_file_cache as dfc
app = dash.Dash("demo")
service = dfc.ServiceData(dfc.CachePlain(1))
service.serve(app)
app.layout = html.Div(
(
html.Div(html.Button(id="btn", children="Download")),
html.Div(id="downloader-js-finish-trigger", hidden=True),
html.Div(id="downloader-js-trigger", hidden=True),
html.Div(id="downloader-trigger", hidden=True),
)
)
@app.callback(Output("downloader-trigger", "children"), Input("btn", "n_clicks"))
def a_callback_creating_data(n_clicks: Optional[int]) -> str:
if not n_clicks:
return dash.no_update
address = service.register(
fobj=io.StringIO("test file data..."),
file_name="test.txt",
mime_type="text/plain",
one_time_service=True,
download=True,
)
return address
@app.callback(
Output("downloader-js-trigger", "children"),
Input("downloader-trigger", "children"),
prevent_initial_call=True,
)
def download_redirect(trigger: Optional[str]):
"""Trigger of download link redirection."""
if not trigger:
return dash.no_update
return trigger
app.clientside_callback(
"""
function (uri) {
var link = document.createElement("a");
link.setAttribute("download", "");
link.setAttribute("target", "_blank");
link.setAttribute("rel", "no-refresh");
link.href = uri;
document.body.appendChild(link);
link.click();
link.remove();
return "success";
}
""",
Output("downloader-js-finish-trigger", "children"),
Input("downloader-js-trigger", "children"),
prevent_initial_call=True,
)
if __name__ == "__main__":
app.run()
PlainDownloader(...)
is the simplest component providing the downloading features.
By firing the callback on the property url
, a downloading event will be triggered
instantly.
The implementations behind PlainDownloader
is totally the same as the the above
example without_downloader.py
. When firing the callback on url
, a temporary and
invisilbe <a>
tag will be created and clicked. After the downloading starts, the
<a>
tag will be removed. The url can be provided by the cache of the
ServiceData(...)
.
Remember to specify the file_name
and mark the download=True
when registering the file
to the cache. These options ensure that the dynamic link to the file will trigger the downloading
event, and the name of the downloaded file will be specified by file_name
.
Why should we use this downloader?
Until dash==3.0.x
, the dcc.Download
still
works on the bytes data directly, which means that, the data will be encoded as bytes
on the server side and sent to the client (browser). In other words,
dcc.send_file
is not a wrapper of
flask.send_file
, it cannot handle
large-size data.
In the meanwhile, dcc.Download
is implemented
with FileSaver.js
which will store all the data
to be downloaded in a blob
first. It means that dcc.Download
does not have the
ability to handle the large file, either.
In comparison, our solution is based on
flask.stream_with_context
,
and the downloader accesses the address of the file directly, which is compatible with
an arbirary size of the data.
PlainDownloader
is implemented by a temporary link. It totally relies on the native
functionalities of the browser to download the file. So, it will not meet any issue
in the following cases:
- Download a large-size file.
- Access a cross-origin resource (i.e. a file from another site) even if the resource does not provide any cross-origin allowance.
Use a Downloader
component with more fine-grained controls
dfc.PlainDownloader
is useful enough in most cases. However, if users need the following
features, they may need to use the fine-grained version dfc.Downloader
.
- Users need to fire a callback after the downloading event is finalized.
- Users need to catch the errors caused by the downloadnig events, like losing connection or canceled by users.
The cases cannot be handled by dfc.PlainDownloader
because PlainDownloader
does not
have any control of view on the downloading event which is deligated to the browser.
In comparison, dfc.Downloader
makes the downloading event fully controled by the
javascript module StreamSaver.js
, thus
allowing more fine-grained controls.
import io
from typing import Optional
import dash
from dash import html
from dash import Output, Input
import dash_file_cache as dfc
app = dash.Dash("demo")
service = dfc.ServiceData(dfc.CachePlain(1))
service_mitm = dfc.ServiceDownloader("/dfc-downloader")
service.serve(app)
service_mitm.serve(app)
app.layout = html.Div(
(
html.Div(html.Button(id="btn", children="Download")),
dfc.Downloader(id="downloader", mitm="/dfc-downloader"),
html.Div(id="trigger"),
)
)
@app.callback(Output("downloader", "url"), Input("btn", "n_clicks"))
def a_callback_creating_data(n_clicks: Optional[int]) -> str:
if not n_clicks:
return dash.no_update
address = service.register(
fobj=io.StringIO("test file data..."),
file_name="test.txt",
mime_type="text/plain",
one_time_service=True,
download=True,
)
return address
@app.callback(
Output("trigger", "children"),
Input("downloader", "status"),
prevent_initial_call=True,
)
def trigger_get_status(status: Optional[dfc.DownloaderStatus]):
if not status:
return dash.no_update
return str(status)
if __name__ == "__main__":
app.run()
Compared to the previous example, the usages of Downloader
have the following
differences:
- A special
ServiceDownloader
is preconfigured with a service route/dfc-downloader
. - The same route is used as the
mitm
property ofdfc.Downloader(mitm=...)
. - The property
status
of theDownloader
is catched by a<div>
tag.
The property mitm
provides a technique for preserving the downloading event active.
Configuring the mitm
property is important. If users do not configure it, the default
mitm
property will refer to a remote site hosted
by jimmywarting's GitHub site which
will be not accessible if the device is offline. By using ServiceDownloader()
,
the files required by the mitm
property will be hosted by the local device.
The mitm
property of Downloader
needs to be configured the same as the
route used for initializing the service ServiceDownloader
. Otherwise, users may
find that the download event does not start and no error messages are catched.
The second callback catches the finalizing event triggered by the Downloader
. The
status dictionary contains a string code and an HTTP code. If the download event
finalizes successfully, the string code would be success
.
Use a Downloader
component for fetching a preconfigured cross-origin resource
In some cases, users may need to deploy multiple services on different hosts. Suppose that the following scripts are run in two different processes, respectively,
- Dashboard: Downloader
- Service: File Source
import io
from typing import Optional
import dash
from dash import html
from dash import Output, Input
import dash_file_cache as dfc
app = dash.Dash("demo")
service = dfc.ServiceData(dfc.CachePlain(1))
service_mitm = dfc.ServiceDownloader("/dfc-downloader")
service.serve(app)
service_mitm.serve(app)
app.layout = html.Div(
(
html.Div(html.Button(id="btn", children="Download")),
dfc.Downloader(
id="downloader", allow_cross_origin=True, mitm="/dfc-downloader"
),
html.Div(id="trigger"),
)
)
@app.callback(Output("downloader", "url"), Input("btn", "n_clicks"))
def a_callback_creating_data(n_clicks: Optional[int]) -> str:
if not n_clicks:
return dash.no_update
return "http://127.0.0.1:8081/file"
@app.callback(
Output("trigger", "children"),
Input("downloader", "status"),
prevent_initial_call=True,
)
def trigger_get_status(status: Optional[dfc.DownloaderStatus]):
if not status:
return dash.no_update
return str(status)
if __name__ == "__main__":
app.run(host="0.0.0.0", port="8080")
import io
import flask
from dash_file_cache import CachePlain, ServiceData
app = flask.Flask("serve")
service = ServiceData(CachePlain(1), allowed_cross_origin="*")
@app.route("/file")
def file():
addr = service.register(
fobj=io.StringIO("test some files."),
file_name="new.txt",
mime_type="text/plain",
download=True,
)
resp = flask.redirect(addr, code=302)
resp.headers["Access-Control-Allow-Origin"] = "*"
return resp
if __name__ == "__main__":
service.serve(app)
app.run(host="0.0.0.0", port=8081)
The two scripts need to be run simultaneously. On the dashboard side, the component
property allow_cross_origin
is configured as True
, which means that the downloader
will try to access the cross-origin resources if possible. In this case, the request
will work in the cors
mode.
It is not ready enough if the cross-origin configurations are made only on the
dashboard side. On the server side, the response need to contain several headers
related to the cross-origin access. Therefore, in the above example, we configure
the service argument allowed_cross_origin
to allow the cross-origin access from all
devices. In this case, every response provided by ServiceData
will be automatically
configured with cross-origin headers.
The most important thing to access a cross-origin service is that the cross-origin
headers are configured on the accessed side. If the remote resource is not configured
as cross-origin, even if the property allow_cross_origin
is specified on the
downloader side, the resource is still not accessible.
Use a Downloader
component for fetching a fully remote file
In some cases, we may need to access a remote file, but we do not have the access to configure the cross-origin services of the remote device. In this case, we have provided a proxy-based way to access the cross-site data.
import io
from typing import Optional
import dash
from dash import html
from dash import Output, Input
import dash_file_cache as dfc
app = dash.Dash("demo")
service = dfc.ServiceData(dfc.CachePlain(1))
service_mitm = dfc.ServiceDownloader("/dfc-downloader")
service.serve(app)
service_mitm.serve(app)
app.layout = html.Div(
(
html.Div(html.Button(id="btn", children="Download")),
dfc.Downloader(id="downloader" mitm="/dfc-downloader"),
html.Div(id="trigger"),
)
)
@app.callback(Output("downloader", "url"), Input("btn", "n_clicks"))
def a_callback_creating_data(n_clicks: Optional[int]) -> str:
if not n_clicks:
return dash.no_update
return service.register_request("https://testfile.org/1.3GBiconpng", download=True)
@app.callback(
Output("trigger", "children"),
Input("downloader", "status"),
prevent_initial_call=True,
)
def trigger_get_status(status: Optional[dfc.DownloaderStatus]):
if not status:
return dash.no_update
return str(status)
if __name__ == "__main__":
app.run(host="0.0.0.0", port="8080")
The method service.register_request
allows users to register remote URLs to the cache.
In this case, the local device will serve as a proxy, or a middleware. Before being
accessed, the cache only stores the URL of the file. Once the file is accessed, the
file will be loaded and sent chunk-by-chunk. For each chunk, the data is fetched from
the remote site to the dashboard host device first. After that, the file data is
forwarded to the browser. Since the browser get the file from the dashboard host,
there is no need to make any cross-origin configurations.
Summary
The following table summarizes, compares and show the usages of different downloaders.
Large File | Work offline | File from another site | Catch the event status | |
---|---|---|---|---|
PlainDownloader() | ||||
Downloader() | ||||
Downloader(mitm=...) | ||||
Downloader(allow_cross_origin) | Cross-origin only | |||
url=service.register_request |
Please configure the downloader properly according to the need. Typically, mitm
should be configured, and users can access any Internet files by using the cached
address returned by service.register_request(...)
.