You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2023/07/12 03:35:28 UTC
[superset] branch master updated: feat(csv-upload): Configurable max filesize (#24618)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 88418fc609 feat(csv-upload): Configurable max filesize (#24618)
88418fc609 is described below
commit 88418fc60906bcc7fa365e1ed4c81912c7447cbe
Author: Rob Moore <gi...@users.noreply.github.com>
AuthorDate: Wed Jul 12 04:35:22 2023 +0100
feat(csv-upload): Configurable max filesize (#24618)
---
superset/config.py | 3 +++
superset/forms.py | 25 ++++++++++++++++++-
superset/views/database/forms.py | 2 ++
tests/unit_tests/forms_tests.py | 54 ++++++++++++++++++++++++++++++++++++++++
4 files changed, 83 insertions(+), 1 deletion(-)
diff --git a/superset/config.py b/superset/config.py
index 9c6adf599b..4a7434093c 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -750,6 +750,9 @@ CSV_EXTENSIONS = {"csv", "tsv", "txt"}
COLUMNAR_EXTENSIONS = {"parquet", "zip"}
ALLOWED_EXTENSIONS = {*EXCEL_EXTENSIONS, *CSV_EXTENSIONS, *COLUMNAR_EXTENSIONS}
+# Optional maximum file size in bytes when uploading a CSV
+CSV_UPLOAD_MAX_SIZE = None
+
# CSV Options: key/value pairs that will be passed as argument to DataFrame.to_csv
# method.
# note: index option should not be overridden
diff --git a/superset/forms.py b/superset/forms.py
index f1e220ba95..1266870301 100644
--- a/superset/forms.py
+++ b/superset/forms.py
@@ -16,10 +16,12 @@
# under the License.
"""Contains the logic to create cohesive forms on the explore view"""
import json
+import os
from typing import Any, Optional
from flask_appbuilder.fieldwidgets import BS3TextFieldWidget
-from wtforms import Field
+from flask_babel import gettext as _
+from wtforms import Field, ValidationError
class JsonListField(Field):
@@ -53,6 +55,27 @@ class CommaSeparatedListField(Field):
self.data = []
+class FileSizeLimit: # pylint: disable=too-few-public-methods
+ """Imposes an optional maximum filesize limit for uploaded files"""
+
+ def __init__(self, max_size: Optional[int]):
+ self.max_size = max_size
+
+ def __call__(self, form: dict[str, Any], field: Any) -> None:
+ if self.max_size is None:
+ return
+
+ field.data.flush()
+ size = os.fstat(field.data.fileno()).st_size
+ if size > self.max_size:
+ raise ValidationError(
+ _(
+ "File size must be less than or equal to %(max_size)s bytes",
+ max_size=self.max_size,
+ )
+ )
+
+
def filter_not_empty_values(values: Optional[list[Any]]) -> Optional[list[Any]]:
"""Returns a list of non empty values or None"""
if not values:
diff --git a/superset/views/database/forms.py b/superset/views/database/forms.py
index b906e5e70b..767f4bb4e5 100644
--- a/superset/views/database/forms.py
+++ b/superset/views/database/forms.py
@@ -33,6 +33,7 @@ from wtforms.validators import DataRequired, Length, NumberRange, Optional, Rege
from superset import app, db, security_manager
from superset.forms import (
CommaSeparatedListField,
+ FileSizeLimit,
filter_not_empty_values,
JsonListField,
)
@@ -109,6 +110,7 @@ class CsvToDatabaseForm(UploadToDatabaseForm):
description=_("Select a file to be uploaded to the database"),
validators=[
FileRequired(),
+ FileSizeLimit(config["CSV_UPLOAD_MAX_SIZE"]),
FileAllowed(
config["ALLOWED_EXTENSIONS"].intersection(config["CSV_EXTENSIONS"]),
_(
diff --git a/tests/unit_tests/forms_tests.py b/tests/unit_tests/forms_tests.py
new file mode 100644
index 0000000000..0ede23551f
--- /dev/null
+++ b/tests/unit_tests/forms_tests.py
@@ -0,0 +1,54 @@
+import contextlib
+import tempfile
+from typing import Optional
+
+import pytest
+from flask_wtf.file import FileField
+from wtforms import Form, ValidationError
+
+from superset.forms import FileSizeLimit
+
+
+def _get_test_form(size_limit: Optional[int]) -> Form:
+ class TestForm(Form):
+ test = FileField("test", validators=[FileSizeLimit(size_limit)])
+
+ return TestForm()
+
+
+@contextlib.contextmanager
+def _tempfile(contents: bytes):
+ with tempfile.NamedTemporaryFile() as f:
+ f.write(contents)
+ f.flush()
+
+ yield f
+
+
+def test_file_size_limit_pass() -> None:
+ """Permit files which do not exceed the size limit"""
+ limit = 100
+ form = _get_test_form(limit)
+
+ with _tempfile(b"." * limit) as f:
+ form.test.data = f
+ assert form.validate() is True
+
+
+def test_file_size_limit_fail() -> None:
+ """Reject files which are too large"""
+ limit = 100
+ form = _get_test_form(limit)
+
+ with _tempfile(b"." * (limit + 1)) as f:
+ form.test.data = f
+ assert form.validate() is False
+
+
+def test_file_size_limit_ignored_if_none() -> None:
+ """Permit files when there is no limit"""
+ form = _get_test_form(None)
+
+ with _tempfile(b"." * 200) as f:
+ form.test.data = f
+ assert form.validate() is True