Werkzeug: Exceptions SpooledTemporaryFile lors du téléchargement de fichiers

Créé le 18 août 2018  ·  13Commentaires  ·  Source: pallets/werkzeug

Nous avons rencontré un problème plus tôt cette année avec SpooledTemporaryFile lançant des exceptions sur les téléchargements de fichiers après être passé de 0.12.x à 0.14.x. Je n'ai pas eu le temps de regarder en profondeur, je n'ai pas trouvé grand-chose en ligne et je ne voulais pas vous faire suer si c'était de notre faute ou si cela se résumait à des dépendances incompatibles. Je viens d'épingler à 0.12.2, j'ai écrit un petit test qui pourrait détecter le problème, et je suis passé à autre chose.

J'ai vu une question SO pour flask-admin dans ma boîte de réception ce soir qui ressemblait à ce que nous avons vu, alors j'ai pensé que cela valait au moins la peine d'être soulevé. La question SO n'est pas très bien formulée, elle n'est donc peut-être pas la plus utile (https://stackoverflow.com/questions/51858248/attributeerror-spooledtemporaryfile-object-has-no-attribute-translate).

Si le test aide :

def test_werkzeug_spooled_temp_file(self):
    """
    When we jumped from Werkzeug 0.12.x to 0.14.x, we started running into an error on any attempt to upload a CSV:

    File "/home/vagrant/site/videohub/admin.py", line 728, in import_content
    for line in csv.DictReader(io.TextIOWrapper(new_file), restval=None):
    File "/usr/local/lib/python3.6/dist-packages/werkzeug/datastructures.py", line 2745, in __getattr__
    return getattr(self.stream, name)
    AttributeError: 'SpooledTemporaryFile' object has no attribute 'readable'

    Until/unless we debug this, we need to stay on Werkzeug versions where it doesn't break.
    """

    with self.client:
        from videohub import admin

        admin.auth_admin = lambda: True

        try:
            what = self.client.post(
                "/admin/importcontent/",
                data={"csv": (io.BytesIO(b"my file contents"), "test.csv")},
                content_type="multipart/form-data",
            )
        except AttributeError as e:
            if (
                str(e)
                == "'SpooledTemporaryFile' object has no attribute 'readable'"
            ):
                self.fail("Werkzeug/SpooledTemporaryFile still exists.")

Dans le fil SO, j'ai encré un ensemble de problèmes/PR github, des problèmes Python et un autre fil SO qui peuvent tous être liés ; re-lien ici au cas où cela disparaît:

Commentaire le plus utile

Toutes mes excuses pour le cas difficile de reproduction. J'ai confirmé que nous voyons toujours le problème et avons passé un peu de temps cet après-midi à le réduire à une reproduction raisonnablement concise.

Dans le processus, j'ai réussi à repérer une solution de contournement potentielle. Je n'ai pas testé la solution de contournement de manière approfondie, donc je ne sais pas si cela causera ses propres problèmes (en particulier compte tenu du rapport de @jdalegonzalez selon lequel la plate-forme peut être un facteur ici).

Dépendances :

pip install flask==1.0.2 pytest pytest-flask

Test:

import pytest, io, csv
from flask import Flask, request


def fields(ob):
    return csv.DictReader(ob).fieldnames[0]


@pytest.fixture(scope="session")
def app():
    x = Flask(__name__)
    x.testing = True

    @x.route("/textio", methods=["POST"])
    def upload_file_textio():
        """succeeds with 0.12.2; fails with werkzeug==0.14.1;"""
        return fields(io.TextIOWrapper(request.files["csv"]))

    @x.route("/stringio", methods=["POST"])
    def upload_file_stringio():
        """potential workaround; succeeds for both versions"""
        return fields(io.StringIO(request.files["csv"].read().decode()))

    with x.app_context():
        yield x


@pytest.mark.parametrize("uri", ["/textio", "/stringio"])
def test_werkzeug_spooled_temp_file(client, uri):
    what = client.post(
        uri,
        data={"csv": (io.BytesIO(b"my file contents"), "test.csv")},
        content_type="multipart/form-data",
    )
    assert what.data == b"my file contents"

Tous les 13 commentaires

Pour ce que ça vaut, ça ne semble pas aussi simple que la version de Werkzeug. Nous utilisons 0.14.1 avec python 3.6.6 sur Alpine3.6 et tout fonctionne. Lorsque nous utilisons Werkzeug 0.14.1 sur python 3.7.0 avec Alpine 3.8.1, nous voyons le problème.

Il semble que https://github.com/python/cpython/pull/3249 puisse éventuellement produire un correctif du côté Python.

HI @abathur - J'ai essayé de créer un repo, comme ci-dessous, et je n'arrive pas à reproduire l'erreur =

app.py

