I'm trying to offer file downloads (.csv and .png) to users of a Bokeh webapp. Currently this is implemented (as per this example and this SO question) by saving a copy of the file locally and executing Javascript that reads that file:
def save_file(self, save_function, file_name):
"""
:param save_function: callable that takes a single argument: the file path to save to
:param file_name: default name for the downloaded file
"""
js_download = """
fetch(local_path, {cache: "no-store"}).then(response => response.blob())
.then(blob => {
//addresses IE
if (navigator.msSaveBlob) {
navigator.msSaveBlob(blob, save_name);
}
else {
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = save_name
link.target = '_blank'
link.style.visibility = 'hidden'
link.dispatchEvent(new MouseEvent('click'))
}
return response.text();
});
"""
# Make a tempory file name to avoid conflicts
temp_name = next(tempfile._get_candidate_names())
temp_path = f'static/{temp_name}.tmp'
save_function(temp_path)
self.exectute_js(CustomJS(args=dict(local_path='/AzDataTools/' + temp_path,
save_name=file_name), code=js_download))
def exectute_js(self, custom_js):
# Setup a dummy plot to trigger the javascript
dummy = self.get_any_figure().circle([1], [2], alpha=0)
dummy.glyph.js_on_change('size', custom_js)
dummy.glyph.size = 1
Ideally, I'd like to this skipping the local file step by saving to a memory stream. My attempt at this looks like:
def save_file_via_memory(self, save_function, file_name):
"""
:param save_function: callable that takes a single argument: the file path to save to
:param file_name: default name for the downloaded file
"""
js_download = """
var blob = new Blob(data)
//addresses IE
if (navigator.msSaveBlob) {
navigator.msSaveBlob(blob, save_name);
}
else {
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = save_name
link.target = '_blank'
link.style.visibility = 'hidden'
link.dispatchEvent(new MouseEvent('click'))
};
"""
with io.BytesIO() as data:
save_function(data)
self.exectute_js(CustomJS(args=dict(data=data, save_name=file_name), code=js_download))
However, this generates errors indicating the byte stream cannot be serialised. Does anybody know how to pass the byte stream to the JavaScript? (the JavaScript may also be incorrect, this is the first time I've used it...)
The workaround I've ended up with is to convert the file to a text string. For csvs and images this looks like:
def save_text_file(self, text, file_name):
"""
:param text: text to save
:param file_name: default name for the downloaded file
"""
js_download = """
const blob = new Blob([text_string], { type: 'text/csv;charset=utf-8;' })
//addresses IE
if (navigator.msSaveBlob) {
navigator.msSaveBlob(blob, file_name)
} else {
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = file_name
link.target = '_blank'
link.style.visibility = 'hidden'
link.dispatchEvent(new MouseEvent('click'))
}
"""
self.exectute_js(CustomJS(args=dict(text_string=text, file_name=file_name), code=js_download))
def image_to_png_string(image_bytes):
data_uri = base64.b64encode(image_bytes).decode('utf-8')
return 'data:image/png;base64,' + data_uri
def save_image_file(self, image, file_name):
"""
:param image: image bytes to save
:param file_name: default name for the downloaded file
"""
js_download = """
fetch(image_string, {cache: "no-store"}).then(response => response.blob())
.then(blob => {
//addresses IE
if (navigator.msSaveBlob) {
navigator.msSaveBlob(blob, file_name);
}
else {
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = file_name
link.target = '_blank'
link.style.visibility = 'hidden'
link.dispatchEvent(new MouseEvent('click'))
}
return response.text();
});
"""
self.exectute_js(CustomJS(args=dict(image_string=image_to_png_string(image), file_name=file_name),
code=js_download))