Werkzeug: استثناءات SpooledTarilyFile عند تحميل الملف

تم إنشاؤها على ١٨ أغسطس ٢٠١٨  ·  13تعليقات  ·  مصدر: pallets/werkzeug

واجهتنا مشكلة في وقت سابق من هذا العام مع SpooledTporaryFile بإلقاء استثناءات على عمليات تحميل الملفات بعد القفز من 0.12.x إلى 0.14.x. لم يكن لديك الوقت لإلقاء نظرة متعمقة ، ولم أستطع العثور على الكثير عبر الإنترنت ، ولم أرغب في إجهادك جميعًا إذا كان خطأنا أو نزلنا إلى بعض التبعيات غير المتوافقة. تم التثبيت للتو على 0.12.2 ، وكتب اختبارًا صغيرًا يمكنه اكتشاف المشكلة ، والمضي قدمًا.

لقد رأيت سؤالًا خاصًا بـ flask-admin في صندوق الوارد الخاص بي الليلة بدا مشابهًا لما رأيناه ، لذلك اعتقدت أنه على الأقل يستحق التحدث. سؤال SO ليس مصاغًا جيدًا ، لذلك قد لا يكون الأكثر استخدامًا (https://stackoverflow.com/questions/51858248/attributeerror-spooledtertainfile-object-has-no-attribute-translate).

في حال كان الاختبار يساعد:

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

في خيط SO ، قمت بتوقيع مجموعة من مشكلات github / العلاقات العامة ، ومشكلات Python ، وخيط SO آخر قد يكون جميعها ذات صلة ؛ إعادة الربط هنا في حالة اختفاء ذلك:

التعليق الأكثر فائدة

الاعتذار عن حالة التكاثر الصعبة. لقد أكدت أننا ما زلنا نرى المشكلة وأمضينا بعض الوقت بعد ظهر هذا اليوم في تقليصها إلى إعادة إنتاج مقتضبة بشكل معقول.

في هذه العملية ، تمكنت من اكتشاف حل بديل محتمل. لم أختبر الحل البديل على نطاق واسع ، لذلك لا أعرف ما إذا كان سيتسبب في مشاكل من تلقاء نفسه (خاصة بالنظر إلى تقرير jdalegonzalez أن النظام الأساسي قد يكون عاملاً هنا).

التبعيات:

pip install flask==1.0.2 pytest pytest-flask

اختبار:

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"

ال 13 كومينتر

لما يستحق الأمر ، لا يبدو أنه بسيط مثل إصدار Werkzeug. نحن نستخدم 0.14.1 مع python 3.6.6 على Alpine3.6 وكل شيء يعمل. عندما نستخدم Werkzeug 0.14.1 على python 3.7.0 مع Alpine 3.8.1 فإننا نرى المشكلة.

يبدو أن https://github.com/python/cpython/pull/3249 قد يؤدي في النهاية إلى إصلاح جانب Python.

مرحبًا abathur - لقد حاولت إنشاء الريبو ، وفقًا لما هو مذكور أدناه ، ولم أستطع إعادة إنتاج الخطأ =

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

مقتطف لإنشاء ملف كبير

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

/ ccdavidism

ليس لدي الكثير من الوقت للنظر في هذه اللحظة ، لكنني واجهت مشكلة في إعادة إنتاجها الآن أيضًا. أنا على الطريق في الوقت الحالي وأستعد لرحلة في الصباح ، لكنني أحاول أن أتذكر معرفة ما إذا كان بإمكاني إعادة إنتاج الظروف الأولية في وقت لاحق من هذا الأسبوع.

jdalegonzalez هل لديك بعض الملاحظات حول مشاركتك مع هذه المشكلة والتي قد تساعد في ملء الصورة في هذه الأثناء؟

الاعتذار عن حالة التكاثر الصعبة. لقد أكدت أننا ما زلنا نرى المشكلة وأمضينا بعض الوقت بعد ظهر هذا اليوم في تقليصها إلى إعادة إنتاج مقتضبة بشكل معقول.

في هذه العملية ، تمكنت من اكتشاف حل بديل محتمل. لم أختبر الحل البديل على نطاق واسع ، لذلك لا أعرف ما إذا كان سيتسبب في مشاكل من تلقاء نفسه (خاصة بالنظر إلى تقرير jdalegonzalez أن النظام الأساسي قد يكون عاملاً هنا).

التبعيات:

pip install flask==1.0.2 pytest pytest-flask

اختبار:

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"

مرحبا abathur ،

لقد ألقيت نظرة سريعة على تنفيذ الريبو الخاص بك وفي لمحة ، يبدو أن الإخراج أدناه قابل لإعادة إنتاج المشكلة. سأحاول إلقاء نظرة أخرى في الأيام القليلة المقبلة ، لكنني مبتدئ فيما يتعلق بالقارورة والأفات والتركيبات 😃

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

بيئة

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

أهلا،

يبدو أن تقديم SpooledTemporaryFile من العلاقات العامة أدناه هو مصدر هذه المشكلة ...

PR: # 1198: دعم ترميز النقل المقسم

  • تم تقديم ملف SpooledTporaryFile - https://github.com/pallets/werkzeug/pull/1198/files#diff-91178c333961d75c285b7784d1b81cecR40)

PR: # 1222 - دعم إضافي للأنظمة الأساسية التي تفتقر إلى ملفات temp المخزنة

  • تنفيذ السلوك لـ FormDataParser.default_stream_factory - على الأنظمة الأساسية التي تدعم الكائن SpooledTemporaryFile و total_content_length > max_size

التعليق على تنفيذ SpooledTemporaryFile - يبدو أنه اجتياز رمز الاختبار 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()

يبدو أنه من ارتباطات abathur - توجد مشكلة في فئة SpooledTemporaryFile لأنها تفتقد إلى تنفيذ السمات المجردة "المفترضة" لـ IOBase . أيضًا ، قد يؤثر هذا على حالات الملفات التي يقل حجمها عن 512 كيلو بايت (؟)

يتطلع تطبيق التصحيح القرد أدناه على FormDataParser.default_stream_factory إلى اجتياز رمز الاختبار 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()

أو ربما...

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

ما هو أفضل مسار للعمل؟

  • تنفيذ رقعة قرد مقابل SpooledTemporaryFile ؟ (ربما توجد طريقة أفضل للقيام بذلك ...)
  • تجنب تنفيذ SpooledTemporaryFile ، حتى يتم إصدار إصلاح في إصدار لاحق من python وإضافة إعادة توجيه اعتمادًا على الإصدار python؟
  • ما زلت مبتدئًا في هذا المجال ، أعتقد أن هناك خيارات أخرى؟ :)