import os
from flask import Flask, flash, request, redirect, url_for
from werkzeug.utils import secure_filename


UPLOAD_FOLDER = './uploads'
ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'])

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER


def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


@app.route('/', methods=['GET', 'POST'])
def upload_file():

    if request.method == 'POST':
        # check if the post request has the file part
        if 'file' not in request.files:
            flash('No file part')
            return redirect(request.url)

        file = request.files['file']

        # if user does not select file, browser also
        # submit an empty part without filename
        if file.filename == '':
            flash('No selected file')
            return redirect(request.url)

        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return redirect(url_for('uploaded_file', filename=filename))


    return '''
    <!doctype html>
    <title>Upload new File</title>
    <h1>Upload new File</h1>
    <form method=post enctype=multipart/form-data>
      <input type=file name=file>
      <input type=submit value=Upload>
    </form>
    '''


from flask import send_from_directory

@app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)


if __name__ == '__main__':
    app.run()

Extrait pour générer un gros fichier

dd if=/dev/zero of=filename bs=1024 count=2M

Ubuntu - 18.10
Python -  3.6.7rc1

> pip freeze
Click==7.0
Flask==1.0.2
itsdangerous==1.1.0
Jinja2==2.10
MarkupSafe==1.1.0
Werkzeug==0.14.1

/cc @davidisme

Je n'ai pas beaucoup de temps pour regarder pour le moment, mais j'ai eu du mal à le reproduire tout à l'heure aussi. Je suis sur la route en ce moment et je prépare un vol dans la matinée, mais j'essaierai de me souvenir pour voir si je peux reproduire les conditions initiales plus tard cette semaine.

@jdalegonzalez Avez-vous des notes sur votre rencontre avec ce problème qui pourraient aider à compléter le tableau entre-temps ?

Toutes mes excuses pour le cas difficile de reproduction. J'ai confirmé que nous voyons toujours le problème et avons passé un peu de temps cet après-midi à le réduire à une reproduction raisonnablement concise.

Dans le processus, j'ai réussi à repérer une solution de contournement potentielle. Je n'ai pas testé la solution de contournement de manière approfondie, donc je ne sais pas si cela causera ses propres problèmes (en particulier compte tenu du rapport de @jdalegonzalez selon lequel la plate-forme peut être un facteur ici).

Dépendances :

pip install flask==1.0.2 pytest pytest-flask

Test:

import pytest, io, csv
from flask import Flask, request


def fields(ob):
    return csv.DictReader(ob).fieldnames[0]


@pytest.fixture(scope="session")
def app():
    x = Flask(__name__)
    x.testing = True

    @x.route("/textio", methods=["POST"])
    def upload_file_textio():
        """succeeds with 0.12.2; fails with werkzeug==0.14.1;"""
        return fields(io.TextIOWrapper(request.files["csv"]))

    @x.route("/stringio", methods=["POST"])
    def upload_file_stringio():
        """potential workaround; succeeds for both versions"""
        return fields(io.StringIO(request.files["csv"].read().decode()))

    with x.app_context():
        yield x


@pytest.mark.parametrize("uri", ["/textio", "/stringio"])
def test_werkzeug_spooled_temp_file(client, uri):
    what = client.post(
        uri,
        data={"csv": (io.BytesIO(b"my file contents"), "test.csv")},
        content_type="multipart/form-data",
    )
    assert what.data == b"my file contents"

Salut @abathur ,

J'ai jeté un coup d'œil sur l'exécution de votre référentiel pytest et en un coup d'œil, la sortie ci-dessous semble être reproductible du problème. J'essaierai d'y jeter un coup d'œil dans les prochains jours, mais je suis novice en matière de flask, pytests et fixtures 😃

Testing started at 20:35 ...
/Users/jayCee/PycharmProjects/test_werkzeug_spooled_temp_file/venv/bin/python "/Users/jayCee/Library/Application Support/JetBrains/Toolbox/apps/PyCharm-P/ch-1/182.4505.26/PyCharm.app/Contents/helpers/pycharm/_jb_pytest_runner.py" --target test_werkzeug_spooled_temp.py::test_werkzeug_spooled_temp_file
Launching pytest with arguments test_werkzeug_spooled_temp.py::test_werkzeug_spooled_temp_file in /Users/jayCee/PycharmProjects/test_werkzeug_spooled_temp_file/test

============================= test session starts ==============================
platform darwin -- Python 3.6.6, pytest-4.0.0, py-1.7.0, pluggy-0.8.0
rootdir: /Users/jayCee/PycharmProjects/test_werkzeug_spooled_temp_file/test, inifile:
plugins: flask-0.14.0collected 2 items

