Repository: marimo-team/marimo

Issue:
File tree upload - hardcoded max size
### Description

I've read https://github.com/marimo-team/marimo/issues/609

and honestly I don't understand, why isn't there an option to set the file upload limit myself.
https://github.com/marimo-team/marimo/blob/ce197686b4da2cf67a9ed39aba4a72df23e745b8/frontend/src/components/editor/file-tree/upload.tsx#L10

It really made me consider twice using marimo for a small audio converting and transcribing app. In that case I needed to upload audio files that sometimes have more than 100 MB. I solved it for now by outsourcing chunking of the audio on the user and accepting multiple smaller files. But that's not great UX.

For example streamlit has 200 MB by default and you can change that in server config. https://docs.streamlit.io/develop/api-reference/widgets/st.file_uploader

### Suggested solution

Make file upload MAX_SIZE a settable parameter.

### Alternative

_No response_

### Additional context

_No response_

Discussion:
@mscolnick: @lomohov are you using the file upload widget or using the file sidebar? Are you building this as an app or just using this in a notebook?

We can make this configurable.
@lomohov: Hi, I am using marimo.ui.file, not sure if I referenced the right tsx file (my bad) and I built it as an app.

Thanks!

Solution patch:
diff --git a/frontend/src/plugins/impl/FileUploadPlugin.tsx b/frontend/src/plugins/impl/FileUploadPlugin.tsx
index e0a7d1a7..3f0a5a5d 100644
--- a/frontend/src/plugins/impl/FileUploadPlugin.tsx
+++ b/frontend/src/plugins/impl/FileUploadPlugin.tsx
@@ -20,12 +20,14 @@ type FileUploadType = "button" | "area";
  * @param filetypes - file types to accept (same as HTML input's accept attr)
  * @param multiple - whether to allow the user to upload multiple files
  * @param label - a label for the file upload area
+ * @param max_size - the maximum size of the file to upload (in bytes)
  */
 interface Data {
   filetypes: string[];
   multiple: boolean;
   kind: FileUploadType;
   label: string | null;
+  max_size: number;
 }
 
 type T = Array<[string, string]>;
@@ -38,6 +40,7 @@ export class FileUploadPlugin implements IPlugin<T, Data> {
     multiple: z.boolean(),
     kind: z.enum(["button", "area"]),
     label: z.string().nullable(),
+    max_size: z.number(),
   });
 
   render(props: IPluginProps<T, Data>): JSX.Element {
@@ -49,6 +52,7 @@ export class FileUploadPlugin implements IPlugin<T, Data> {
         kind={props.data.kind}
         value={props.value}
         setValue={props.setValue}
+        max_size={props.data.max_size}
       />
     );
   }
@@ -110,28 +114,16 @@ function groupFileTypesByMIMEType(extensions: string[]) {
   return filesByMIMEType;
 }
 
