Button to download a file

I’ve built a Plotly Dash app that allows users to navigate through directories and download files. The files are .log files and are converted into .csv format before download.

The issue I’m facing is with the download functionality. When I first click the download button, it downloads the previously requested file (or first time it will download html page instead). Only when I click the download button for the second time, it downloads the correct file.

Here’s the code, where file_path is the path to the log file to be converted and downloaded (note update_download_link callback is the one that does not work correctly):

import datetime
import os
from pathlib import Path
import dash_bootstrap_components as dbc
import pandas as pd
from dash import ALL, Dash, Input, Output, State, callback_context, html, dcc
from dash.exceptions import PreventUpdate
from icons import icons
import io
import time
import uuid


def serve_layout():
    app_layout = html.Div([
        html.Link(
            rel="stylesheet",
            href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css"),
        html.Br(), html.Br(),
        dbc.Row([
            dbc.Col(lg=1, sm=1, md=1),
            dbc.Col([
                dcc.Store(id='stored_cwd', data=os.getcwd()),
                html.H1('File Browser'),
                html.Hr(), html.Br(), html.Br(), html.Br(),
                html.H5(html.B(html.A("⬆️ Parent directory", href='#',
                                    id='parent_dir'))),
                html.H3([html.Code(os.getcwd(), id='cwd')]),
                        html.Br(), html.Br(),
                html.Div(id='cwd_files',
                        style={'height': 500, 'overflow': 'scroll'}),
            ], lg=10, sm=11, md=10)
        ]),
        dcc.Download(id="download"),
        html.A(
            "Download CSV",
            id="download_csv",
            className="btn btn-outline-secondary btn-sm",
            href="",
            download=""
        )
    ] + [html.Br() for _ in range(15)])

    return app_layout



@app.callback(
    Output('cwd', 'children'),
    Input('stored_cwd', 'data'),
    Input('parent_dir', 'n_clicks'),
    Input('cwd', 'children'),
    prevent_initial_call=True)
def get_parent_directory(stored_cwd, n_clicks, currentdir):
    triggered_id = callback_context.triggered_id
    if triggered_id == 'stored_cwd':
        return stored_cwd
    parent = Path(currentdir).parent.as_posix()
    return parent


@app.callback(
    Output('cwd_files', 'children'),
    Input('cwd', 'children'))
def list_cwd_files(cwd):
    path = Path(cwd)
    all_file_details = []
    if path.is_dir():
        files = sorted(os.listdir(path), key=str.lower)
        for i, file in enumerate(files):
            filepath = Path(file)
            full_path=os.path.join(cwd, filepath.as_posix())
            is_dir = Path(full_path).is_dir()
            link = html.A([
                html.Span(
                file, id={'type': 'listed_file', 'index': i},
                title=full_path,
                style={'fontWeight': 'bold', 'fontSize': 18} if is_dir else {}
            )], href='#')
            details = file_info(Path(full_path))
            details['filename'] = link
            if is_dir:
                details['extension'] = html.Img(
                    src=app.get_asset_url('icons/default_folder.svg'),
                    width=25, height=25)
            else:
                details['extension'] = icon_file(details['extension'][1:])
            all_file_details.append(details)

    df = pd.DataFrame(all_file_details)
    df = df.rename(columns={"extension": ''})
    table = dbc.Table.from_dataframe(df, striped=False, bordered=False,
                                    hover=True, size='sm')
    return html.Div(table)



@app.callback(
    Output('stored_cwd', 'data'),  # note the change here
    Input({'type': 'listed_file', 'index': ALL}, 'n_clicks'),
    State({'type': 'listed_file', 'index': ALL}, 'title'))
def store_clicked_file(n_clicks, title):
    if not n_clicks or set(n_clicks) == {None}:
        raise PreventUpdate
    ctx = callback_context
    index = ctx.triggered_id['index']
    file_path = title[index]
    return file_path  # always returning the file path now



@app.callback(
    Output('download_csv', 'href'),
    Output('download_csv', 'download'),
    Input('stored_cwd', 'data'),
    Input('download_csv', 'n_clicks'),
    prevent_initial_call=True
)
def update_download_link(file_path, n_clicks):
    # when there is no click, do not proceed
    if n_clicks is None:
        raise PreventUpdate
    
    if file_path.endswith(".log"):
        with open(file_path, "r") as f:
            log_content = f.read()
        csv_data = import__(log_content)

        temp_filename = save_file(csv_data)  
        
        # delay and then rename the temp file
        time.sleep(10)
        filename = f'{uuid.uuid1()}.csv'
        os.rename(os.path.join('downloads', temp_filename), os.path.join('downloads', filename))
        
        download_link = f'/download_csv?value={filename}'
        return download_link, filename
    else:
        return "#", ""  

I am using temp_filename because without it files bigger than 1mb does not getting downloaded at all for some reason.

helper functions:

def import__(file_content):
    # Convert the file content string to a StringIO object
    file_io = io.StringIO(file_content)

    # Split the file content into lines
    lines = file_content.splitlines()

    # Search for the header row number
    headerline = 0
    for n, line in enumerate(lines):
        if "Header" in line:
            headerline = n
            break

    # Go back to the start of the StringIO object before reading with pandas
    file_io.seek(0)

    # Read the content using pandas
    # Use the StringIO object (file_io) and set the 'skiprows' parameter
    data = pd.read_csv(file_io, sep='|', header = headerline) # header=None, skiprows=headerline)
    data = data.drop(data.index[-1])
    return data


def save_file(df):
    """Save DataFrame to a .csv file and return the file's name."""
    filename = f'{uuid.uuid1()}.csv'
    filepath = os.path.join('downloads', filename)  # assuming the script has permission to write to this location
    print(f"Saving to {filepath}")
    df.to_csv(filepath, index=False)
    return filename

also Flask API is:

@app.server.route('/download_csv')
def download_csv():
    """Provide the DataFrame for csv download."""
    value = request.args.get('value')
    file_path = os.path.join('downloads', value) # Compute the file path
    df = pd.read_csv(file_path) # Read the CSV data
    csv = df.to_csv(index=False, encoding='utf-8') # Convert DataFrame to CSV

    # Create a string response
    return Response(
        csv,
        mimetype="text/csv",
        headers={"Content-disposition": f"attachment; filename={value}"}
    )

Here are screenshots:

1

1

2

2

3

3

4

4

5

5

I’m not sure why the file ready for download is always one step behind. I put some sort of delay time.sleep(10) to ensure the file write operation is completed before the download begins, but it does not work.

Is there any way I can ensure that the correct file is downloaded on the first button click?

Hi @illuminato ,

Welcome to this forum!

Have you tried dcc.send_data_frame() ? And you don’t actually need to implement the API by yourself.

1 Like