test_werkzeug_spooled_temp.py F
test_werkzeug_spooled_temp.py:27 (test_werkzeug_spooled_temp_file[/textio])
client = <FlaskClient <Flask 'test_werkzeug_spooled_temp'>>, uri = '/textio'

    @pytest.mark.parametrize("uri", ["/textio", "/stringio"])
    def test_werkzeug_spooled_temp_file(client, uri):

        what = client.post(
            uri,
            data={"csv": (io.BytesIO(b"my file contents"), "test.csv")},
>           content_type="multipart/form-data",
        )

test_werkzeug_spooled_temp.py:34: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../venv/lib/python3.6/site-packages/werkzeug/test.py:840: in post
    return self.open(*args, **kw)
../venv/lib/python3.6/site-packages/flask/testing.py:200: in open
    follow_redirects=follow_redirects
../venv/lib/python3.6/site-packages/werkzeug/test.py:803: in open
    response = self.run_wsgi_app(environ, buffered=buffered)
../venv/lib/python3.6/site-packages/werkzeug/test.py:716: in run_wsgi_app
    rv = run_wsgi_app(self.application, environ, buffered=buffered)
../venv/lib/python3.6/site-packages/werkzeug/test.py:923: in run_wsgi_app
    app_rv = app(environ, start_response)
../venv/lib/python3.6/site-packages/flask/app.py:2309: in __call__
    return self.wsgi_app(environ, start_response)
../venv/lib/python3.6/site-packages/flask/app.py:2295: in wsgi_app
    response = self.handle_exception(e)
../venv/lib/python3.6/site-packages/flask/app.py:1741: in handle_exception
    reraise(exc_type, exc_value, tb)
../venv/lib/python3.6/site-packages/flask/_compat.py:35: in reraise
    raise value
../venv/lib/python3.6/site-packages/flask/app.py:2292: in wsgi_app
    response = self.full_dispatch_request()
../venv/lib/python3.6/site-packages/flask/app.py:1815: in full_dispatch_request
    rv = self.handle_user_exception(e)
../venv/lib/python3.6/site-packages/flask/app.py:1718: in handle_user_exception
    reraise(exc_type, exc_value, tb)
../venv/lib/python3.6/site-packages/flask/_compat.py:35: in reraise
    raise value
../venv/lib/python3.6/site-packages/flask/app.py:1813: in full_dispatch_request
    rv = self.dispatch_request()
../venv/lib/python3.6/site-packages/flask/app.py:1799: in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
test_werkzeug_spooled_temp.py:17: in upload_file_textio
    return fields(io.TextIOWrapper(request.files["csv"]))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <FileStorage: 'test.csv' ('text/csv')>, name = 'readable'

    def __getattr__(self, name):
>       return getattr(self.stream, name)
E       AttributeError: 'SpooledTemporaryFile' object has no attribute 'readable'

../venv/lib/python3.6/site-packages/werkzeug/datastructures.py:2745: AttributeError
.                                         [100%]
=================================== FAILURES ===================================
___________________ test_werkzeug_spooled_temp_file[/textio] ___________________
...
# truncated duplicate above output ...

Environ

macOS Mojave 10.14
Python 3.6.6

pip freeze
atomicwrites==1.2.1
attrs==18.2.0
Click==7.0
Flask==1.0.2
itsdangerous==1.1.0
Jinja2==2.10
MarkupSafe==1.1.0
more-itertools==4.3.0
pluggy==0.8.0
py==1.7.0
pytest==4.0.0
pytest-flask==0.14.0
six==1.11.0
Werkzeug==0.14.1

Salut,

Il semble que l'introduction de SpooledTemporaryFile dans les relations publiques ci-dessous soit la source de ce problème...

PR : #1198 : Prise en charge de l'encodage de transfert fragmenté

  • Introduction de SpooledTemporaryFile - https://github.com/pallets/werkzeug/pull/1198/files#diff-91178c333961d75c285b7784d1b81cecR40)

PR : #1222 - Ajout de la prise en charge des plates-formes dépourvues de fichiers temporaires spoolés


Commentant l'implémentation de SpooledTemporaryFile - semble réussir le code de test https://github.com/pallets/werkzeug/issues/1344#issuecomment -438836862

def default_stream_factory(total_content_length, filename, content_type,
                           content_length=None):
    """The stream factory that is used per default."""
    max_size = 1024 * 500
#    if SpooledTemporaryFile is not None:
#        return SpooledTemporaryFile(max_size=max_size, mode='wb+')
    if total_content_length is None or total_content_length > max_size:
        return TemporaryFile('wb+')
return BytesIO()

Cela ressemble à des liens @abathur - il y a un problème avec la classe SpooledTemporaryFile car il manque l'implémentation des attributs abstraits "supposés" de IOBase . En outre, cela aurait un impact sur les cas de fichiers inférieurs à 512 kilo-octets (?)

