diff --git a/.changeset/tiny-cars-spend.md b/.changeset/tiny-cars-spend.md new file mode 100644 index 0000000000..5ae1213e0d --- /dev/null +++ b/.changeset/tiny-cars-spend.md @@ -0,0 +1,68 @@ +--- +"@gradio/app": minor +"@gradio/audio": minor +"@gradio/chatbot": minor +"@gradio/checkbox": minor +"@gradio/checkboxgroup": minor +"@gradio/client": minor +"@gradio/code": minor +"@gradio/colorpicker": minor +"@gradio/dataframe": minor +"@gradio/dropdown": minor +"@gradio/fallback": minor +"@gradio/file": minor +"@gradio/fileexplorer": minor +"@gradio/gallery": minor +"@gradio/highlightedtext": minor +"@gradio/html": minor +"@gradio/image": minor +"@gradio/imageeditor": minor +"@gradio/json": minor +"@gradio/label": minor +"@gradio/markdown": minor +"@gradio/model3d": minor +"@gradio/multimodaltextbox": minor +"@gradio/number": minor +"@gradio/plot": minor +"@gradio/radio": minor +"@gradio/simpledropdown": minor +"@gradio/simpleimage": minor +"@gradio/simpletextbox": minor +"@gradio/slider": minor +"@gradio/statustracker": minor +"@gradio/storybook": minor +"@gradio/textbox": minor +"@gradio/tootils": minor +"@gradio/upload": minor +"@gradio/uploadbutton": minor +"@gradio/utils": minor +"@gradio/video": minor +"gradio": minor +"gradio_client": minor +--- + + +#### Setting File Upload Limits + +We have added a `max_file_size` size parameter to `launch()` that limits to size of files uploaded to the server. This limit applies to each individual file. This parameter can be specified as a string or an integer (corresponding to the size in bytes). + +The following code snippet sets a max file size of 5 megabytes. + +```python +import gradio as gr + +demo = gr.Interface(lambda x: x, "image", "image") + +demo.launch(max_file_size="5mb") +# or +demo.launch(max_file_size=5 * gr.FileSize.MB) +``` + +![max_file_size_upload](https://github.com/gradio-app/gradio/assets/41651716/7547330c-a082-4901-a291-3f150a197e45) + + +#### Error states can now be cleared + +When a component encounters an error, the error state shown in the UI can now be cleared by clicking on the `x` icon in the top right of the component. This applies to all types of errors, whether it's raised in the UI or the server. + +![error_modal_calculator](https://github.com/gradio-app/gradio/assets/41651716/16cb071c-accd-45a6-9c18-0dea27d4bd98) \ No newline at end of file diff --git a/.config/playwright-setup.js b/.config/playwright-setup.js index 17f0dcdeea..eb980ec98c 100644 --- a/.config/playwright-setup.js +++ b/.config/playwright-setup.js @@ -115,7 +115,12 @@ ${demos.map((d) => `from demo.${d}.run import demo as ${d}`).join("\n")} app = FastAPI() ${demos - .map((d) => `app = gr.mount_gradio_app(app, ${d}, path="/${d}")`) + .map( + (d) => + `app = gr.mount_gradio_app(app, ${d}, path="/${d}", max_file_size=${ + d == "upload_file_limit_test" ? "'15kb'" : "None" + })` + ) .join("\n")} config = uvicorn.Config(app, port=${port}, log_level="info") diff --git a/client/js/src/client.ts b/client/js/src/client.ts index 405a0ab528..00bc2c0c39 100644 --- a/client/js/src/client.ts +++ b/client/js/src/client.ts @@ -250,6 +250,9 @@ export function api_factory( } catch (e) { return { error: BROKEN_CONNECTION_MSG }; } + if (!response.ok) { + return { error: await response.text() }; + } const output: UploadResponse["files"] = await response.json(); uploadResponses.push(...output); } diff --git a/client/js/src/types.ts b/client/js/src/types.ts index 2c0b4375ca..e248ba3e75 100644 --- a/client/js/src/types.ts +++ b/client/js/src/types.ts @@ -21,6 +21,7 @@ export interface Config { stylesheets: string[]; path: string; protocol?: "sse_v2.1" | "sse_v2" | "sse_v1" | "sse" | "ws"; + max_file_size?: number; } export interface Payload { diff --git a/client/js/src/upload.ts b/client/js/src/upload.ts index 5995024dbe..c891417dc1 100644 --- a/client/js/src/upload.ts +++ b/client/js/src/upload.ts @@ -13,12 +13,24 @@ export async function upload( file_data: FileData[], root: string, upload_id?: string, + max_file_size?: number, upload_fn: typeof upload_files = upload_files ): Promise<(FileData | null)[] | null> { let files = (Array.isArray(file_data) ? file_data : [file_data]).map( (file_data) => file_data.blob! ); + const oversized_files = files.filter( + (f) => f.size > (max_file_size ?? Infinity) + ); + if (oversized_files.length) { + throw new Error( + `File size exceeds the maximum allowed size of ${max_file_size} bytes: ${oversized_files + .map((f) => f.name) + .join(", ")}` + ); + } + return await Promise.all( await upload_fn(root, files, undefined, upload_id).then( async (response: { files?: string[]; error?: string }) => { diff --git a/client/python/gradio_client/client.py b/client/python/gradio_client/client.py index 0d37f80eb7..c3bbd6151a 100644 --- a/client/python/gradio_client/client.py +++ b/client/python/gradio_client/client.py @@ -4,6 +4,7 @@ from __future__ import annotations import concurrent.futures import hashlib import json +import math import os import re import secrets @@ -17,6 +18,7 @@ import warnings from concurrent.futures import Future from dataclasses import dataclass from datetime import datetime +from functools import partial from pathlib import Path from threading import Lock from typing import Any, Callable, Literal @@ -1169,7 +1171,7 @@ class Endpoint: if self.client.upload_files and self.input_component_types[i].value_is_file: d = utils.traverse( d, - self._upload_file, + partial(self._upload_file, data_index=i), lambda f: utils.is_filepath(f) or utils.is_file_obj_with_meta(f) or utils.is_http_url_like(f), @@ -1217,7 +1219,7 @@ class Endpoint: else: return data - def _upload_file(self, f: str | dict) -> dict[str, str]: + def _upload_file(self, f: str | dict, data_index: int) -> dict[str, str]: if isinstance(f, str): warnings.warn( f'The Client is treating: "{f}" as a file path. In future versions, this behavior will not happen automatically. ' @@ -1228,6 +1230,22 @@ class Endpoint: else: file_path = f["path"] if not utils.is_http_url_like(file_path): + component_id = self.dependency["inputs"][data_index] + component_config = next( + ( + c + for c in self.client.config["components"] + if c["id"] == component_id + ), + {}, + ) + max_file_size = self.client.config.get("max_file_size", None) + max_file_size = math.inf if max_file_size is None else max_file_size + if os.path.getsize(file_path) > max_file_size: + raise ValueError( + f"File {file_path} exceeds the maximum file size of {max_file_size} bytes " + f"set in {component_config.get('label', '') + ''} component." + ) with open(file_path, "rb") as f: files = [("files", (Path(file_path).name, f))] r = httpx.post( diff --git a/client/python/test/conftest.py b/client/python/test/conftest.py index 986dc48062..4613820835 100644 --- a/client/python/test/conftest.py +++ b/client/python/test/conftest.py @@ -446,3 +446,16 @@ def many_endpoint_demo(): butn2.click(noop, msg2, msg2) return demo + + +@pytest.fixture +def max_file_size_demo(): + with gr.Blocks() as demo: + file_1b = gr.File() + upload_status = gr.Textbox() + + file_1b.upload( + lambda x: "Upload successful", file_1b, upload_status, api_name="upload_1b" + ) + + return demo diff --git a/client/python/test/files/alphabet.txt b/client/python/test/files/alphabet.txt new file mode 100644 index 0000000000..e85d5b4528 --- /dev/null +++ b/client/python/test/files/alphabet.txt @@ -0,0 +1 @@ +abcdefghijklmnopqrstuvwxyz \ No newline at end of file diff --git a/client/python/test/files/cheetah1.jpg b/client/python/test/files/cheetah1.jpg new file mode 100644 index 0000000000..c510ff30e0 Binary files /dev/null and b/client/python/test/files/cheetah1.jpg differ diff --git a/client/python/test/test_client.py b/client/python/test/test_client.py index 65c49e71c3..a5af511699 100644 --- a/client/python/test/test_client.py +++ b/client/python/test/test_client.py @@ -36,9 +36,12 @@ HF_TOKEN = os.getenv("HF_TOKEN") or HfFolder.get_token() @contextmanager def connect( - demo: gr.Blocks, serialize: bool = True, output_dir: str = DEFAULT_TEMP_DIR + demo: gr.Blocks, + serialize: bool = True, + output_dir: str = DEFAULT_TEMP_DIR, + max_file_size=None, ): - _, local_url, _ = demo.launch(prevent_thread_lock=True) + _, local_url, _ = demo.launch(prevent_thread_lock=True, max_file_size=max_file_size) try: yield Client(local_url, serialize=serialize, output_dir=output_dir) finally: @@ -85,6 +88,18 @@ class TestClientPredictions: with pytest.raises(ValueError, match="invalid state"): Client("gradio-tests/paused-space") + def test_raise_error_max_file_size(self, max_file_size_demo): + with connect(max_file_size_demo, max_file_size="15kb") as client: + with pytest.raises(ValueError, match="exceeds the maximum file size"): + client.predict( + file(Path(__file__).parent / "files" / "cheetah1.jpg"), + api_name="/upload_1b", + ) + client.predict( + file(Path(__file__).parent / "files" / "alphabet.txt"), + api_name="/upload_1b", + ) + @pytest.mark.flaky def test_numerical_to_label_space(self): client = Client("gradio-tests/titanic-survival") diff --git a/demo/image_mod_default_image/run.ipynb b/demo/image_mod_default_image/run.ipynb index 9e2befb5bc..1a17252385 100644 --- a/demo/image_mod_default_image/run.ipynb +++ b/demo/image_mod_default_image/run.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: image_mod_default_image"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["# Downloading files from the demo repo\n", "import os\n", "os.mkdir('images')\n", "!wget -q -O images/cheetah1.jpg https://github.com/gradio-app/gradio/raw/main/demo/image_mod_default_image/images/cheetah1.jpg\n", "!wget -q -O images/lion.jpg https://github.com/gradio-app/gradio/raw/main/demo/image_mod_default_image/images/lion.jpg\n", "!wget -q -O images/logo.png https://github.com/gradio-app/gradio/raw/main/demo/image_mod_default_image/images/logo.png"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "import os\n", "\n", "\n", "def image_mod(image):\n", " return image.rotate(45)\n", "\n", "\n", "cheetah = os.path.join(os.path.abspath(''), \"images/cheetah1.jpg\")\n", "\n", "demo = gr.Interface(image_mod, gr.Image(type=\"pil\", value=cheetah), \"image\",\n", " flagging_options=[\"blurry\", \"incorrect\", \"other\"], examples=[\n", " os.path.join(os.path.abspath(''), \"images/lion.jpg\"),\n", " os.path.join(os.path.abspath(''), \"images/logo.png\")\n", " ])\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: image_mod_default_image"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["# Downloading files from the demo repo\n", "import os\n", "os.mkdir('images')\n", "!wget -q -O images/cheetah1.jpg https://github.com/gradio-app/gradio/raw/main/demo/image_mod_default_image/images/cheetah1.jpg\n", "!wget -q -O images/lion.jpg https://github.com/gradio-app/gradio/raw/main/demo/image_mod_default_image/images/lion.jpg\n", "!wget -q -O images/logo.png https://github.com/gradio-app/gradio/raw/main/demo/image_mod_default_image/images/logo.png"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "import os\n", "\n", "\n", "def image_mod(image):\n", " return image.rotate(45)\n", "\n", "\n", "cheetah = os.path.join(os.path.abspath(''), \"images/cheetah1.jpg\")\n", "\n", "demo = gr.Interface(image_mod, gr.Image(type=\"pil\", value=cheetah), \"image\",\n", " flagging_options=[\"blurry\", \"incorrect\", \"other\"], examples=[\n", " os.path.join(os.path.abspath(''), \"images/lion.jpg\"),\n", " os.path.join(os.path.abspath(''), \"images/logo.png\")\n", " ])\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch(max_file_size=\"70kb\")\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/image_mod_default_image/run.py b/demo/image_mod_default_image/run.py index c2ad1f8be4..d05c3e299b 100644 --- a/demo/image_mod_default_image/run.py +++ b/demo/image_mod_default_image/run.py @@ -15,4 +15,4 @@ demo = gr.Interface(image_mod, gr.Image(type="pil", value=cheetah), "image", ]) if __name__ == "__main__": - demo.launch() + demo.launch(max_file_size="70kb") diff --git a/demo/upload_file_limit_test/run.ipynb b/demo/upload_file_limit_test/run.ipynb new file mode 100644 index 0000000000..f61fe8abd5 --- /dev/null +++ b/demo/upload_file_limit_test/run.ipynb @@ -0,0 +1 @@ +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: upload_file_limit_test"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "with gr.Blocks() as demo:\n", " gr.Markdown(\"\"\"\n", " # \u2b06\ufe0f\ud83d\udcc1 max_file_size test\n", " The demo has a max file size of 15kb. The error modal should pop up when a file larger than that is uploaded. \n", " \"\"\")\n", " with gr.Row():\n", " with gr.Column():\n", " gr.Image(label=\"Image\", interactive=True)\n", " gr.Gallery(label=\"Gallery\", interactive=True)\n", " gr.File(label=\"Single File\", interactive=True, file_count=\"single\")\n", " with gr.Column():\n", " gr.Model3D(label=\"Model 3D\", interactive=True,)\n", " gr.MultimodalTextbox(label=\"Multimodal Textbox\", interactive=True)\n", " gr.UploadButton(label=\"Upload Button\", interactive=True)\n", " with gr.Column():\n", " gr.Video(label=\"Video\", interactive=True)\n", " gr.Audio(label=\"Audio\", interactive=True)\n", " gr.File(label=\"Multiple Files\", interactive=True, file_count=\"multiple\")\n", "\n", "\n", "if __name__ == \"__main__\":\n", " # The upload limit is set in playwright_setup.js\n", " # since the e2e tests use mount_gradio_app\n", " demo.launch(max_file_size=\"15kb\")"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/upload_file_limit_test/run.py b/demo/upload_file_limit_test/run.py new file mode 100644 index 0000000000..25770e3429 --- /dev/null +++ b/demo/upload_file_limit_test/run.py @@ -0,0 +1,26 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Markdown(""" + # ⬆️📁 max_file_size test + The demo has a max file size of 15kb. The error modal should pop up when a file larger than that is uploaded. + """) + with gr.Row(): + with gr.Column(): + gr.Image(label="Image", interactive=True) + gr.Gallery(label="Gallery", interactive=True) + gr.File(label="Single File", interactive=True, file_count="single") + with gr.Column(): + gr.Model3D(label="Model 3D", interactive=True,) + gr.MultimodalTextbox(label="Multimodal Textbox", interactive=True) + gr.UploadButton(label="Upload Button", interactive=True) + with gr.Column(): + gr.Video(label="Video", interactive=True) + gr.Audio(label="Audio", interactive=True) + gr.File(label="Multiple Files", interactive=True, file_count="multiple") + + +if __name__ == "__main__": + # The upload limit is set in playwright_setup.js + # since the e2e tests use mount_gradio_app + demo.launch(max_file_size="15kb") \ No newline at end of file diff --git a/gradio/__init__.py b/gradio/__init__.py index 433374a83b..23e8b6bae6 100644 --- a/gradio/__init__.py +++ b/gradio/__init__.py @@ -96,7 +96,7 @@ from gradio.templates import ( TextArea, ) from gradio.themes import Base as Theme -from gradio.utils import NO_RELOAD, get_package_version, set_static_paths +from gradio.utils import NO_RELOAD, FileSize, get_package_version, set_static_paths from gradio.wasm_utils import IS_WASM if not IS_WASM: diff --git a/gradio/blocks.py b/gradio/blocks.py index ea3e9c0b34..260f9529b6 100644 --- a/gradio/blocks.py +++ b/gradio/blocks.py @@ -1853,6 +1853,7 @@ Received outputs: "show_error": getattr(self, "show_error", False), "show_api": self.show_api, "is_colab": utils.colab_check(), + "max_file_size": getattr(self, "max_file_size", None), "stylesheets": self.stylesheets, "theme": self.theme.name, "protocol": "sse_v3", @@ -2031,6 +2032,7 @@ Received outputs: share_server_address: str | None = None, share_server_protocol: Literal["http", "https"] | None = None, auth_dependency: Callable[[fastapi.Request], str | None] | None = None, + max_file_size: str | int | None = None, _frontend: bool = True, ) -> tuple[FastAPI, str, str]: """ @@ -2066,6 +2068,7 @@ Received outputs: share_server_address: Use this to specify a custom FRP server and port for sharing Gradio apps (only applies if share=True). If not provided, will use the default FRP server at https://gradio.live. See https://github.com/huggingface/frp for more information. share_server_protocol: Use this to specify the protocol to use for the share links. Defaults to "https", unless a custom share_server_address is provided, in which case it defaults to "http". If you are using a custom share_server_address and want to use https, you must set this to "https". auth_dependency: A function that takes a FastAPI request and returns a string user ID or None. If the function returns None for a specific request, that user is not authorized to access the app (they will see a 401 Unauthorized response). To be used with external authentication systems like OAuth. Cannot be used with `auth`. + max_file_size: The maximum file size in bytes that can be uploaded. Can be a string of the form "", where value is any positive integer and unit is one of "b", "kb", "mb", "gb", "tb". If None, no limit is set. Returns: app: FastAPI app object that is running the demo local_url: Locally accessible link to the demo @@ -2128,6 +2131,7 @@ Received outputs: raise ValueError("`blocked_paths` must be a list of directories.") self.validate_queue_settings() + self.max_file_size = utils._parse_file_size(max_file_size) self.config = self.get_config_file() self.max_threads = max_threads diff --git a/gradio/route_utils.py b/gradio/route_utils.py index 14be277810..27ef429635 100644 --- a/gradio/route_utils.py +++ b/gradio/route_utils.py @@ -454,6 +454,7 @@ class GradioMultiPartParser: max_fields: Union[int, float] = 1000, upload_id: str | None = None, upload_progress: FileUploadProgress | None = None, + max_file_size: int | float, ) -> None: self.headers = headers self.stream = stream @@ -464,6 +465,7 @@ class GradioMultiPartParser: self.upload_progress = upload_progress self._current_files = 0 self._current_fields = 0 + self.max_file_size = max_file_size self._current_partial_header_name: bytes = b"" self._current_partial_header_value: bytes = b"" self._current_part = MultipartPart() @@ -594,6 +596,12 @@ class GradioMultiPartParser: assert part.file # for type checkers # noqa: S101 await part.file.write(data) part.file.sha.update(data) # type: ignore + if os.stat(part.file.file.name).st_size > self.max_file_size: + if self.upload_progress is not None: + self.upload_progress.set_done(self.upload_id) # type: ignore + raise MultiPartException( + f"File size exceeded maximum allowed size of {self.max_file_size} bytes." + ) for part in self._file_parts_to_finish: assert part.file # for type checkers # noqa: S101 await part.file.seek(0) @@ -603,6 +611,7 @@ class GradioMultiPartParser: # Close all the files if there was an error. for file in self._files_to_close_on_error: file.close() + Path(file.name).unlink() raise exc parser.finalize() diff --git a/gradio/routes.py b/gradio/routes.py index 7d9b13c5be..2f6e21f388 100644 --- a/gradio/routes.py +++ b/gradio/routes.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio import contextlib +import math import sys if sys.version_info >= (3, 9): @@ -1043,21 +1044,26 @@ class App(FastAPI): try: if upload_id: file_upload_statuses.track(upload_id) + max_file_size = app.get_blocks().max_file_size + max_file_size = max_file_size if max_file_size is not None else math.inf multipart_parser = GradioMultiPartParser( request.headers, request.stream(), max_files=1000, max_fields=1000, + max_file_size=max_file_size, upload_id=upload_id if upload_id else None, upload_progress=file_upload_statuses if upload_id else None, ) form = await multipart_parser.parse() except MultiPartException as exc: - raise HTTPException(status_code=400, detail=exc.message) from exc + code = 413 if "maximum allowed size" in exc.message else 400 + return PlainTextResponse(exc.message, status_code=code) output_files = [] files_to_copy = [] locations: list[str] = [] + for temp_file in form.getlist("files"): if not isinstance(temp_file, GradioUploadFile): raise TypeError("File is not an instance of GradioUploadFile") @@ -1172,6 +1178,7 @@ def mount_gradio_app( blocked_paths: list[str] | None = None, favicon_path: str | None = None, show_error: bool = True, + max_file_size: str | int | None = None, ) -> fastapi.FastAPI: """Mount a gradio.Blocks to an existing FastAPI application. @@ -1188,6 +1195,7 @@ def mount_gradio_app( blocked_paths: List of complete filepaths or parent directories that this gradio app is not allowed to serve (i.e. users of your app are not allowed to access). Must be absolute paths. Warning: takes precedence over `allowed_paths` and all other directories exposed by Gradio by default. favicon_path: If a path to a file (.png, .gif, or .ico) is provided, it will be used as the favicon for this gradio app's page. show_error: If True, any errors in the gradio app will be displayed in an alert modal and printed in the browser console log. Otherwise, errors will only be visible in the terminal session running the Gradio app. + max_file_size: The maximum file size in bytes that can be uploaded. Can be a string of the form "", where value is any positive integer and unit is one of "b", "kb", "mb", "gb", "tb". If None, no limit is set. Example: from fastapi import FastAPI import gradio as gr @@ -1200,6 +1208,7 @@ def mount_gradio_app( # Then run `uvicorn run:app` from the terminal and navigate to http://localhost:8000/gradio. """ blocks.dev_mode = False + blocks.max_file_size = utils._parse_file_size(max_file_size) blocks.config = blocks.get_config_file() blocks.validate_queue_settings() if auth is not None and auth_dependency is not None: diff --git a/gradio/utils.py b/gradio/utils.py index 47993d48af..d3bd619ecd 100644 --- a/gradio/utils.py +++ b/gradio/utils.py @@ -1280,3 +1280,27 @@ def async_lambda(f: Callable) -> Callable: return f(*args, **kwargs) return function_wrapper + + +class FileSize: + B = 1 + KB = 1024 * B + MB = 1024 * KB + GB = 1024 * MB + TB = 1024 * GB + + +def _parse_file_size(size: str | int | None) -> int | None: + if isinstance(size, int) or size is None: + return size + + size = size.replace(" ", "") + + last_digit_index = next( + (i for i, c in enumerate(size) if not c.isdigit()), len(size) + ) + size_int, unit = int(size[:last_digit_index]), size[last_digit_index:].upper() + multiple = getattr(FileSize, unit, None) + if not multiple: + raise ValueError(f"Invalid file size unit: {unit}") + return multiple * size_int diff --git a/guides/01_getting-started/03_sharing-your-app.md b/guides/01_getting-started/03_sharing-your-app.md index 21b189f0bc..d2f6bf70df 100644 --- a/guides/01_getting-started/03_sharing-your-app.md +++ b/guides/01_getting-started/03_sharing-your-app.md @@ -451,4 +451,16 @@ Gradio DOES NOT ALLOW access to: - **Any other paths on the host machine**. Users should NOT be able to access other arbitrary paths on the host. +Sharing your Gradio application will also allow users to upload files to your computer or server. You can set a maximum file size for uploads to prevent abuse and to preserve disk space. You can do this with the `max_file_size` parameter of `.launch`. For example, the following two code snippets limit file uploads to 5 megabytes per file. + +```python +import gradio as gr + +demo = gr.Interface(lambda x: x, "image", "image") + +demo.launch(max_file_size="5mb") +# or +demo.launch(max_file_size=5 * gr.FileSize.MB) +``` + Please make sure you are running the latest version of `gradio` for these security settings to apply. diff --git a/js/app/src/Blocks.svelte b/js/app/src/Blocks.svelte index d32cd218f3..3dda260631 100644 --- a/js/app/src/Blocks.svelte +++ b/js/app/src/Blocks.svelte @@ -3,7 +3,7 @@ import { _ } from "svelte-i18n"; import { client } from "@gradio/client"; - import type { LoadingStatusCollection } from "./stores"; + import type { LoadingStatus, LoadingStatusCollection } from "./stores"; import type { ComponentMeta, Dependency, LayoutNode } from "./types"; import type { UpdateTransaction } from "./init"; @@ -430,6 +430,8 @@ trigger_share(title, description); } else if (event === "error" || event === "warning") { messages = [new_message(data, -1, event), ...messages]; + } else if (event == "clear_status") { + update_status(id, "complete", data); } else { const deps = $targets[id]?.[event]; @@ -452,6 +454,21 @@ $: set_status($loading_status); + function update_status( + id: number, + status: "error" | "complete" | "pending", + data: LoadingStatus + ): void { + data.status = status; + update_value([ + { + id, + prop: "loading_status", + value: data + } + ]); + } + function set_status(statuses: LoadingStatusCollection): void { const updates = Object.entries(statuses).map(([id, loading_status]) => { let dependency = dependencies[loading_status.fn_index]; @@ -518,6 +535,7 @@ on:destroy={({ detail }) => handle_destroy(detail)} {version} {autoscroll} + max_file_size={app.config.max_file_size} /> {/if} diff --git a/js/app/src/MountComponents.svelte b/js/app/src/MountComponents.svelte index d4c03ffa2b..42ce7bf288 100644 --- a/js/app/src/MountComponents.svelte +++ b/js/app/src/MountComponents.svelte @@ -8,6 +8,7 @@ export let theme_mode: any; export let version: any; export let autoscroll: boolean; + export let max_file_size: number | null = null; const dispatch = createEventDispatcher<{ mount?: never }>(); onMount(() => { @@ -15,4 +16,12 @@ }); - + diff --git a/js/app/src/Render.svelte b/js/app/src/Render.svelte index 377461a060..a7f302dfd1 100644 --- a/js/app/src/Render.svelte +++ b/js/app/src/Render.svelte @@ -1,5 +1,5 @@ @@ -40,6 +41,7 @@ autoscroll={gradio.autoscroll} i18n={gradio.i18n} {...loading_status} + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} /> ; export let latex_delimiters: { left: string; @@ -127,6 +128,7 @@ autoscroll={gradio.autoscroll} i18n={gradio.i18n} {...loading_status} + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} /> ; export let interactive: boolean; @@ -53,6 +54,7 @@ autoscroll={gradio.autoscroll} i18n={gradio.i18n} {...loading_status} + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} /> {#if multiselect} diff --git a/js/fallback/Index.svelte b/js/fallback/Index.svelte index af756468c8..129f056ae7 100644 --- a/js/fallback/Index.svelte +++ b/js/fallback/Index.svelte @@ -19,6 +19,7 @@ change: never; select: SelectData; input: never; + clear_status: LoadingStatus; }>; @@ -28,6 +29,7 @@ autoscroll={gradio.autoscroll} i18n={gradio.i18n} {...loading_status} + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} /> {/if} diff --git a/js/file/Index.svelte b/js/file/Index.svelte index 5ecd3838b5..f54acb4c72 100644 --- a/js/file/Index.svelte +++ b/js/file/Index.svelte @@ -39,6 +39,7 @@ upload: never; clear: never; select: SelectData; + clear_status: LoadingStatus; }>; export let file_count: string; export let file_types: string[] = ["file"]; @@ -72,6 +73,7 @@ status={pending_upload ? "generating" : loading_status?.status || "complete"} + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} /> {#if !interactive} { value = detail; }} @@ -100,6 +103,11 @@ on:clear={() => gradio.dispatch("clear")} on:select={({ detail }) => gradio.dispatch("select", detail)} on:upload={() => gradio.dispatch("upload")} + on:error={({ detail }) => { + loading_status = loading_status || {}; + loading_status.status = "error"; + gradio.dispatch("error", detail); + }} i18n={gradio.i18n} > diff --git a/js/file/shared/FileUpload.svelte b/js/file/shared/FileUpload.svelte index cb650160a2..8430f31868 100644 --- a/js/file/shared/FileUpload.svelte +++ b/js/file/shared/FileUpload.svelte @@ -18,6 +18,7 @@ export let root: string; export let height: number | undefined = undefined; export let i18n: I18nFormatter; + export let max_file_size: number | null = null; async function handle_upload({ detail @@ -62,8 +63,10 @@ on:load={handle_upload} filetype={file_types} {file_count} + {max_file_size} {root} bind:dragging + on:error > diff --git a/js/fileexplorer/Index.svelte b/js/fileexplorer/Index.svelte index 50790fed3a..147fbb706e 100644 --- a/js/fileexplorer/Index.svelte +++ b/js/fileexplorer/Index.svelte @@ -31,6 +31,7 @@ export let min_width: number | undefined = undefined; export let gradio: Gradio<{ change: never; + clear_status: LoadingStatus; }>; export let server: { ls: (path: string[]) => Promise; @@ -62,6 +63,7 @@ {...loading_status} autoscroll={gradio.autoscroll} i18n={gradio.i18n} + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} /> ; + clear_status: LoadingStatus; }>; const dispatch = createEventDispatcher(); @@ -65,12 +66,14 @@ autoscroll={gradio.autoscroll} i18n={gradio.i18n} {...loading_status} + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} /> {#if interactive && no_value} ({ image: x, caption: null })); gradio.dispatch("upload", value); }} + on:error={({ detail }) => { + loading_status = loading_status || {}; + loading_status.status = "error"; + gradio.dispatch("error", detail); + }} > diff --git a/js/highlightedtext/HighlightedText.stories.svelte b/js/highlightedtext/HighlightedText.stories.svelte index cfcefac34e..1170124428 100644 --- a/js/highlightedtext/HighlightedText.stories.svelte +++ b/js/highlightedtext/HighlightedText.stories.svelte @@ -13,14 +13,6 @@ { token: "dogs", class_or_confidence: "-" }, { token: "elephants", class_or_confidence: "+" } ]} - gradio={new Gradio( - 0, - document.body, - "light", - "1.1.1", - "http://localhost:7860", - false - )} {...args} /> diff --git a/js/highlightedtext/Index.svelte b/js/highlightedtext/Index.svelte index fc36e45852..d6386b7a16 100644 --- a/js/highlightedtext/Index.svelte +++ b/js/highlightedtext/Index.svelte @@ -16,6 +16,7 @@ export let gradio: Gradio<{ select: SelectData; change: never; + clear_status: LoadingStatus; }>; export let elem_id = ""; export let elem_classes: string[] = []; @@ -69,6 +70,7 @@ autoscroll={gradio.autoscroll} i18n={gradio.i18n} {...loading_status} + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} /> {#if label} gradio.dispatch("clear_status", loading_status)} /> {#if label} ; $: label, gradio.dispatch("change"); @@ -24,6 +25,7 @@ i18n={gradio.i18n} {...loading_status} variant="center" + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} />
; $: { @@ -125,6 +126,7 @@ autoscroll={gradio.autoscroll} i18n={gradio.i18n} {...loading_status} + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} /> {#if active_source === "upload" || !active_source} diff --git a/js/image/shared/ImageUploader.svelte b/js/image/shared/ImageUploader.svelte index 3cba6add8e..28ac44a48d 100644 --- a/js/image/shared/ImageUploader.svelte +++ b/js/image/shared/ImageUploader.svelte @@ -25,6 +25,7 @@ export let selectable = false; export let root: string; export let i18n: I18nFormatter; + export let max_file_size: number | null = null; let upload: Upload; let uploading = false; @@ -114,6 +115,7 @@ on:load={handle_upload} on:error {root} + {max_file_size} disable_click={!sources.includes("upload")} > {#if value === null} diff --git a/js/imageeditor/Index.svelte b/js/imageeditor/Index.svelte index 8d1158e2e9..b1b251c76c 100644 --- a/js/imageeditor/Index.svelte +++ b/js/imageeditor/Index.svelte @@ -66,6 +66,7 @@ clear: never; select: SelectData; share: ShareData; + clear_status: LoadingStatus; }>; let editor_instance: InteractiveImageEditor; @@ -136,6 +137,7 @@ autoscroll={gradio.autoscroll} i18n={gradio.i18n} {...loading_status} + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} /> gradio.dispatch("select", detail)} @@ -169,6 +171,7 @@ autoscroll={gradio.autoscroll} i18n={gradio.i18n} {...loading_status} + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} /> ; $: { @@ -58,6 +59,7 @@ autoscroll={gradio.autoscroll} i18n={gradio.i18n} {...loading_status} + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} /> diff --git a/js/label/Index.svelte b/js/label/Index.svelte index 298db49a2d..f23a67ad2c 100644 --- a/js/label/Index.svelte +++ b/js/label/Index.svelte @@ -13,6 +13,7 @@ export let gradio: Gradio<{ change: never; select: SelectData; + clear_status: LoadingStatus; }>; export let elem_id = ""; export let elem_classes: string[] = []; @@ -48,6 +49,7 @@ autoscroll={gradio.autoscroll} i18n={gradio.i18n} {...loading_status} + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} /> {#if show_label} diff --git a/js/markdown/Index.svelte b/js/markdown/Index.svelte index 9e71dc3160..78895470b2 100644 --- a/js/markdown/Index.svelte +++ b/js/markdown/Index.svelte @@ -23,6 +23,7 @@ export let line_breaks = false; export let gradio: Gradio<{ change: never; + clear_status: LoadingStatus; }>; export let latex_delimiters: { left: string; @@ -46,6 +47,7 @@ i18n={gradio.i18n} {...loading_status} variant="center" + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} />
gradio.dispatch("clear_status", loading_status)} /> {#if value} @@ -97,6 +98,7 @@ autoscroll={gradio.autoscroll} i18n={gradio.i18n} {...loading_status} + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} /> { + loading_status = loading_status || {}; + loading_status.status = "error"; + gradio.dispatch("error", detail); + }} i18n={gradio.i18n} + max_file_size={gradio.max_file_size} > diff --git a/js/model3D/shared/Model3DUpload.svelte b/js/model3D/shared/Model3DUpload.svelte index 1eb2a5d331..12ab38ce8d 100644 --- a/js/model3D/shared/Model3DUpload.svelte +++ b/js/model3D/shared/Model3DUpload.svelte @@ -16,6 +16,7 @@ export let i18n: I18nFormatter; export let zoom_speed = 1; export let pan_speed = 1; + export let max_file_size: number | null = null; // alpha, beta, radius export let camera_position: [number | null, number | null, number | null] = [ @@ -87,8 +88,10 @@ diff --git a/js/multimodaltextbox/Index.svelte b/js/multimodaltextbox/Index.svelte index 54df76e39d..893066a214 100644 --- a/js/multimodaltextbox/Index.svelte +++ b/js/multimodaltextbox/Index.svelte @@ -20,6 +20,8 @@ select: SelectData; input: never; focus: never; + error: string; + clear_status: LoadingStatus; }>; export let elem_id = ""; export let elem_classes: string[] = []; @@ -63,6 +65,7 @@ autoscroll={gradio.autoscroll} i18n={gradio.i18n} {...loading_status} + on:clear_status={() => gradio.dispatch("clear_status", loading_status)} /> {/if} @@ -83,12 +86,16 @@ {autofocus} {container} {autoscroll} + max_file_size={gradio.max_file_size} on:change={() => gradio.dispatch("change", value)} on:input={() => gradio.dispatch("input")} on:submit={() => gradio.dispatch("submit")} on:blur={() => gradio.dispatch("blur")} on:select={(e) => gradio.dispatch("select", e.detail)} on:focus={() => gradio.dispatch("focus")} + on:error={({ detail }) => { + gradio.dispatch("error", detail); + }} disabled={!interactive} /> diff --git a/js/multimodaltextbox/shared/MultimodalTextbox.svelte b/js/multimodaltextbox/shared/MultimodalTextbox.svelte index f50cf98237..7207bbbbe1 100644 --- a/js/multimodaltextbox/shared/MultimodalTextbox.svelte +++ b/js/multimodaltextbox/shared/MultimodalTextbox.svelte @@ -34,6 +34,7 @@ export let autoscroll = true; export let root: string; export let file_types: string[] | null = null; + export let max_file_size: number | null = null; let upload_component: Upload; let hidden_upload: HTMLInputElement; @@ -176,6 +177,7 @@ function handle_upload_click(): void { if (hidden_upload) { + hidden_upload.value = ""; hidden_upload.click(); } } @@ -206,11 +208,13 @@ on:load={handle_upload} filetype={accept_file_types} {root} + {max_file_size} bind:dragging bind:uploading show_progress={false} disable_click={true} bind:hidden_upload + on:error > {#if submit_btn !== null}