اعتدنا على عدم استخدام SpooledTemporaryFile ، ولكن بدا الأمر وكأنه طريقة مضمنة للقيام بما كان يفعله الكود الأصلي. لقد تسبب في الكثير من المشاكل منذ ذلك الحين. ربما علينا العودة إلى عدم استخدام SpooledTemporaryFile .

😞 ألاحظ أن bpo-26175 لديه طلب سحب قيد التقدم الآن ، على الأقل ، راجع python / cpython # 3249

تحدثت الدردشة مع mjpieters عن فكرة مختلفة. يمكننا تغيير FileStorage.__getattr__ لتجربة stream._file إذا كانت سمة غير موجودة في stream لكنها تحتوي على سمة _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

مرحبًا ، قم اليوم بترقية flask إلى 1.0.2 مع need werkzeug> = 0.14

لدي نفس الخطأ مع الإصدار الأخير من werkzeug (0.14.1) مع python 3.6.7 (الذي يأتي مع ubuntu 18.04):

AttributeError: الكائن "SpooledTarilyFile" ليس له سمة "قابل للقراءة"

هل تخطط لتحويل الإصلاح إلى الإصدار 0.14؟

شكرا لك.

الإصدار القادم سيكون 0.15. يرجى اتباع هذا الريبو أو مدونتنا أو https://twitter.com/PalletsTeam للحصول على إعلانات الإصدار.

هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات