Как очистить временный файл, используемый с send_file?

В настоящее время я разрабатываю интерфейс json на стороне сервера, в котором несколько временных файлов манипулируют во время запросов.

Мое текущее решение для очистки этих файлов в конце запроса выглядит так:

@app.route("/method",methods=['POST'])
def api_entry():
    with ObjectThatCreatesTemporaryFiles() as object:
        object.createTemporaryFiles()
        return "blabalbal"

В этом случае очистка выполняется в объекте.__exit__()

Однако в некоторых случаях мне нужно вернуть временные файлы клиенту, и в этом случае код выглядит так:

@app.route("/method",methods=['POST'])
def api_entry():
    with ObjectThatCreatesTemporaryFiles() as object:
        object.createTemporaryFiles()
        return send_file(object.somePath)

В настоящее время это не работает, потому что, когда происходит очистка, фляга находится в процессе чтения файла и отправки его клиенту. ¨ Как я могу это решить?

Редактировать: я забыл упомянуть, что файлы находятся во временных каталогах.


person monoceres    schedule 12.11.2012    source источник


Ответы (5)


Метод, который я использовал, заключается в использовании слабых ссылок для удаления файла после завершения ответа.

import shutil
import tempfile
import weakref

class FileRemover(object):
    def __init__(self):
        self.weak_references = dict()  # weak_ref -> filepath to remove

    def cleanup_once_done(self, response, filepath):
        wr = weakref.ref(response, self._do_cleanup)
        self.weak_references[wr] = filepath

    def _do_cleanup(self, wr):
        filepath = self.weak_references[wr]
        print('Deleting %s' % filepath)
        shutil.rmtree(filepath, ignore_errors=True)

file_remover = FileRemover()

И в вызове фляги у меня было:

@app.route('/method')
def get_some_data_as_a_file():
    tempdir = tempfile.mkdtemp()
    filepath = make_the_data(dir_to_put_file_in=tempdir)
    resp = send_file(filepath)
    file_remover.cleanup_once_done(resp, tempdir)
    return resp

Это довольно общий подход, и этот подход работал в трех разных веб-фреймворках Python, которые я использовал.

person Rasjid Wilcox    schedule 21.08.2015
comment
Это решение было полезным. У меня есть ситуация, когда мой код создает три файла. Если пользователь выбирает загрузку одного файла, остальные удаляются. Выбранный файл остается, но я думаю, что это потому, что я в среде Windows. Спасибо за это. - person mattrweaver; 07.02.2018
comment
Это работает, и основная функция основана на weakref, для тех, кто не знаком с weakref документами. python.org/3/library/weakref.html содержит некоторые пояснения. - person Shihe Zhang; 29.10.2018
comment
просто resp = send_file(filepath), затем os.reomve(filepath) и return resp этих трех строк было достаточно, мне не нужно было использовать класс FileRemover или что-то еще! Спасибо - person Peko Chan; 29.01.2020
comment
@PekoChan Это будет работать в Linux и (возможно) Mac OS, но не в Windows. В Linux вы можете удалить открытый файл. В винде нельзя. - person Rasjid Wilcox; 30.01.2020
comment
Работает как шарм! - person SiboVG; 08.04.2020
comment
Я пробовал это в Windows 2021 - 07 -01, и это не работает. Это дает мне ошибку разрешения, потому что файл используется флягой, из-за того, что он поставил ignore_errors = True, исключение не будет выдано, если rmtree не сможет удалить файл / каталог - person Ángel; 01.07.2021
comment
@ Ángel За последние несколько лет я замечал сбои в последних версиях Windows в других ситуациях. Удаление ignore_errors и повторная попытка несколько раз при сбое, похоже, решают эту проблему. Вероятно, это сработает и в этом случае. - person Rasjid Wilcox; 02.07.2021
comment
@RasjidWilcox Я бы предпочел сначала сделать это нормально, см. Мой последний пост, я думаю, что это единственный способ, который совместим со всеми системами. Но имеет компромисс, потому что файл должен быть в памяти - person Ángel; 02.07.2021

Если вы используете Flask 0.9 или выше, вы можете использовать декоратор after_this_request:

@app.route("/method",methods=['POST'])
def api_entry():
    tempcreator = ObjectThatCreatesTemporaryFiles():
    tempcreator.createTemporaryFiles()

    @after_this_request
    def cleanup(response):
        tempcreator.__exit__()
        return response

    return send_file(tempcreator.somePath)

ИЗМЕНИТЬ