-// We may want to increase this based on user feedback
-//
-// But rather than forever increasing this, we should consider
-// adding a non-browser file-chooser which allows users to select files from
-// their local filesystem, and we only return the path.
-//
-// By using the browser's file chooser, it is more secure as we don
-// not get access to the uploaded file's path but
-// we are forced to upload the file to browser memory before
-// sending it to the server.
-const MAX_SIZE = 100_000_000; // 100 MB
-
 /* TODO(akshayka): Allow uploading files one-by-one and removing uploaded files
  * when multiple is `True`*/
 export const FileUpload = (props: FileUploadProps): JSX.Element => {
   const acceptGroups = groupFileTypesByMIMEType(props.filetypes);
-  const { setValue, kind, multiple, value } = props;
+  const { setValue, kind, multiple, value, max_size } = props;
   const { getRootProps, getInputProps, isFocused, isDragAccept, isDragReject } =
     useDropzone({
       accept: acceptGroups,
       multiple: multiple,
-      maxSize: MAX_SIZE,
+      maxSize: max_size,
       onError: (error) => {
         Logger.error(error);
         toast({
diff --git a/frontend/src/stories/file-upload.stories.tsx b/frontend/src/stories/file-upload.stories.tsx
index efab5f5a..7547f205 100644
--- a/frontend/src/stories/file-upload.stories.tsx
+++ b/frontend/src/stories/file-upload.stories.tsx
@@ -15,6 +15,7 @@ export const AcceptAny = {
       label={null}
       value={[]}
       setValue={() => null}
+      max_size={100_000_000}
     />
   ),
 
@@ -30,6 +31,7 @@ export const AcceptLongText = {
       label={"Drop here, ".repeat(100)}
       value={[]}
       setValue={() => null}
+      max_size={100_000_000}
     />
   ),
 
@@ -45,6 +47,7 @@ export const AcceptTxtOnly = {
       label={null}
       value={[]}
       setValue={() => null}
+      max_size={100_000_000}
     />
   ),
 
@@ -60,6 +63,7 @@ export const AcceptTxtOnlyButton = {
       label={null}
       value={[]}
       setValue={() => null}
+      max_size={100_000_000}
     />
   ),
 
@@ -75,6 +79,7 @@ export const SingleFileArea = {
       label={null}
       value={[]}
       setValue={() => null}
+      max_size={100_000_000}
     />
   ),
 
@@ -90,6 +95,7 @@ export const SingleFileButton = {
       label={null}
       value={[]}
       setValue={() => null}
+      max_size={100_000_000}
     />
   ),
 
diff --git a/marimo/_data/get_datasets.py b/marimo/_data/get_datasets.py
index 17aac49b..a893eab7 100644
--- a/marimo/_data/get_datasets.py
+++ b/marimo/_data/get_datasets.py
@@ -112,10 +112,10 @@ def _get_databases_from_duckdb_internal(
     if connection is None:
         import duckdb
 
-        databases = duckdb.execute("SHOW ALL TABLES").fetchall()
+        databases_result = duckdb.execute("SHOW ALL TABLES").fetchall()
     else:
-        databases = connection.execute("SHOW ALL TABLES").fetchall()
-    if not len(databases):
+        databases_result = connection.execute("SHOW ALL TABLES").fetchall()
+    if not len(databases_result):
         # No tables
         return []
 
@@ -130,7 +130,7 @@ def _get_databases_from_duckdb_internal(
         column_names,
         column_types,
         *_rest,
-    ) in databases:
+    ) in databases_result:
         assert len(column_names) == len(column_types)
         assert isinstance(column_names, list)
         assert isinstance(column_types, list)
@@ -167,9 +167,9 @@ def _get_databases_from_duckdb_internal(
         databases_dict[database][schema].append(table)
 
     # Convert grouped data into Database objects
-    databases = []
+    databases: list[Database] = []
     for database, schemas_dict in databases_dict.items():
-        schema_list = []
+        schema_list: list[Schema] = []
         for schema_name, tables in schemas_dict.items():
             schema_list.append(Schema(name=schema_name, tables=tables))
         databases.append(
@@ -241,7 +241,15 @@ def _db_type_to_data_type(db_type: str) -> DataType:
     ]:
         return "datetime"
     # Nested types
-    if db_type in ["array", "list", "struct", "map", "union", "varchar[]"]:
+    if "[]" in db_type:
+        return "unknown"
+    if (
+        db_type.startswith("union")
+        or db_type.startswith("map")
+        or db_type.startswith("struct")
+        or db_type.startswith("list")
+        or db_type.startswith("array")
+    ):
         return "unknown"
     # Special types
     if db_type == "bit":
diff --git a/marimo/_plugins/ui/_impl/input.py b/marimo/_plugins/ui/_impl/input.py
index 4ce18bbc..e232ecfe 100644
--- a/marimo/_plugins/ui/_impl/input.py
+++ b/marimo/_plugins/ui/_impl/input.py
@@ -1324,6 +1324,8 @@ class file(UIElement[list[tuple[str, str]], Sequence[FileUploadResults]]):
         on_change (Callable[[Sequence[FileUploadResults]], None], optional):
             Optional callback to run when this element's value changes.
             Defaults to None.
+        max_size (int, optional): The maximum size of the file to upload
+            (in bytes). Defaults to 100MB.
     """
 
     _name: Final[str] = "marimo-file"
@@ -1334,6 +1336,7 @@ class file(UIElement[list[tuple[str, str]], Sequence[FileUploadResults]]):
         multiple: bool = False,
         kind: Literal["button", "area"] = "button",
         *,
+        max_size: int = 100_000_000,  # 100MB default
         label: str = "",
         on_change: Optional[
             Callable[[Sequence[FileUploadResults]], None]
@@ -1351,6 +1354,9 @@ class file(UIElement[list[tuple[str, str]], Sequence[FileUploadResults]]):
                     f"Invalid types: {', '.join(invalid_types)}"
                 )
 
+        if max_size <= 0:
+            raise ValueError("max_size must be greater than 0")
+
         super().__init__(
             component_name=file._name,
             initial_value=[],
@@ -1359,6 +1365,7 @@ class file(UIElement[list[tuple[str, str]], Sequence[FileUploadResults]]):
                 "filetypes": filetypes if filetypes is not None else [],
                 "multiple": multiple,
                 "kind": kind,
+                "max_size": max_size,
             },
             on_change=on_change,
         )


Test patch:
diff --git a/tests/_data/test_get_datasets.py b/tests/_data/test_get_datasets.py
index 504f2ff5..bad054eb 100644
--- a/tests/_data/test_get_datasets.py
+++ b/tests/_data/test_get_datasets.py
@@ -48,7 +48,22 @@ CREATE TABLE all_types (
     col_blob BLOB,
     col_bit BIT,
     col_uuid UUID,
-    col_json JSON
+    col_json JSON,
+    col_real REAL,
+    col_numeric NUMERIC(10,2),
+    col_char CHAR(10),
+    col_bpchar BPCHAR(10),
+    col_text TEXT,
+    col_string STRING,
+    col_timestamp_ns TIMESTAMP_NS,
+    col_timestamptz TIMESTAMPTZ,
+    col_datetime DATETIME,
+    col_array INTEGER[],
+    col_struct STRUCT(id INTEGER, name VARCHAR),
+    col_map MAP(INTEGER, VARCHAR),
+    col_union UNION(int INTEGER, varchar VARCHAR),
+    col_enum ENUM('a', 'b', 'c'),
+    col_uhugeint UHUGEINT
 );
 
 CREATE SCHEMA s1;
@@ -64,7 +79,7 @@ all_types_tables = [
         source_type="duckdb",
         source="memory",
         num_rows=None,
-        num_columns=22,
+        num_columns=37,
         variable_name=None,
         columns=[
             DataTableColumn(
@@ -199,6 +214,96 @@ all_types_tables = [
                 external_type="JSON",
                 sample_values=[],
             ),
+            DataTableColumn(
+                name="col_real",
+                type="number",
+                external_type="FLOAT",
+                sample_values=[],
+            ),
+            DataTableColumn(
+                name="col_numeric",
+                type="number",
+                external_type="DECIMAL(10,2)",
+                sample_values=[],
+            ),
+            DataTableColumn(
+                name="col_char",
+                type="string",
+                external_type="VARCHAR",
+                sample_values=[],
+            ),
+            DataTableColumn(
+                name="col_bpchar",
+                type="string",
+                external_type="VARCHAR",
+                sample_values=[],
+            ),
+            DataTableColumn(
+                name="col_text",
+                type="string",
+                external_type="VARCHAR",
+                sample_values=[],
+            ),
+            DataTableColumn(
+                name="col_string",
+                type="string",
+                external_type="VARCHAR",
+                sample_values=[],
+            ),
+            DataTableColumn(
+                name="col_timestamp_ns",
+                type="datetime",
+                external_type="TIMESTAMP_NS",
+                sample_values=[],
+            ),
+            DataTableColumn(
+                name="col_timestamptz",
+                type="datetime",
+                external_type="TIMESTAMP WITH TIME ZONE",
+                sample_values=[],
+            ),
+            DataTableColumn(
+                name="col_datetime",
+                type="datetime",
+                external_type="TIMESTAMP",
+                sample_values=[],
+            ),
+            DataTableColumn(
+                name="col_array",
+                type="unknown",
+                external_type="INTEGER[]",
+                sample_values=[],
+            ),
+            DataTableColumn(
+                name="col_struct",
+                type="unknown",
+                external_type='STRUCT(id INTEGER, "name" VARCHAR)',
+                sample_values=[],
+            ),
+            DataTableColumn(
+                name="col_map",
+                type="unknown",
+                external_type="MAP(INTEGER, VARCHAR)",
+                sample_values=[],
+            ),
+            DataTableColumn(
+                name="col_union",
+                type="unknown",
+                external_type='UNION("int" INTEGER, "varchar" VARCHAR)',
+                sample_values=[],
+            ),
+            DataTableColumn(
+                name="col_enum",
+                type="string",
+                external_type="ENUM('a', 'b', 'c')",
+                sample_values=[],
+            ),
+            DataTableColumn(
+                name="col_uhugeint",
+                type="integer",
+                external_type="UHUGEINT",
+                sample_values=[],
+            ),
         ],
     )
 ]
diff --git a/tests/_plugins/ui/_impl/test_input.py b/tests/_plugins/ui/_impl/test_input.py
index 493e830f..58abd7e8 100644
--- a/tests/_plugins/ui/_impl/test_input.py
+++ b/tests/_plugins/ui/_impl/test_input.py
@@ -608,6 +608,23 @@ def test_file_validation() -> None:
     assert "or contain a forward slash" in str(e.value)
     assert "doc, pdf" in str(e.value)
 
+    # Test max_size validation
+    with pytest.raises(ValueError) as e:
+        ui.file(max_size=0)
+    assert "max_size must be greater than 0" in str(e.value)
+
+    with pytest.raises(ValueError) as e:
+        ui.file(max_size=-1)
+    assert "max_size must be greater than 0" in str(e.value)
+
+    # Test default max_size
+    f = ui.file()
+    assert f._component_args["max_size"] == 100_000_000  # 100MB
+
+    # Test custom max_size
+    f = ui.file(max_size=50_000_000)  # 50MB
+    assert f._component_args["max_size"] == 50_000_000
+
 
 @pytest.mark.skipif(not HAS_NUMPY, reason="numpy not installed")
 def test_numpy_steps() -> None: