Приложение Dash требует удаления загруженных файлов Excel из папки загрузок.

Вопрос или проблема

Идея заключается в том, что несколько файлов Excel могут быть загружены и отображены, также есть возможность удалить каждый файл из представления и из папки загрузки. Удаление файла не выполняется. Вот код с ошибкой “Имя файла равно None, пропуск удаления”. Кнопка удаления активна и пытается удалить файл, но в конечном итоге не удается.

import os
from dash import dcc, html, dash_table, Input, Output, State, callback, callback_context
from dash.dependencies import ALL
import dash_bootstrap_components as dbc
import base64
import datetime
import io
import pandas as pd

# Импорт функций управления файлами
from constants.index import table_style, cell_style, header_style

from components.image_component import create_image

# Получить иконки загрузки
upload_icon = "assets/images/icons/upload-colored-icon.png"
download_icon = "assets/images/icons/download-icon.png"

# Директория загрузки
upload_directory = "uploaded_files"

# Убедитесь, что директория загрузки существует
if not os.path.exists(upload_directory):
    os.makedirs(upload_directory)

# Определите стили пользовательского загрузчика
loader_style = {
    "position": "fixed",
    "top": "0",
    "left": "0",
    "width": "100%",
    "height": "100%",
    "backgroundColor": "rgba(255, 255, 255, 0.8)",  # Полупрозрачный фон
    "display": "flex",
    "justifyContent": "center",
    "alignItems": "center",
    "zIndex": "9999",  # Убедитесь, что он сверху всего
}


def file_uploader():
    return html.Div(
        [
            dcc.Upload(
                id="upload-data",
                children=dbc.Button(
                    className="upload-button",
                    id="upload-button",
                    children=[
                        create_image(image_path=upload_icon, image_style="icon"),
                        html.Span("Загрузить файлы"),
                    ],
                ),
                multiple=True,
            ),
            dbc.Alert("Загруженные файлы должны соответствовать заданным форматам.", className="upload-alert"),
            dcc.Loading(
                id="loading-spinner",
                type="circle",
                fullscreen=True,
                style=loader_style,
                children=html.Div(id="output-data-upload"),
            ),
        ]
    )


def save_file(contents, filename):
    """Сохранить загруженный файл на сервере."""
    content_type, content_string = contents.split(",")
    decoded = base64.b64decode(content_string)

    # Создать полный путь к файлу
    file_path = os.path.join(upload_directory, filename)

    # Записать файл в зависимости от его типа
    try:
        with open(file_path, "wb") as f:
            f.write(decoded)
    except Exception as e:
        print(f"Ошибка при сохранении файла {filename}: {e}")
        return html.Div([f"Произошла ошибка при сохранении файла: {e}"])

    return file_path  # Вернуть путь к сохранённому файлу


def load_saved_files():
    """Загрузить и отобразить сохранённые файлы с сервера."""
    saved_files = []
    for filename in os.listdir(upload_directory):
        file_path = os.path.join(upload_directory, filename)

        # Отобразить каждый сохранённый файл
        if filename.endswith(".csv"):
            df = pd.read_csv(file_path)
        elif filename.endswith(".xls") or filename.endswith(".xlsx"):
            df = pd.read_excel(file_path)
        else:
            continue

        saved_files.append(
            html.Div(
                [
                    html.Div(
                        className="flex",
                        children=[
                            html.H5(filename),
                            dbc.Button(
                                "Удалить",
                                id={"type": "delete-button", "index": filename},
                                color="danger",
                                n_clicks=0,
                            ),
                        ],
                    ),
                    dash_table.DataTable(
                        df.to_dict("records"), [{"name": i, "id": i} for i in df.columns],
                        page_size=10,
                        style_table=table_style,
                        style_cell=cell_style,
                        style_header=header_style,
                        fixed_rows={"headers": True},
                    ),
                    html.Hr(),
                ]
            )
        )
    return saved_files


def parse_contents(contents, filename, date):
    """Сохранить и разобрать загруженный файл."""
    # Сохраните файл на сервере и получите его путь
    file_path = save_file(contents, filename)

    decoded = base64.b64decode(contents.split(",")[1])
    try:
        if filename.endswith(".csv"):
            df = pd.read_csv(io.StringIO(decoded.decode("utf-8")))
        elif filename.endswith(".xls") or filename.endswith(".xlsx"):
            df = pd.read_excel(io.BytesIO(decoded))
    except Exception as e:
        print(e)
        return html.Div(["Произошла ошибка при обработке этого файла."])

    return html.Div(
        [
            html.Div(
                className="flex",
                children=[
                    html.H5(filename),
                    html.Div(f"Файл {filename} успешно сохранён по адресу {file_path}."),
                ],
            ),
            html.H6(datetime.datetime.fromtimestamp(date)),
            dash_table.DataTable(
                df.to_dict("records"),
                [{"name": i, "id": i} for i in df.columns],
                page_size=10,
                style_table=table_style,
                style_cell=cell_style,
                style_header=header_style,
                fixed_rows={"headers": True},
            ),
            html.Hr(),
            html.Div("Сырые данные"),
            html.Pre(
                contents[0:200] + "...",
                style={"whiteSpace": "pre-wrap", "wordBreak": "break-all"},
            ),
        ]
    )