Поскольку это не работает, вы можете попробовать вместо этого использовать cStringIO. (это предполагает, что ваши файлы достаточно малы, чтобы поместиться в памяти):

@app.route("/method", methods=["POST"])
def api_entry():
    file_data = dataObject.createFileData()
    # Simplest `createFileData` method:  
    # return cStringIO.StringIO("some\ndata")
    return send_file(file_data,
                        as_attachment=True,
                        mimetype="text/plain",
                        attachment_filename="somefile.txt")

В качестве альтернативы вы можете создать временные файлы, как делаете сейчас, но не удалять их приложением. Вместо этого настройте задание cron (или запланированное задание, если вы работаете в Windows), чтобы оно запускалось каждый час или около того и удаляло файлы во временном каталоге, которые были созданы более получаса назад.

person Sean Vieira    schedule 12.11.2012
comment
Мой ответ кажется грязным взломом рядом с этим. - person madjar; 12.11.2012
comment
К сожалению, похоже, что во фляге файл все еще открыт во время вызова @after_this_request :( - person monoceres; 12.11.2012
comment
@monoceres :-( Это плохо - я обновил свой ответ некоторыми дополнительными предложениями. - person Sean Vieira; 13.11.2012

У меня есть два решения.


Первое решение — удалить файл в методе __exit__, но не закрывать его. Таким образом, файл-объект по-прежнему доступен, и вы можете передать его send_file.

Это будет работать только в том случае, если вы не используете X-Sendfile, потому что оно использует имя файла.


Второе решение — полагаться на сборщик мусора. Вы можете передать send_file файл-объект, который будет очищать файл при удалении (метод __del__). Таким образом, файл удаляется только тогда, когда файл-объект удаляется из python. Для этого вы можете использовать TemporaryFile, если вы еще этого не сделали. .

person madjar    schedule 12.11.2012
comment
Интересно, я рассмотрю эти параметры, а также посмотрю мое редактирование, касающееся каталогов. - person monoceres; 12.11.2012
comment
Первое решение все еще работает, потому что оно основано на том факте, что (по крайней мере, в Linux) открытый файл все еще можно прочитать после того, как файл удален (отключен). Второй не будет, если объект, который выполняет очистку, является файловым объектом. - person madjar; 12.11.2012

Это немного поздно, но это то, что я сделал, используя предложения madjar (на случай, если кто-то еще столкнется с этим). Это небольшая вспомогательная функция, которую я использую (в качестве параметра она принимает объект PyExcelerate Workbook), которую вы можете адаптировать к своему случаю. Просто измените способ создания/сборки вашего tempfile.TemporaryFile, и все готово! Протестировано на Windows 8.1 и Ubuntu 12.04.

def xlsx_to_response(wb, filename):
    f = tempfile.TemporaryFile()
    wb._save(f)
    f.seek(0)
    response = send_file(f, as_attachment=True, attachment_filename=filename,
                         add_etags=False)

    f.seek(0, os.SEEK_END)
    size = f.tell()
    f.seek(0)
    response.headers.extend({
        'Content-Length': size,
        'Cache-Control': 'no-cache'
    })
    return response
person rhyek    schedule 23.04.2014
comment
Большое спасибо за это. Я сделал все, что мог, но это не сработало. Когда я увидел ваш ответ, я понял, что забыл f.seek(0) - person ghirlekar; 28.06.2018

Совместимое с Windows/Linux/Mac решение

Я пробовал слабые ссылки, встроенные декораторы фляги, и ничего не получалось.

Единственное, что работало в каждой системе, — это создать временный файл в памяти с помощью io.BytesIO.

import os
import io
import tempfile
from multiprocessing import Process

import flask


def background_job(callback):
    task = Process(target=callback())
    task.start()

def send_temp_file(file_path: str, temp_dir: tempfile.TemporaryDirectory, remove_dir_after_send=True):
    with open(file_path, "rb") as f:
        content = io.BytesIO(f.read())
    response = flask.send_file(content,
                               as_attachment=True,
                               attachment_filename=os.path.split(file_path)[0])
    if remove_dir_after_send:
        background_job(temp_dir.cleanup)
    return response



app = flask.Flask(__name__)


@app.route("/serve_file/", methods=["GET"])
def serve_file():
    temp_dir = tempfile.TemporaryDirectory()
    file_path = os.path.join(temp_dir.name, "test.txt")
    with open(file_path, "w") as f:
        f.write("Hello World!")

    return send_temp_file(file_path, temp_dir)


if __name__ == "__main__":
    app.run(port=1337)
person Ángel    schedule 01.07.2021