Gradio 前端文件上传体系详解:@gradio/upload 模块的组件、工具函数与源码实现
【免费下载链接】gradioBuild and share delightful machine learning apps, all in Python. 🌟 Star to support our work!项目地址: https://gitcode.com/GitHub_Trending/gr/gradio
导读
@gradio/upload是 Gradio 前端(Svelte)中负责"文件上传"这一核心交互的基础模块:它向上层组件提供可拖拽、可点击、可粘贴剪贴板的文件选择能力,向底层衔接@gradio/client的 HTTP 上传通道,并在上传期间渲染实时进度。本文以 js/upload/README.md 为骨架,结合 js/upload/src 的完整源码与 js/upload/src/utils.test.ts 测试用例,逐层拆解Upload、ModifyUpload、UploadProgress三个组件和normalise_file、get_fetchable_url_or_file、upload、prepare_files四个工具函数,帮助你理解 Gradio 所有文件类组件(Image、Audio、Video、File、Gallery 等)上传能力的统一底座,并能在自定义 Svelte 组件中直接复用这套 API。
一、模块总览:一个包,三组件,四个核心函数
@gradio/upload位于仓库 js/upload,其入口 js/upload/src/index.ts 导出内容如下:
export { default as Upload } from "./Upload.svelte"; export { default as ModifyUpload } from "./ModifyUpload.svelte"; export { default as UploadProgress } from "./UploadProgress.svelte"; export { create_drag, is_valid_mimetype, to_accept_attribute } from "./utils";也就是说,这个包同时承担两种职责:
- 交互组件:
Upload(上传区/拖拽区)、ModifyUpload(已上传文件的操作工具条)、UploadProgress(上传进度展示); - 工具函数:
normalise_file、get_fetchable_url_or_file、upload、prepare_files(README 中声明的四个导出),以及从utils.ts再导出的create_drag、is_valid_mimetype、to_accept_attribute。
从使用范围看,@gradio/upload是 Gradio 前端的事实公共依赖:仓库中至少有 14 个组件包直接引入它,例如 js/image/shared/ImageUploader.svelte、js/audio/interactive/InteractiveAudio.svelte、js/video/shared/InteractiveVideo.svelte、js/gallery/shared/Gallery.svelte、js/model3D/shared/Model3DUpload.svelte、js/dataframe/shared/Table.svelte 等。可以推断:任何需要"让用户把本地文件送进 Gradio 后端"的组件,都建立在Upload之上。
包的元信息见 js/upload/package.json:版本0.18.2,声明"type": "module",依赖@gradio/atoms、@gradio/icons、@gradio/client、@gradio/utils四个工作区包,并声明 Svelte 5(^5.48.0)为 peer dependency——这与源码中大量使用 Svelte 5 的$props()、$state()、$bindable()语法一致。
二、Upload 组件:从"点击/拖拽/粘贴"到文件上传
Upload是上传交互的入口组件,完整实现位于 js/upload/src/Upload.svelte。README 中给出了它的 Props 清单,结合源码可整理出下表(含默认值与源码中的类型约束):
| Props | 类型 | 默认值 | 源码说明 |
|---|---|---|---|
filetype | string \| string[] \| null | null | 允许的文件类型,null表示不限制 |
dragging | boolean($bindable) | false | 是否处于拖拽悬停状态,由外部绑定 |
boundedheight | boolean | true | 限制上传区高度 |
center | boolean | true | 内容是否居中 |
flex | boolean | true | 使用 flex 布局排列内容 |
file_count | "single" \| "multiple" \| "directory" | "single" | 单选/多选/目录上传 |
disable_click | boolean | false | 是否禁用点击打开文件选择器 |
root | string | 无 | 后端服务根地址,必填 |
hidden | boolean | false | 是否隐藏整个上传区 |
除 README 列出的这些外,源码还暴露了更多内部 Props,如format("blob" \| "file",默认"file")、uploading、show_progress(默认true)、max_file_size(默认null,即不限制)、upload(类型为Client["upload"])、stream_handler(Client["stream"])、icon_upload、height、aria_label、upload_promise、onload、onerror,以及 Svelte 5 的childrensnippet。
2.1 三种交互入口:点击、拖拽、剪贴板粘贴
Upload组件的模板在 Upload.svelte 第 270-322 行分三种形态渲染:
- 剪贴板粘贴(
filetype === "clipboard"时):渲染一个按钮,点击后调用paste_clipboard(),通过navigator.clipboard.read()读取剪贴板,提取第一个image/*类型的数据,构造File后交给load_files。文件名形如clipboard.png。 - 上传中(
uploading && show_progress):渲染UploadProgress进度组件,需要root、upload_id、file_data与stream_handler四个 Props。 - 默认上传区:渲染一个
<button>,其上通过 Svelte actionuse:drag挂载拖拽与点击逻辑,并将accepted_types、mode(即file_count)、disable_click传入;同时支持childrensnippet 自定义内部内容(比如图标与提示文案)。
2.2 filetype 的规范化处理
filetype不是直接透传给<input accept>的,而是要经过process_file_type规范化(Upload.svelte 第 77-93 行):
const validFileTypes = ["image", "video", "audio", "text", "file"]; const process_file_type = (type: string): string => { if (ios && type.startsWith(".")) { use_post_upload_validation = true; // iOS 上启用上传后校验 return type; } if (ios && type.includes("file/*")) { return "*"; // iOS 无法处理 file/* 通配 } if (type.startsWith(".") || type.endsWith("/*")) { return type; // ".png" 或 "image/*" 原样保留 } if (validFileTypes.includes(type)) { return type + "/*"; // "image" -> "image/*" } return "." + type; // "png" -> ".png" };这里体现了两个重要的平台兼容细节(源码第 67-75 行的get_ios通过navigator.userAgent判断 iPhone/iPad):
- iOS 上,扩展名类型的文件在浏览器端无法可靠过滤,于是标记
use_post_upload_validation = true,在load_files阶段用is_valid_file做二次校验,非法文件触发onerror("Invalid file type: ... Only ... allowed."); - iOS 上
file/*通配被替换为"*",避免系统文件选择器解析失败。
is_valid_file(第 198-221 行)的校验逻辑是:扩展名类型比对file.name后缀(不区分大小写),*/*通配直接放行,category/*比对file.type前缀,否则精确比对 MIME 类型。
2.3 上传主流程:prepare_files -> upload -> onload
当文件经过过滤后,进入核心链路(load_files/load_files_from_upload,Upload.svelte 第 168-249 行):
- 用
prepare_files(files)把File[]包装成FileData[](携带path、orig_name、blob、size、mime_type); - 调用
handle_upload,先生成upload_id(Math.random().toString(36).substring(2, 15),第 135 行),置uploading = true,并把上传包装成一个 Promise 赋给可绑定的upload_promise; - 调用
upload(file_data, root, upload_id, max_file_size ?? Infinity)真正上传; file_count === "single"时取_file_data[0]回调onload,否则回调整个数组;异常时走onerror并resolve([]),避免 Promise 悬挂。
值得注意的是format = "blob"模式(第 240-249 行、258-267 行):此时不发起上传,直接把File(或File[])原样交给onload,供仅需本地预览的场景(如不落盘的文件组件)使用。
三、ModifyUpload 组件:编辑、撤销、下载、清空
ModifyUpload是已上传文件上的操作工具条,README 声明的 Props 与源码 js/upload/src/ModifyUpload.svelte 一致:
export let editable = false; // 显示"编辑"按钮 export let undoable = false; // 显示"撤销"按钮 export let absolute = true; // 定位方式(源码中保留为样式定位) export let i18n: I18nFormatter; // 国际化格式化器,必填它在IconButtonWrapper内按条件渲染四种按钮(源码第 28-61 行):
editable时渲染Edit图标按钮,点击触发onedit?.();undoable时渲染Undo图标按钮,触发onundo?.();- 传入
download字符串时,用DownloadLink(来自@gradio/atoms)包裹Download图标按钮,实现download属性的文件下载; - 始终渲染
Clear(清空)按钮,触发onclear?.(),并stopPropagation防止冒泡到上层点击事件。
图标来自@gradio/icons,文案("common.edit"、"common.undo"、"common.download"、"common.clear")通过i18n格式化——这就是i18n为必填 Props 的原因。例如在 js/imageeditor/shared/ImageEditor.svelte、js/video/shared/VideoControls.svelte 中,ModifyUpload被用来为已上传的图片/视频提供编辑与撤销入口。
四、UploadProgress 组件:基于 SSE 的实时上传进度
UploadProgress组件(js/upload/src/UploadProgress.svelte)负责在Upload上传期间展示进度。它接收upload_id、root、files、stream_handler、ondone五个 Props,进度数据来源不是轮询,而是SSE(Server-Sent Events)流:
const upload_progress_url = resolve_current_origin_url( root, `/gradio_api/upload_progress?upload_id=${upload_id}` ); stream = await stream_handler(upload_progress_url);resolve_current_origin_url来自@gradio/utils,将相对路径与root拼接为同源地址。后端对应的 SSE 端点定义在 gradio/routes.py 第 1789 行(GET /upload_progress,带login_check依赖)以及 gradio/static_server.py 第 118-120 行(同时暴露/gradio_api/upload_progress与/upload_progress)。从 gradio/routes.py 第 1789-1819 行的实现可以看到消息协议:
- 每 0.05 秒检查一次(
check_rate = 0.05),心跳间隔 15 秒; - 上传完成后发送
{"msg": "done"}; - 否则发送
{"msg": "update", "orig_name": ..., "chunk_size": ...}。
前端在onmessage中(UploadProgress.svelte 第 60-71 行)解析 JSON:msg === "done"时关闭流并调用ondone,否则把orig_name与chunk_size累加到对应文件的progress字段。组件通过 CSS 变量--upload-progress-width驱动进度条与圆环(conic-gradient)的宽度,视觉上同时展示"正在上传第 N 个文件"与整体进度。onDestroy中确保流被关闭,避免连接泄漏。
五、四个核心工具函数解读
README 以类型声明形式给出了normalise_file、get_fetchable_url_or_file、upload、prepare_files四个函数,下面结合源码逐一解读。
5.1 prepare_files:File[] -> FileData[]
export async function prepare_files( files: File[], is_stream?: boolean ): Promise<FileData[]> { return files.map( (f, i) => new FileData({ path: f.name, orig_name: f.name, blob: f, size: f.size, mime_type: f.type, is_stream }) ); }该函数把浏览器File对象包装成 Gradio 的FileData结构(README 中该实现与 client/js/src/upload.ts 第 53-68 行的prepare_files完全一致)。FileData类同样定义在 client/js/src/upload.ts 第 70-112 行,其meta字段固定为{ _type: "gradio.FileData" },这是前后端识别文件数据结构的标志;blob字段在构造时若传入了url则会被置为undefined,表示该文件已经存在于服务器、无需再次上传。
5.2 upload:真正把文件送到后端
export async function upload( file_data: FileData[], root: string, upload_fn: typeof upload_files = upload_files ): Promise<(FileData | null)[] | null>默认实现即@gradio/client的 client/js/src/upload.ts 第 3-51 行:
- 从
FileData中取出blob; - 用
max_file_size ?? Infinity做大小校验,超限直接抛出带filesize()格式化提示的错误; - 调用
this.upload_files(root_url, files, upload_id),其底层实现(client/js/src/utils/upload_files.ts 第 5-52 行)按每批 1000 个文件(chunkSize = 1000)构造FormData,POST 到${root}${api_prefix}/upload?upload_id=...;若配置了 token,会附带Authorization: Bearer ...请求头(第 14-16 行);响应非 2xx 时返回{ error: "HTTP ...: ..." }; - 成功后把返回的文件路径编码为完整 URL:
${root}${api_prefix}/file=${encoded_path}(每个路径段经encodeURIComponent),回填为FileData.url,并将原始FileData的其余字段合并。
upload函数在Upload组件中作为 Props 注入,可替换实现;UploadButton(js/uploadbutton/shared/UploadButton.svelte 第 73-105 行)展示了同样的调用模式:prepare_files后upload(all_file_data, root, undefined, max_file_size ?? Infinity),再按单/多选分发onchange/onupload回调。
5.3 normalise_file 与 get_fetchable_url_or_file:文件地址的"归一化"
export function normalise_file( file: FileData | null, server_url: string, proxy_url: string | null ): FileData | null; export function normalise_file( file: FileData[] | null, server_url: string, proxy_url: string | null ): FileData[] | null; export function normalise_file( file: FileData[] | FileData | null, server_url: string, // root proxy_url: string | null // root_url ): FileData[] | FileData | null; export function get_fetchable_url_or_file( path: string | null, server_url: string, proxy_url: string | null ): string从 README 的类型声明(含注释// root: string、// root_url: string | null)可以推断:server_url参数实际对应前端上下文中的root(应用根地址),proxy_url对应代理后的root_url。这类"归一化"函数的目的是:在 SSR、代理部署、不同源加载等场景下,把后端返回的裸文件路径统一换算成当前页面可访问的 URL,或将已存在于服务器上的FileData补齐url字段。需要说明的是,当前仓库快照中并未找到这两个函数在js/upload/src下的具体实现——其在各组件包 CHANGELOG(如 js/button/CHANGELOG.md 中 #7528 条目)中被记录为"将get_fetchable_url_or_file()从前端移除并重构",因此这里只能依据 README 的类型签名说明其契约:输入一个(或一组)FileData或裸路径,输出带可访问地址的FileData/ 字符串。
5.4 附属工具:create_drag、is_valid_mimetype、to_accept_attribute
README 的导入语句中虽未列出,但入口 index.ts 还导出了三个utils.ts函数,它们是文件过滤与拖拽交互的基石。
is_valid_mimetype(js/upload/src/utils.ts 第 1-38 行)判断一个文件是否匹配accept规则:
null、"*"、"file/*"(含数组中含有这些值)直接放行;- 支持逗号分隔字符串或数组两种形式;
- 扩展名规则要求"真实后缀"匹配:文件名长度必须大于扩展名长度且
endsWith匹配(避免.js匹配my.js.txt),并且支持复合扩展名如.nii.gz、.tar.gz(因为endsWith天然匹配多段后缀); - MIME 规则支持
category/*通配,比对uploaded_file_type是否以category/开头; - 文件名比较不区分大小写。
to_accept_attribute(第 40-58 行)把规则转换成原生<input accept>属性:核心技巧是扩展名加宽——对复合扩展名.nii.gz额外补一个.gz,因为浏览器原生文件选择器只认最后一个点后的扩展名。测试 js/upload/src/utils.test.ts 中的用例可以佐证:to_accept_attribute([".nii.gz"])得到".nii.gz, .gz"。
create_drag(第 74-209 行)返回{ drag, open_file_upload }:drag是 Svelte action,内部创建隐藏的<input type="file">(带aria-label="File upload"与data-testid="file-upload",用于无障碍与测试定位),根据mode设置multiple/webkitdirectory/mozdirectory属性实现多选与目录上传;监听drag/dragstart/dragend/dragover(统一 preventDefault 阻止浏览器默认打开文件)、dragenter/dragleave(驱动on_drag_change以切换拖拽高亮)、drop(取出dataTransfer.files交给on_files)与click(打开文件选择器,支持ignore_click_selector忽略指定区域的点击,如避免点击工具条也触发上传);update时销毁并重建隐藏 input 以应用新选项,destroy时移除全部监听。open_file_upload则供外部以编程方式触发文件选择。
六、测试验证:utils 的正确性边界
js/upload/src/utils.test.ts 用 Vitest 覆盖了上述三个工具函数的关键边界,是理解语义的权威参考:
- is_valid_mimetype:复合扩展名匹配(
brain.nii.gz匹配.nii.gz,但不匹配.nii;src.tar.gz匹配.tar.gz);复合扩展名不阻断简单扩展名(brain.nii.gz仍匹配.gz);大小写不敏感(photo.PNG匹配.png);必须是真实后缀(my.js.txt不匹配.js);整个文件名等于扩展名时不匹配(.png、.nii.gz均不匹配);MIME 通配(image/*匹配image/png不匹配video/mp4);*与null全放行;逗号分隔字符串与数组等价。 - to_accept_attribute:复合扩展名加宽(
.nii.gz->.nii.gz, .gz);普通扩展名与 MIME 原样保留;已有.gz时不重复添加;null返回undefined(不设置 accept)。 - create_drag:点击被
ignore_click_selector覆盖的子元素(如.toolbar-wrap内的按钮)时不会触发文件选择器;点击拖拽区本身会触发一次HTMLInputElement.click。这两条用例同时验证了dragaction 的事件绑定与销毁(action.destroy())行为。
七、在自定义组件中复用的最佳实践
综合上文,在一个基于 Svelte 5 的自定义 Gradio 前端组件中接入上传能力,标准姿势如下:
<script lang="ts"> import { Upload, prepare_files, is_valid_mimetype } from "@gradio/upload"; import type { FileData, Client } from "@gradio/client"; let { root, upload, onchange }: { root: string; upload: Client["upload"]; onchange: (data: FileData | FileData[]) => void; } = $props(); async function handle_files(files: File[]): Promise<void> { const file_data = await prepare_files(files); const result = await upload(file_data, root); onchange(file_count === "single" ? result?.[0] : result); } </script> <Upload {root} {upload} filetype={["image", "video"]} file_count="multiple" onload={handle_files} > <!-- 自定义提示内容 --> </Upload>实践要点:
root与upload是必传的:upload通常来自@gradio/client的Client实例方法(Client["upload"]),由上层注入,组件自身不负责创建连接;- 用
prepare_files而非手工构造FileData,保证meta._type等约定字段齐全; - 类型过滤首选
filetype(内部自动处理 iOS 兼容与 MIME 通配),需要更精细控制时可复用is_valid_mimetype/to_accept_attribute; - 多文件上传不要超过底层
upload_files的每批 1000 个上限,超大文件集会被自动分批; - 展示进度时复用
UploadProgress,记得传入upload_id(Upload在upload_promise中暴露)与stream_handler,并确认后端暴露/gradio_api/upload_progressSSE 端点(gradio/routes.py 与 gradio/static_server.py 均已注册)。
八、小结
@gradio/upload是 Gradio 前端所有文件型组件共用的上传底座:Upload统一了点击/拖拽/剪贴板三种输入方式并对 iOS 做了专门适配,ModifyUpload提供已上传文件的操作工具条,UploadProgress通过 SSE 消费后端的upload_progress端点渲染实时进度;prepare_files/upload完成File -> FileData -> 后端存储 -> 可访问 URL的完整链路,normalise_file/get_fetchable_url_or_file负责跨部署形态的路径归一化,utils.ts中的类型过滤与拖拽工具则保证了交互的正确性与可测试性。理解这一层实现,无论是要排查上传问题、还是为 Gradio 编写新的自定义组件,都能做到有的放矢。
【免费下载链接】gradioBuild and share delightful machine learning apps, all in Python. 🌟 Star to support our work!项目地址: https://gitcode.com/GitHub_Trending/gr/gradio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考