def delete_file(filename):
    """Удалить выбранный файл из директории загрузки."""
    file_path = os.path.join(upload_directory, filename)
    if os.path.exists(file_path):
        os.remove(file_path)
        print(f"Файл {filename} удалён.")
    else:
        print(f"Файл {filename} не найден.")


@callback(
    Output("output-data-upload", "children"),
    Input("upload-data", "contents"),
    State("upload-data", "filename"),
    State("upload-data", "last_modified"),
    Input({"type": "delete-button", "index": ALL}, "n_clicks"),
    State({"type": "delete-button", "index": ALL}, "index"),
)
def update_output(list_of_contents, list_of_names, list_of_dates, delete_clicks, delete_filenames):
    ctx = callback_context
    triggered = ctx.triggered

    # Обрабатывать удаление файла
    if triggered and "delete-button" in triggered[0]["prop_id"]:
        for i, n_clicks in enumerate(delete_clicks):
            if n_clicks > 0:
                filename_to_delete = delete_filenames[i]

                # Отладка: убедитесь, что имя файла передано
                if filename_to_delete is not None:
                    print(f"Попытка удалить файл: {filename_to_delete}")
                    delete_file(filename_to_delete)
                else:
                    print("Имя файла равно None, пропуск удаления")
                break  # Убедитесь, что мы удаляем только один файл за клик

    # Отобразить сохранённые файлы или вновь загруженные файлы
    if list_of_contents is not None:
        children = [parse_contents(c, n, d) for c, n, d in zip(list_of_contents, list_of_names, list_of_dates)]
        return children
    else:
        return load_saved_files()

Ответ или решение

Для решения проблемы с удалением загруженных файлов Excel из папки загрузок в вашем Dash-приложении, необходимо внести несколько изменений в ваш код, чтобы обеспечить правильное обращение с файлами.

Проблема, с которой вы столкнулись, связана с тем, что переменная filename_to_delete становится None, что приводит к сообщению об ошибке "Filename is None, skipping deletion". Это может произойти, если delete_filenames[i] возвращает некорректное значение.

Шаги для решения проблемы:

  1. Проверьте правильность получения имени файла для удаления.
    Обратите внимание на то, что при обращении к State({"type": "delete-button", "index": ALL}, "index"), возвращается список словарей с именами файлов. Убедитесь, что они правильно извлекаются.

  2. Корректировка функции удаления.
    Убедитесь, что функция delete_file корректно получает имя файла для удаления.

  3. Оптимизация кода.
    Можно упростить логику обработки нажатия кнопки удаления и устранить лишние проверки.

Вот исправленный код подключения для функции удаления:

@callback(
    Output("output-data-upload", "children"),
    Input("upload-data", "contents"),
    State("upload-data", "filename"),
    State("upload-data", "last_modified"),
    Input({"type": "delete-button", "index": ALL}, "n_clicks"),
    State({"type": "delete-button", "index": ALL}, "id"),
)
def update_output(list_of_contents, list_of_names, list_of_dates, delete_clicks, delete_ids):
    ctx = callback_context

    # Используем флаг для отслеживания нажатий кнопок
    if ctx.triggered:
        button_id = ctx.triggered[0]["prop_id"].split('.')[0]
        if "delete-button" in button_id:
            index_to_delete = eval(button_id)["index"]  # Получаем индекс файла для удаления
            if index_to_delete:
                filename_to_delete = index_to_delete
                print(f"Attempting to delete file: {filename_to_delete}")
                delete_file(filename_to_delete)
            else:
                print("Filename is None, skipping deletion")

    # Если содержимое загружено, парсим файлы
    if list_of_contents is not None:
        children = [parse_contents(c, n, d) for c, n, d in zip(list_of_contents, list_of_names, list_of_dates)]
        return children
    else:
        return load_saved_files()

Основные изменения:

  • Мы изменили то, как мы получаем filename_to_delete из delete_ids. Теперь мы напрямую используем eval(button_id) для извлечения имени файла, что должно устранить проблему с None.
  • Обработчик нажатий теперь более очевидный и понятный.

Надеюсь, данные рекомендации помогут вам устранить проблему с удалением файлов. Если возникнут дополнительные вопросы, не стесняйтесь обращаться за помощью.

Оцените материал
Добавить комментарий

Капча загружается...