dash-core-components icon indicating copy to clipboard operation
dash-core-components copied to clipboard

[BUG] dcc.Upload does not support re-upload the same file.

Open Jerry-Ma opened this issue 4 years ago • 13 comments

It appears that if one uses the dcc.Upload.contents property for triggering callback, it fails when the user tries to upload the same file again, due to the content is the same.

I would imagine that a property similar to n_clicks should be provided to trigger callback on each upload event.

Versions:

dash                      1.12.0
dash-core-components      1.10.0
dash-html-components      1.0.3
dash-renderer             1.4.1

Jerry-Ma avatar May 30 '20 01:05 Jerry-Ma

Hm that's odd and not what I would expect. I don't think that we check if content changed or not, and in Dash we don't have any internal checks to "not fire the same callback if the value remains the same". So, perhaps this is something being done by the internal upload component?

chriddyp avatar May 30 '20 01:05 chriddyp

Confirmed by trying out the second example on the docs: https://dash.plotly.com/dash-core-components/upload. Thanks for reporting @Jerry-Ma !

chriddyp avatar May 30 '20 01:05 chriddyp

If anyone in the community wants to dig into this:

  • Here is the Upload.react.js code: https://github.com/plotly/dash-core-components/blob/dev/src/fragments/Upload.react.js#L1-L94
  • Here is the underlying React component we're using: https://github.com/react-dropzone/react-dropzone

chriddyp avatar Jun 03 '20 18:06 chriddyp

This answer in a stack overflow thread about uploading the same file twice (albeit not in dropstuff) and this answer in a github issue describing a similar problem in read-file-reader-input both point to a similar problem, that the browser only fires a callback (change, in these cases, but it feels similar) when something actually changes.

From the browser's point of view, when one drags in the same file nothing has changed. They both advocate setting .value = ''.

I wonder if setting this.value = '' after line 44 in Upload.react.js would solve the problem. I'm don't have a JS environment set up (I'm not a JS programmer) so I can't easily try it.

Perhaps someone can give it a test (or something similar)?

hartzell avatar Jun 11 '20 14:06 hartzell

The react-dropzone folks refer questions to Stackoverflow rather than their Issues.

I've posted a question here.

hartzell avatar Jun 14 '20 16:06 hartzell

First time posting on an issue. A big thank you for this entire framework!

Seems like https://github.com/plotly/dash-core-components/pull/859 would fix this bug. Could this or any equivalent solution be considered and merged?

Awe42 avatar Jul 26 '21 09:07 Awe42

Is there anything wrong with the solution provided in #859 ? Would be nice to see it merged (or reviewed).

noxthot avatar Jan 13 '22 14:01 noxthot

@noxthot I'm not really sure why the contributor of #859 closed that PR, it does seem like that solution would work though requiring users to add an extra input upload_timestamp to their callbacks just to cover this edge case is a little awkward, I'd rather we try and find a solution that doesn't require that. Regardless, at this point the PR would need to be recreated in the main dash repo as the dash-core-components repo is no longer used.

alexcjohnson avatar Jan 17 '22 15:01 alexcjohnson

In the meantime, a workaround is to simply use a callback to replace the upload component after reading its contents.

JonThom avatar Feb 08 '22 13:02 JonThom

@JonThom You mean to pass a new dcc.Upload (with the same ID) component to a container div around it every time it is used? That seems very unelegenat :D No updates on this?

luggie avatar Jun 24 '23 11:06 luggie

@luggie yes that's been my workaround. Haven't really been following this since Feb 2022 so don't know if there are better solutions now.

JonThom avatar Jun 25 '23 09:06 JonThom

@JonThom @luggie A simpler workaround that's worked for me is to set the contents of the dcc.Upload component to None after processing the contents.

Here's as minimal an example I could come up with which demonstrates the workaround. You can upload a file, then click the 'delete all' button, then re-upload the same file again without a problem. You can also upload the same file twice in a row to get two copies of it in the dropdown list.

from dash import Dash, html, dcc, Input, Output, State, callback

app = Dash(__name__)
app.layout = html.Div([
    dcc.Store(id='session', storage_type='memory', data={'filenames': []}),
    dcc.Upload([html.Div('Drag and drop file or click to Upload...')], id='file-picker', multiple=True),
    dcc.Dropdown(id='file-dropdown', className='file-dropdown'),
    html.Button('delete all', id='delete-all')
])


@callback(
    [Output('file-dropdown', 'options'),
     Output('file-dropdown', 'value'),
     Output('session', 'data'),
     Output('file-picker', 'contents')],  # workaround for issue 816
    Input('file-picker', 'contents'),
    State('file-picker', 'filename'),
    State('session', 'data'),
    prevent_initial_call=True
)
def upload_file(file_contents_list, filename_list, session_data):
    for file_contents, filename in zip(file_contents_list, filename_list):
        session_data = process_file(file_contents, filename, session_data)
    filenames = session_data['filenames']
    currently_selected_file = filenames[0] if filenames[0] else None
    return filenames, currently_selected_file, session_data, None  # Always return None for the file-picker contents


@callback(
    [Output('file-dropdown', 'options', allow_duplicate=True),
     Output('file-dropdown', 'value', allow_duplicate=True),
     Output('session', 'data', allow_duplicate=True)],
    Input('delete-all', 'n_clicks'),
    prevent_initial_call=True
)
def delete_all_uploads(_):
    return [], None, {'filenames': []}


def process_file(file_contents, filename, session_data):
    # Do whatever you want with file_contents and filename in here. In your application, you'd probably want to do 
    # something more sophisticated. Here, I'm just adding the filename to a list in a dcc.Store component for 
    # demonstration purposes.
    session_data['filenames'].append(filename)
    return session_data


app.run(debug=True)

hydrusbeta avatar Jan 28 '24 00:01 hydrusbeta

Great @hydrusbeta, that's worked fine, thanks!!

ricardomgi avatar Mar 08 '24 17:03 ricardomgi