L'application du correctif de singe ci-dessous à FormDataParser.default_stream_factory semble réussir le code de test https://github.com/pallets/werkzeug/issues/1344#issuecomment -438836862 :

def default_stream_factory(total_content_length, filename, content_type,
                           content_length=None):
    """The stream factory that is used per default."""
    max_size = 1024 * 500
    if SpooledTemporaryFile is not None:
        monkeypatch_SpooledTemporaryFile = SpooledTemporaryFile(max_size=max_size, mode='wb+')
        monkeypatch_SpooledTemporaryFile.readable = monkeypatch_SpooledTemporaryFile._file.readable
        monkeypatch_SpooledTemporaryFile.writable = monkeypatch_SpooledTemporaryFile._file.writable
        monkeypatch_SpooledTemporaryFile.seekable = monkeypatch_SpooledTemporaryFile._file.seekable
        return monkeypatch_SpooledTemporaryFile
    if total_content_length is None or total_content_length > max_size:
        return TemporaryFile('wb+')
    return BytesIO()

ou peut-être...

class SpooledTemporaryFile_Patched(SpooledTemporaryFile): 
    """Patch for `SpooledTemporaryFile exceptions on file upload #1344`
      - SpooledTemporaryFile does not fully satisfy the abstract for IOBase - https://bugs.python.org/issue26175 
      - bpo-26175: Fix SpooledTemporaryFile IOBase abstract - https://github.com/python/cpython/pull/3249

     TODO: Remove patch once `bpo-26175: Fix SpooledTemporaryFile IOBase abstract` is resolved..."""

    def readable(self):
        return self._file.readable

    def writable(self):
        return self._file.writable

    def seekable(self):
        return self._file.seekable

...

def default_stream_factory(total_content_length, filename, content_type, content_length=None):
    """The stream factory that is used per default.

    Patch: `SpooledTemporaryFile exceptions on file upload #1344`, Remove once `bpo-26175: Fix SpooledTemporaryFile IOBase abstract` is resolved...
      - SpooledTemporaryFile does not fully satisfy the abstract for IOBase - https://bugs.python.org/issue26175 
      - bpo-26175: Fix SpooledTemporaryFile IOBase abstract - https://github.com/python/cpython/pull/3249"""
    max_size = 1024 * 500
    if SpooledTemporaryFile is not None:
        # TODO: Remove patch once `bpo-26175: Fix SpooledTemporaryFile IOBase abstract` is resolved...
        SpooledTemporaryFile = SpooledTemporaryFile_Patched
        return SpooledTemporaryFile_Patched(max_size=max_size, mode='wb+')
    if total_content_length is None or total_content_length > max_size:
        return TemporaryFile('wb+')
    return BytesIO()

Quelle serait la meilleure marche à suivre ?

  • Implémenter un correctif de singe pour SpooledTemporaryFile ? (Il y a probablement une meilleure façon de faire cela...)
  • Évitez l'implémentation de SpooledTemporaryFile , jusqu'à ce qu'un correctif soit publié dans une version ultérieure de python et ajoutez une redirection en fonction de la version python ?
  • Je suis encore novice dans ce domaine, je suppose qu'il existe d'autres options ? :)

Auparavant, nous n'utilisions pas SpooledTemporaryFile , mais cela semblait être un moyen intégré de faire ce que faisait le code d'origine. Cela a causé beaucoup de problèmes depuis. Nous devrions probablement recommencer à ne plus utiliser SpooledTemporaryFile .

😞 Je note que bpo-26175 a une pull request en cours maintenant, au moins, voir python/cpython#3249

Discuter avec @mjpieters a soulevé une idée différente. Nous pourrions changer FileStorage.__getattr__ pour essayer stream._file si l'attribut n'existe pas sur stream mais il a un attribut _file .

class FileStorage:
    def __getattr__(self, name):
        try:
            return getattr(self.stream, name)
        except AttributeError:
            if hasattr(self.stream, "_file"):
                return getattr(self.stream._file, name)
            raise

Bonjour, aujourd'hui, mettez à niveau Flask vers la version 1.0.2 avec besoin de werkzeug >= 0,14

J'ai la même erreur avec la dernière version de werkzeug (0.14.1) avec python 3.6.7 (fournie avec ubuntu 18.04):

AttributeError : l'objet 'SpooledTemporaryFile' n'a pas d'attribut 'lisible'

Prévoyez-vous de porter le correctif vers la version 0.14 ?

Merci.

La prochaine version sera la 0.15. Veuillez suivre ce dépôt, notre blog ou https://twitter.com/PalletsTeam pour les annonces de sortie.

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

c17r picture c17r  ·  4Commentaires

davidism picture davidism  ·  9Commentaires

ngaya-ll picture ngaya-ll  ·  8Commentaires

masklinn picture masklinn  ·  11Commentaires

KangOl picture KangOl  ·  16Commentaires