HuggingFace这阵子被我玩成了个人专属的“免费图床+网盘”,存储配额直接用到100G级别,图片上传后能拿到一条稳定外链,大文件则通过一行命令直传仓库。整套东西跑在HuggingFace自家免费提供的Gradio应用里,不需要自己买服务器。这篇文章把思路、前置准备、核心代码、大文件处理技巧和踩坑记录全部拆开讲,适合平时需要给博客配图、想低成本搭一个分享入口、或者经常给同事传但又不想开会员的人参考。
1. 思路拆解:为什么把HuggingFace当成存储用
1.1 100G免费额度到底藏在哪
很多人在HuggingFace上下模型、跑推理,却忽略了它另一个非常实在的能力:面向账号开放的Git LFS大文件存储。模型仓库和数据集仓库里的文件,一旦超过普通Git能承受的体积,就会被自动移交到LFS存储,这部分容量官方按账号发放免费额度,日常个人使用基本碰不到天花板。
这个额度比主流代码托管平台的免费仓库空间大得多,而且它不限制你存什么内容。你可以创建一个“模型”类型的仓库,但里面不放模型,只放图片、压缩包、日志、安装包。这就像拿着一个模型仓库的外壳,干网盘的活。HuggingFace并不会检查你仓库里必须真的有模型权重,它只关注文件是否合理存在。
除了额度大,HuggingFace对外链的友好程度也高。仓库里任何一个文件都可以直接拼出https://huggingface.co/用户名/仓库名/resolve/main/文件名这样的直链。图片放到博客、论坛、评论里能正常加载;文件放到聊天工具里能被直接下载。既支持预览又支持下载,这对图床场景来说非常关键。
1.2 图床+网盘的组合架构
整套系统用两个组件拼起来:
- 一个HuggingFace Space,运行Gradio应用,负责上传操作、文件管理、生成链接。免费CPU档即可,不用给钱。
- 一个HuggingFace仓库,真正存放所有文件,数据占用的是账号的LFS存储配额,不占用Space的磁盘。
网页上传的链路是:用户选文件→Gradio把文件临时收进Space→后端调用huggingface_hub库把文件推送到指定仓库→拿到直链显示在页面中。小文件走这一条,几十上百MB都没问题。
大文件的链路会绕开Space:直接在本机用Python脚本或命令行工具把文件上传到仓库,再回到Gradio页面复制直链。因为大文件经过Space中转很容易碰到请求超时和临时磁盘上限,绕开之后反而更稳。
这种分工的核心优势是“存储”和“操作界面”解耦。界面可以随时换,文件始终稳定躺在仓库里,直链不会因为界面改版而失效。而且HuggingFace对文件内容的响应做了CDN加速,国内访问速度虽然不如专业图床,但个人博客和文档配图完全够用。
1.3 最适合哪些场景
我的实际使用场景主要有几个:
- 给Markdown博客配图。写技术文章时截图、示意图很多,扔到图床上拿外链,比塞进仓库再等构建方便得多。
- 临时分享文件。几十MB的日志包、测试报告、安装包,生成直链丢给同事,对方直接下载,不用经过第三方网盘审核。
- 做项目素材中转。模型输出的样例图、数据集切片,存一份到仓库,再从任何机器拉取。
不适合的场景也要提前说:敏感内容、违法内容、以及需要强权限管控的团队文件不适合放这里。HuggingFace是公共平台,仓库虽然可以设私有,但本质上它面向的是公开共享,不该把隐私数据托付给它。
2. 准备阶段:账号、仓库、Token和Space
2.1 注册HuggingFace账号
注册过程不复杂,打开HuggingFace官网,点右侧的Sign Up,支持邮箱注册。验证邮件之后会跳到个人主页。注意用户名会直接出现在后续仓库链接里,选择一个你不打算频繁改的名字。
注册完之后顺手看一眼右上角头像里的Settings,进去确认邮箱已验证。有些操作会依赖邮箱可信度,虽然创建一个公开仓库不要求验证,但创建Space时偶尔会遇到限制。
2.2 创建一个存储专用仓库
在HuggingFace页面右上角点击“New”,选择Create New Repository。
仓库类型的表单里有几个选项,我建议选Model。Model仓库的链接最短,直链看起来最干净:https://huggingface.co/用户名/仓库名/resolve/main/图片.png。可能有人觉得用Dataset仓库更“名正言顺”,但Dataset的直链多一段/datasets/前缀,没有Model类型清爽。
仓库名建议用小写英文加连字符,比如my-file-bucket。可见性方面,如果只想自己用,选Private,别人无法直接查看仓库文件列表,但你有直链时对方依然能访问公开文件;至于这一点,后面避坑部分会单独说。如果希望链接能被任何人直接打开并被搜索引擎收录,选Public。
创建完成后,仓库页面会显示空目录和README模型卡,不用管它,我们所有操作都通过API进行。
2.3 拿到有写权限的访问令牌
要让Space里的应用能向仓库写文件,必须有令牌。进入Settings,找到Access Tokens,点击New Token给一个名字,勾选写权限(write)。旧版本的Token生成后会给你一整串以hf_开头的字符串,这个字符串只会完整显示一次,必须立刻复制保存。
Token本质上就是仓库的钥匙,泄露风险不亚于泄露密码。我在文章里所有示例都会用hf_xxxx占位,真实部署时你要替换成自己的。如果Token只存在于个人仓库环境变量里,风险还可控,千万不要把它硬编码到前端页面。
除了网页Token,HuggingFace还支持在Space创建时自动注入一个与用户权限挂钩的Token,这一点会在2.4小节讲到,也是让部署路径变短的关键。
2.4 创建Gradio Space并配置权限
点击右上角New,这次选择Create New Space。Space是HuggingFace给开发者跑小型Web应用的容器环境,免费档提供CPU计算资源和自动化休眠逻辑。
表单需要配置:
- Space名称:例如
my-file-manager - License:选Apache或MIT都随意
- SDK:选Gradio
- Hardware:选CPU basic
- Space visibility:Private代表只有你能访问;Public代表任何人打开链接就能使用
- Advanced configuration里的“访问令牌权限”字段,选择Write
这里有一个非常关键的理解点:Space可以申请一个系统级Token,这是HuggingFace在创建Space时自动生成的,它的权限等同于该用户的Token,作用域受你在配置时选择的权限限制。选Write之后,Space里运行的代码就可以读写你账号下的仓库,而不需要在代码里手动把hf_令牌塞进环境变量。
如果建完Space之后权限忘了选Write,也不要紧,Space的Settings里随时可以改,改完重启Space生效。
3. 核心代码:一个能传能管能出外链的Gradio应用
3.1 项目文件与依赖
整个应用只需要三个文件,结构非常精简:
| 文件 | 用途 |
|---|---|
app.py | Gradio应用主体,包含上传、列表、删除、外链生成逻辑 |
requirements.txt | 声明运行时依赖 |
README.md | 项目说明,非必须但建议保留 |
requirements.txt内容:
gradio>=4.0 huggingface_hub>=0.23.0app.py开头长这样:
import os import time import gradio as gr from huggingface_hub import HfApi REPO_ID = os.getenv("REPO_ID", "yourname/my-file-bucket") REPO_TYPE = os.getenv("REPO_TYPE", "model") # model 或 dataset api = HfApi()REPO_ID是仓库的全名,格式是用户名/仓库名。我在代码里用环境变量读取它,而不是写死,这样以后换仓库不用改代码,只需要在Space的环境变量里调整。
3.2 上传文件并生成Markdown外链
Gradio的gr.Files组件可以让用户一次选择多个文件,选择后回调函数会收到一组本地路径。在后端用HfApi().upload_file把文件推送到目标仓库,然后拼接直链。
def _file_url(remote_path): if REPO_TYPE == "dataset": base = f"https://huggingface.co/datasets/{REPO_ID}" else: base = f"https://huggingface.co/{REPO_ID}" return f"{base}/resolve/main/{remote_path}" def _normalize_path(file_obj): if hasattr(file_obj, "name"): return file_obj.name return str(file_obj) def upload_files(file_objs): if not file_objs: return "请选择文件后再上传", [], "" logs = [] rows = [] md_lines = [] for file_obj in file_objs: local_path = _normalize_path(file_obj) filename = os.path.basename(local_path) remote_name = f"{time.strftime('%Y%m%d%H%M%S')}_{filename}" try: api.upload_file( path_or_fileobj=local_path, path_in_repo=remote_name, repo_id=REPO_ID, repo_type=REPO_TYPE, ) url = _file_url(remote_name) rows.append([remote_name, url]) md_lines.append(f"") logs.append(f"成功:{remote_name}") except Exception as e: logs.append(f"失败:{filename},原因:{e}") return "\n".join(logs), rows, "\n\n".join(md_lines)代码里几个细节值得单独解释。
第一,为什么文件名要加时间戳前缀。如果直接使用原始文件名上传,第二次上传同名文件时会覆盖旧文件,导致旧的直链瞬间404。这在图床场景里是灾难。加时间戳之后,每次上传都生成独立文件,历史链接永久有效。
第二,为什么返回三样东西。第一个是多行上传日志,方便用户看到哪张成功哪张失败;第二个是外链数据表,用于复制链接;第三个是Markdown格式的图片预览,直接把这一串粘贴到博客,图片立即显示。
第三,为什么用hasattr(file_obj, "name")做兼容。不同版本的Gradio对文件组件的返回类型有差异,有的给临时文件路径字符串,有的给临时文件包装对象。统一处理一遍能减少环境差异带来的问题。
3.3 列出仓库文件与删除文件
图床用久了就会堆积大量图片,管理页面必须有文件列表和删除能力。
def list_repo_files(): try: files = api.list_repo_files(repo_id=REPO_ID, repo_type=REPO_TYPE) if not files: return [["(仓库为空)"]] return [[f] for f in files[:200]] except Exception as e: return [[f"读取失败:{e}"]] def delete_file(filename): if not filename: return "请输入要删除的文件名" try: api.delete_file( path_in_repo=filename, repo_id=REPO_ID, repo_type=REPO_TYPE, commit_message=f"delete {filename}", ) return f"已删除:{filename}" except Exception as e: return f"删除失败:{e}"list_repo_files返回仓库根目录下所有文件名。仓库为空时返回一条占位提示。文件数量特别多时,我只取前200条展示,避免Gradio的表格组件被几千条数据拖垮。
删除文件时必须显式提供commit_message,否则部分版本会报参数缺失错误。删除操作本质上是创建一个“删除提交”,这里耐心点,网络波动时提交可能失败,重试一次即可。
3.4 Gradio界面主体与部署路径
把上面这些函数组装进Blocks,界面分三个标签页:上传、管理、大文件引导。
with gr.Blocks(title="HF File Host") as demo: gr.Markdown("## HF 免费图床 / 网盘") with gr.Tab("上传"): with gr.Row(): file_input = gr.Files(label="选择文件") upload_btn = gr.Button("上传", variant="primary") upload_status = gr.Textbox(label="上传日志", lines=3) link_table = gr.Dataframe( headers=["文件名", "外链"], label="上传结果", interactive=False, wrap=True, ) md_output = gr.Markdown(label="Markdown 预览") with gr.Tab("管理"): list_btn = gr.Button("刷新仓库文件列表") file_table = gr.Dataframe( headers=["仓库内文件名"], label="文件列表", interactive=False, ) with gr.Row(): delete_name = gr.Textbox(label="要删除的文件名") delete_btn = gr.Button("删除", variant="stop") delete_status = gr.Textbox(label="删除结果") with gr.Tab("大文件指南"): guide_btn = gr.Button("生成上传命令") guide_output = gr.Markdown(label="操作指引") upload_btn.click(upload_files, inputs=[file_input], outputs=[upload_status, link_table, md_output]) list_btn.click(list_repo_files, inputs=None, outputs=[file_table]) delete_btn.click(delete_file, inputs=[delete_name], outputs=[delete_status]) demo.launch()部署有三种途径。
第一种是直接在Space的Files页面拖拽上传三个文件,上传完成后停留几秒,Space会自动检测到app.py并安装依赖启动应用。这是最省事的方式,适合不熟悉命令行的读者。
第二种是使用Git推送。HuggingFace的Space本质上是一个Git仓库,你可以克隆Space仓库到本地,把代码放进去再推回。
第三种是在本机用huggingface_hub的upload_folder方法直接推送代码到Space仓库。三种方式最终效果一致,选哪种全看个人习惯。我通常用网页拖拽,因为改动频率低,不值得每次打开终端。
部署完成后,Space地址形如https://huggingface.co/spaces/用户名/my-file-manager,点进去就能看到完整的操作界面。
4. 大文件上传:绕开Space,直连仓库
4.1 网页上传的天然短板
不少读者看到网页能上传,就想着把几个G的压缩包直接拖进去。这样做基本会失败。
原因有三层:
- Space免费CPU实例的临时磁盘空间有限,Gradio上传大文件时会先写进临时目录,再推送到仓库。临时目录被写满时,后续操作直接报磁盘不足。
- Space上的Web请求有响应时间上限,上传大文件会跨过这个时限,前端主动断开。
- Space的CPU实例性能有限,尤其在做文件哈希、分块这类计算量较大的步骤时,耗时成倍放大。
结论是:网页上传适合图片、文档、安装包这类几十上百MB的文件。真正的大文件,交给本机脚本或命令行直传仓库,完全绕开Space,反而是最稳的。
4.2 用Python脚本直传大文件
HuggingFace官方库huggingface_hub对LFS上传做了完整封装,支持分块、断点重试、并发上传。这些底层细节不需要自己处理,只要调用高层API就行。
本机先装好依赖:
pip install -U huggingface_hub然后写一个最简单的上传脚本:
from huggingface_hub import HfApi, login login(token="hf_xxxx") api = HfApi() api.upload_file( path_or_fileobj="本地路径/大文件.zip", path_in_repo="files/大文件.zip", repo_id="yourname/my-file-bucket", )如果不想登录,也可以把令牌写到环境变量里:
export HF_TOKEN=hf_xxxx python upload.pyhuggingface_hub在上传时会把文件自动存入LFS,并对大文件启用分块上传机制。实测数GB级别的文件,只要网络稳定,都能安全送到仓库。上传完成之后,在浏览器打开仓库Files页面能看到新文件,直链也即刻生效:
https://huggingface.co/yourname/my-file-bucket/resolve/main/files/大文件.zip把这条链接发给别人,对方点击即可下载。如果想要浏览器强制下载而不是预览,在链接末尾加上?download=true。
除了Python脚本,HuggingFace也提供命令行工具:
huggingface-cli login --token hf_xxxx huggingface-cli upload yourname/my-file-bucket ./大文件.zip files/大文件.zip两种方式等价,看个人习惯。我偏爱Python脚本,因为可以顺便在脚本里做更多逻辑,比如上传前重命名、校验文件大小、多个文件批量循环。
4.3 把命令行引导集成进界面
既然大文件走命令行,那Gradio页面里第三张Tab就显得很重要:它负责生成一条针对当前仓库、当前Token的“命令引导”,让使用者不需要记任何API格式。
在app.py里加入引导逻辑:
def generate_guide(): guide_text = f""" ### 大文件上传引导 网页上传适合小文件,大文件建议在本机执行下面命令: ```bash pip install -U huggingface_hub huggingface-cli login --token <你的Token> huggingface-cli upload {REPO_ID} 本地大文件.zip files/大文件.zip上传后直链:
{_file_url('files/大文件.zip')}也可以改用Python脚本:
from huggingface_hub import HfApi, login login(token="<你的Token>") api = HfApi() api.upload_file( path_or_fileobj="本地大文件.zip", path_in_repo="files/大文件.zip", repo_id="{REPO_ID}", )""" return guide_text
这里给使用者看的Token应该是使用者自己的,不是Space环境变量里的系统Token。在公开使用场景下绝对不要把Space自动注入的Token打印在页面上,否则任何打开你Space的人都能拿到它。如果仅仅是自己访问,Token泄露风险相对可控,但依然不建议养成这种习惯。 ### 4.4 文件夹与批量文件的上传技巧 需要上传整个目录时,可以用`upload_folder`替代逐文件上传: ```python api.upload_folder( repo_id="yourname/my-file-bucket", folder_path="./本地目录", path_in_repo="assets", )这个函数会把整个目录递归上传,非常适合把大量图片素材一次性搬进网盘。上传之后,目录结构会完整保留在assets子目录下。
批量上传大量小文件时,也建议先打包成zip再传。HuggingFace对“文件数量”没有严格上限设置,但仓库里堆了上万个小文件会让列表页卡顿、每次list_repo_files延迟明显。打包传上去,需要哪个解压哪个,反而省心。
文件组织方面,建议在仓库里维护清晰的目录分支:
img/ 存放博客配图 files/ 存放临时分享的大文件 logs/ 存放运行日志 assets/ 存放项目静态资源直链中会带目录前缀,链接本身依然稳定。这样分类之后,以后清理文件也方便,直接在管理界面输入files/大文件.zip就能精确删除。
5. 常见问题与避坑速查
5.1 Space休眠与访问慢
免费CPU档的Space有一个很现实的问题:长时间没有用户访问,它会自动休眠。休眠后再次打开链接,需要等待Space重新启动,常见的重启时间是几十秒到几分钟不等。
这不是故障,而是免费资源配额机制。想缓解的话,可以偶尔打开一下页面保持活跃度。也有人写一个定时任务模拟访问,让Space不自眠,但高频请求同样消耗免费额度。我的建议是接受休眠,把Space当作一个“偶尔打开的管理后台”,而不是24小时在线的服务器。
如果对启动速度要求极高,可以考虑给Space升级付费硬件,但那就违背了“免费搭建”的初衷。
5.2 外链打不开或被迫下载
链接打不开,先确认仓库可见性和文件是否存在。仓库是Private状态时,直链仍能访问,因为直链本身就是带认证能力的,但某些第三方平台在抓取时携带不了凭据,就会拉取失败。
通用做法:给外部人分享时把仓库临时设为Public,或者改用公开仓库存文件。
浏览器打开图片URL却是下载而不是预览,这是HuggingFace对某些Content-Type的处理逻辑。图片通常能正常预览,非图片类型文件如zip、exe则会被强制下载。想强制下载就在URL末尾加?download=true,不加就保留预览行为。
还有一种情况是中文文件名、空格导致链接异常。虽然HuggingFace对UTF-8路径的兼容性在提升,但我仍然建议远端文件名统一用时间戳加英文,路径中的空格换成短横线。这样直链放到各个平台都不会出问题。
5.3 文件重名覆盖
上传时没有加时间戳,第二次上传同名文件会把老文件替换掉,老直链立即失效。老的图床链接在别人的文章里可能已经被引用,一旦失效就会裂图,这是图床场景里最隐蔽的坑。
我在代码里已经用时间戳规避了这个问题。如果你要覆盖更新某个固定文件,比如README、软件安装包,那就不加时间戳,保持相同的远端路径直接覆盖。
5.4 Token与权限踩坑
上报“401 Unauthorized”或“409 Conflict”时,大概率是Token没有写权限,或者Token不属于仓库所有者。检查三处:
- Token创建时是否勾选了write权限
- Space创建时的访问令牌权限是否设置为Write
- 代码里是否误用了别人账号的Token
仓库所有者和Space所有者必须是同一个账号,或者Token配置了跨仓库写权限。最稳妥的方式就是让Space和仓库都在同一个账号下创建,Token自动适配。
5.5 使用边界与合理用量
HuggingFace给的100G额度是免费存储空间,但它不是网盘服务商,不会承诺无限流量或超高的并发吞吐。把直链扔到流量极大的论坛首页、频繁被脚本拉取,可能导致HuggingFace对相关域名或IP做限流。
我的经验是,日常写博客、发文档、私聊传文件,流量不大,完全没问题。但如果拿它当视频床、刷量下载站,风险极大,这种做法也不值得鼓励。
另外,仓库虽然是自己的,但平台终究允许自查。涉及违规、色情、暴力等内容,绝对不要往上传。保护自己的账号,也保护这个免费资源的可用性。
使用一段时间之后,会在管理界面留下大量临时文件。我的习惯是每隔一段时间删掉已经不用的分享包和过期日志,只保留博客配图等长期内容。删除操作在管理页面就能完成,几秒钟的事。
这个方案的灵活度其实比很多人想象的大。把Gradio界面换成Streamlit、加一个带标签的分类系统、给文件做关键词检索,都是顺手的事。存储层不变、直链不变,只要界面代码调整就能持续升级。免费大仓库加上一个自己操控的前台,这套组合用来解决日常文件分享问题,真的很够用。