Werkzeug: Pengecualian SpooledTemporaryFile pada unggahan file

Dibuat pada 18 Agu 2018  ·  13Komentar  ·  Sumber: pallets/werkzeug

Kami mengalami masalah awal tahun ini dengan SpooledTemporaryFile melemparkan pengecualian pada unggahan file setelah melompat dari 0.12.x ke 0.14.x. Tidak punya waktu untuk melihat lebih dalam, tidak dapat menemukan banyak hal secara online, dan tidak ingin merepotkan Anda semua jika itu adalah kesalahan kami atau karena beberapa ketergantungan yang tidak kompatibel. Baru saja disematkan ke 0.12.2, menulis tes kecil yang dapat mendeteksi masalah, dan melanjutkan.

Saya melihat pertanyaan SO untuk flask-admin di kotak masuk saya malam ini yang terdengar mirip dengan apa yang kami lihat, jadi saya pikir itu setidaknya layak untuk diangkat. Pertanyaan SO tidak diutarakan dengan baik, jadi itu mungkin bukan yang paling berguna (https://stackoverflow.com/questions/51858248/attributeerror-spooledtemporaryfile-object-has-no-attribute-translate).

Jika tes membantu:

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.")

Di utas SO saya menandatangani serangkaian masalah/PR github, masalah Python, dan utas SO lain yang semuanya mungkin terkait; menautkan ulang di sini jika itu hilang:

Komentar yang paling membantu

Permintaan maaf untuk kasus reproduksi yang sulit. Saya telah mengkonfirmasi bahwa kami masih melihat masalah ini dan menghabiskan sedikit waktu sore ini untuk mereproduksinya menjadi reproduksi yang cukup singkat.

Dalam prosesnya saya berhasil menemukan solusi potensial. Saya belum menguji solusinya secara ekstensif, jadi saya tidak tahu apakah itu akan menyebabkan masalah sendiri (terutama mengingat laporan oleh @jdalegonzalez bahwa platform mungkin menjadi faktor di sini).

Dependensi:

pip install flask==1.0.2 pytest pytest-flask

Uji:

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"

Semua 13 komentar

Untuk apa nilainya, tampaknya tidak sesederhana versi Werkzeug. Kami menggunakan 0.14.1 dengan python 3.6.6 di Alpine3.6 dan semuanya berfungsi. Ketika kita menggunakan Werkzeug 0.14.1 pada python 3.7.0 dengan Alpine 3.8.1 maka kita melihat masalahnya.

Sepertinya https://github.com/python/cpython/pull/3249 pada akhirnya dapat menghasilkan perbaikan di sisi Python.

HI @abathur - Saya mencoba membuat repo, seperti di bawah ini, dan sepertinya saya tidak dapat mereproduksi kesalahan =

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()

Cuplikan untuk menghasilkan file besar

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

Tidak punya banyak waktu untuk melihat saat ini, tetapi saya juga mengalami kesulitan untuk mereproduksinya sekarang. Saya sedang dalam perjalanan saat ini dan bersiap untuk penerbangan di pagi hari, tetapi saya akan mencoba mengingat untuk melihat apakah saya dapat mereproduksi kondisi awal akhir pekan ini.

@jdalegonzalez Apakah Anda memiliki beberapa catatan tentang masalah Anda yang mungkin membantu mengisi gambar untuk sementara?

Permintaan maaf untuk kasus reproduksi yang sulit. Saya telah mengkonfirmasi bahwa kami masih melihat masalah ini dan menghabiskan sedikit waktu sore ini untuk mereproduksinya menjadi reproduksi yang cukup singkat.

Dalam prosesnya saya berhasil menemukan solusi potensial. Saya belum menguji solusinya secara ekstensif, jadi saya tidak tahu apakah itu akan menyebabkan masalah sendiri (terutama mengingat laporan oleh @jdalegonzalez bahwa platform mungkin menjadi faktor di sini).

Dependensi:

pip install flask==1.0.2 pytest pytest-flask

Uji:

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"

HI @abathur ,

Saya telah melihat sekilas dalam menjalankan repo pytest Anda dan sekilas output di bawah ini terlihat dapat direproduksi dari masalah tersebut. Saya akan mencoba melihat lebih jauh dalam beberapa hari ke depan, tetapi saya seorang pemula dalam hal flask, pytests, dan perlengkapan

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 ...

Mengepung

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

Hai,

Sepertinya pengenalan SpooledTemporaryFile dari PR di bawah ini adalah sumber masalah ini...

PR: #1198: Mendukung penyandian transfer chunked

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

PR: #1222 - Dukungan tambahan untuk platform yang tidak memiliki file temp yang digulung


Mengomentari implementasi SpooledTemporaryFile - tampaknya lulus kode uji 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()

Sepertinya dari tautan @abathur - ada masalah dengan kelas SpooledTemporaryFile karena kehilangan implementasi atribut abstrak "asumsi" IOBase . Juga, ini akan berdampak pada kasus file yang kurang dari 512 kilobyte(?)

Menerapkan patch monyet di bawah ini ke FormDataParser.default_stream_factory tampaknya lulus kode uji 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()

atau mungkin...

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()

Apa yang akan menjadi tindakan terbaik?

  • Menerapkan tambalan monyet untuk SpooledTemporaryFile ? (Mungkin ada cara yang lebih baik untuk melakukan ini...)
  • Hindari implementasi SpooledTemporaryFile , hingga perbaikan dirilis di versi python selanjutnya dan tambahkan pengalihan tergantung pada python ver?
  • Saya masih pemula untuk domain ini, saya kira ada opsi lain? :)

Kami dulu tidak menggunakan SpooledTemporaryFile , tetapi sepertinya cara bawaan untuk melakukan apa yang dilakukan kode asli. Itu menyebabkan banyak masalah sejak itu. Kita mungkin harus kembali untuk tidak menggunakan SpooledTemporaryFile .

Saya perhatikan bahwa bpo-26175 memiliki permintaan tarik yang sedang berlangsung sekarang, setidaknya, lihat python/cpython#3249

Ngobrol dengan @mjpieters memunculkan ide berbeda. Kita dapat mengubah FileStorage.__getattr__ untuk mencoba stream._file jika attr tidak ada pada stream tetapi memiliki atribut _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

Hai, hari ini tingkatkan labu ke 1.0.2 dengan kebutuhan werkzeug >= 0.14

Saya memiliki kesalahan yang sama dengan versi terakhir werkzeug (0.14.1) dengan python 3.6.7 (yang datang dengan ubuntu 18.04):

AttributeError: objek 'SpooledTemporaryFile' tidak memiliki atribut 'dapat dibaca'

Apakah Anda berencana untuk mem-porting perbaikan ke versi 0.14?

Terima kasih.

Versi berikutnya adalah 0.15. Silakan ikuti repo ini, blog kami , atau https://twitter.com/PalletsTeam untuk pengumuman rilis.

Apakah halaman ini membantu?
0 / 5 - 0 peringkat

Masalah terkait

KangOl picture KangOl  ·  16Komentar

caiz picture caiz  ·  3Komentar

lepture picture lepture  ·  6Komentar

taion picture taion  ·  7Komentar

androiddrew picture androiddrew  ·  14Komentar