跳到主要内容
版本:0.2.0

使用下载组件

使用PlainDownloader组件

Dash File Cache提供了一个定制化的组件,用来透过callback触发下载事件。

downloader = PlainDownloader(id: str)

以下代码对比了分别使用、和不使用PlainDownloader的实现。这两种实现的效果是等价的。

with_plain_downloader.py
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()

PlainDownloader(...)是最简单的、用来提供下载功能的组件。透过触发属性url的callback,可以立即触发一个下载事件。

PlainDownloader背后实现的逻辑、和上例without_downloader.py的做法是完全一致的。在触发url的callback时,会临时创建一个不可见的<a>标签、并自动点击它。下载事件开始后,就会移除该<a>标签。标签所设的地址、由ServiceData(...)的缓存提供。

提示

将文件注册到缓存时,切记指定file_name的值,并且标记download=True。这些设置确保了动态生成的链接必定会触发下载事件,且所下载文件的文件名由file_name指定。

使用该下载组件的理由

直到dash==3.0.xdcc.Download组件仍然直接作用于字节串数据。这意味着,数据会先在服务端编码成字节串、然后发送到客户端(浏览器)。换言之,dcc.send_file不是flask.send_file的封装。它无法处理大体量的数据。

与此同时,由于dcc.Download是透过FileSaver.js实现的,它只能在下载数据之前,将全部数据预存在blob里。这意味着dcc.Download也没有能力处理大体量数据。

相对地,这里提供的解决方案,是基于flask.stream_with_context的。下载组件直接访问可下载数据的地址、从而确保了与任意大小的数据兼容。

PlainDownloader透过一个临时超链接来实现。亦即是说,它下载文件时、完全依赖于浏览器的原生功能。故而,在以下应用场景里不会有任何问题:

  • 下载一个大体量文件。
  • 跨域访问一个文件(亦即下载来自其他站点的文件),哪怕服务端不允许跨域访问。

使用具有更细粒度的Downloader组件

在大多情形下,dfc.PlainDownloader已经是足够有用了。然而,如果用户需要以下功能,则应当考虑使用更细粒度版本的dfc.Downloader

  • 用户需要在下载事件结束时、触发一个callback。
  • 用户需要捕获下载事件引发的错误,例如断线、或者取消下载。

dfc.PlainDownloader处理不了这些情景,这是因为它的下载事件完全委派给了浏览器,以至于它不具备控制、检查下载事件的能力。相对地、dfc.Downloader实现的下载事件、完全处于javascript模块StreamSaver.js的控制下,故而允许更细粒度的控制。

with_downloader.py
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()

相比于前例,Downloader的用法有以下不同:

  • 需要设置一个特殊的ServiceDownloader服务,其路由设为/dfc-downloader
  • 同一路由也用于设置dfc.Downloader(mitm=...)的属性mitm
  • 使用一个<div>标签捕获Downloaderstatus属性。

属性mitm用来维持下载事件的激活状态。针对mitm属性的配置是十分有必要的。如果用户不设置它,则会使用默认的mitm属性。这种情况下,它会引用来自jimmywarting的GitHub站点的文件,就会造成离线情况下、不能正常工作的问题。透过设置ServiceDownloader()mitm所需的文件可以配置在本地服务中。

危险

Downloadermitm属性,需要配置得和ServiceDownloader的初始化参数相同。否则,用户可能会发现下载事件无法触发、且捕获不到任何错误信息。

第二个callback捕获了Downloader在下载结束时触发的事件。所得的状态字典、包含了一条字符串状态码、和一条HTTP响应状态码。若下载事件成功完成,则字符状态码为success

使用Downloader组件获取预配置过的跨域资源

在某些情况下,用户可能会需要在不同主机部署多个服务。设若以下脚本分别运行在两个不同的进程中,

cross_origin_download.py
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")

同时运行这些脚本。在dashboard这一侧,配置组件属性allow_cross_originTrue,从而允许下载器在可行的情况下、访问跨域资源。在这种情况下,发出的请求会按照cors模式工作。

仅仅在dashboard一侧做跨域配置是不够的。在服务端,所提供的响应同样需要配置几条、和跨域调用有关的响应头。因此,上例中、将allowed_cross_origin设置为允许来自所有设备的跨域访问。在此情形下,ServiceData所提供的每个响应,都将自动配置好所需的跨域调用响应头。

注意

在被访问的数据一侧、配置跨域调用响应头是重中之重。若远端的资源没有配置成允许跨域,即使在下载器所在的这一段、配置了allow_cross_origin属性,也还是无法访问远端的资源。

使用Downloader组件获取完全处于远端的文件

某些情形下,可能需要访问远端的文件,但又没有办法为远端设备、做任何跨域服务的配置。这样的话,就可以使用一种基于代理的方法来访问跨站数据。

cross_site_download.py
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")

方法service.register_request允许用户将某些指向远端资源的URL、注册到缓存中。这样一来,本地设备就会像代理服务器、或中间件一样工作。在访问该URL之前,缓存只包含文件的URL。一俟访问开始,则会逐块加载、并发送文件。针对每个文件块,数据先从远端站点下载到dashboard所在的主机。其后,将文件块传递给浏览器。由于浏览器只从dashboard主机获取文件,在此情形下、就不需要做任何跨域配置。

总结

下表总结、比较了不同下载器的用法。

大体量文件离线工作跨站访问文件捕获事件状态
PlainDownloader()
Downloader()
Downloader(mitm=...)
Downloader(allow_cross_origin)仅允许跨域访问
url=service.register_request

请按照实际需要、妥善地配置下载器。一般来说,总是需要配置mitm属性,并且用户可以使用service.register_request(...)返回的缓存地址、获取来自因特网的任何文件。