3.2 上传实战:控制台、COSCMD 与 Python SDK
图片迁移这事,我前后换了三种上传方式,刚好对应项目三个阶段:刚开张时拿控制台手动传几张图验证链路;积累了几千张历史图之后改用 COSCMD 命令行批量搬;等社区正式运转,后端接入 Python SDK,用户上传从此不再经过我的服务器硬盘。三种方式没有高下之分,只有合不合场景。本篇把这三种都走一遍,包括我各自踩过的坑。
控制台上传:先验证链路
第一次接触 COS,建议从控制台开始。进入存储桶的文件列表页,把本地图片往里一拖,或者点上传按钮选文件,进度条走完就成功了。整个操作不超过两分钟,但它验证了一件重要的事:存储桶建好了、权限可用、对象确实能通过列表里的访问域名打开。
我最初就是靠控制台确认链路通的,传了两张测试图上去,在浏览器里打开对象 URL,看到图的那一刻心里就有底了。顺带把对象的详情页看一遍:上面列着对象大小、ETag、存储类型和最后修改时间,这些字段以后在代码里都用得上。
控制台适合零散的验证和临时补图,但有两个硬伤:一是上传大小和数量有限制,几百 MB 的原图传不上去;二是传完之后文件列表刷新慢得让人着急。几千张历史图用它传,人会先疯掉。
COSCMD:批量搬运利器
历史图片迁移我用的 COSCMD,一个官方的命令行工具。装好之后配置一次密钥和存储桶,之后所有操作都是一条命令:
pip install coscmd
cos config -a <SecretId> -s <SecretKey> -b pic-community-1250000000 -r ap-shanghai
配置好之后上传单个文件:
cos upload /data/pics/2026/01/cat.jpg pics/2026/01/cat.jpg
批量传整个目录加个参数就行,把本地图片目录整个推上去:
cos upload -rs /data/pics /pics/
这里面的参数含义:-r 表示递归上传目录,-s 表示同步模式,也就是跳过远端已经存在的同名对象。迁移几千张图的时候这两条参数帮我省了大量重复上传的时间。
除了上传,COSCMD 常用的还有几条,日常运维翻来覆去就是它们:
cos list pics/2026/10/ # 列举目录下的对象
cos download pics/2026/10/cat.jpg /tmp/cat.jpg # 下载单个对象
cos delete pics/2026/10/cat.jpg # 删除单个对象
cos -b pic-community-1250000000 list # 看桶里的对象列表
迁移历史数据那天我开了两台机器并行跑,一台传图、一台盯着日志,几千张图两个多小时全部搬完,中途断网一次,重跑同步模式收尾。
COSCMD 我踩过两个小坑。第一是密钥别写进命令历史里,配置好一次之后就不需要再传参了;第二是大目录上传中途断网,重跑一遍同步模式即可,已经传完的不会重传。这一点在迁移历史数据时格外重要,毕竟谁也不想传到一半从头再来。
Python SDK:生产环境的正解
社区正式上线后,上传链路应该是这样的:App 或网页把图片交给后端,后端用 Python SDK 把对象写到 COS,然后把对象 URL 存进 MySQL,再返回给前端。这样图片流量完全不经过应用服务器,也是我后端架构里最痛快的一次解耦。
先装 SDK:
pip install cos-python-sdk-v5
我后端的上传函数精简版如下:
from qcloud_cos import CosConfig, CosS3Client
secret_id = "COS_SECRETID"
secret_key = "COS_SECRETKEY"
region = "ap-shanghai"
bucket = "pic-community-1250000000"
config = CosConfig(
Region=region,
SecretId=secret_id,
SecretKey=secret_key,
Scheme="https",
)
client = CosS3Client(config)
def upload_image(local_path, key):
with open(local_path, "rb") as fp:
response = client.put_object(
Bucket=bucket,
Body=fp,
Key=key,
StorageClass="STANDARD",
ContentType="image/jpeg",
)
return response["ETag"]
上传成功之后,对象的访问地址就是 https://<存储桶>.cos.<地域>.myqcloud.com/<对象Key>,把这段拼出来存库即可。由于权限是私有读写,实际下发的是带签名的临时 URL,这块放在 3.3 篇细讲。
我的完整收尾动作是这样的:上传函数返回 ETag 之后,把对象 Key、大小、ETag 和上传时间写进云数据库 MySQL 的图片表,再由短链服务生成用户可分享的短链。注意 ETag 可以当作文件指纹做去重,同一张图被不同用户上传时,先查表就能省掉一次上传和一份存储空间。这个小优化是我在数据库那篇养成的习惯,放到对象存储上同样合适。
几个要点是我交过学费的:Key 就是对象的路径,用 pics/2026/10/xxx.jpg 这种带日期的层级,后面配生命周期规则会非常方便,别像我一开始用平铺目录后来又返工;ContentType 一定记得设,不然有些浏览器预览图片时会变成下载文件;上传前在代码里做一道文件类型校验,别让用户把压缩包塞进图床。
大文件分块上传
用户上传的图里偶尔有几十 MB 的原图,单次 HTTP 传整文件容易超时。SDK 提供的分块上传接口把大文件切成多块并行传:
response = client.upload_file(
Bucket=bucket,
LocalFilePath="/tmp/big.jpg",
Key="pics/2026/10/big.jpg",
PartSize=10,
MAXThread=10,
EnableMD5=True,
)
这个接口本身就封装了分块、并发和断点续传,我一般不用自己手写分块逻辑。PartSize 是每块大小(MB),MAXThread 是并发数。传到一半断了重新调用会自动跳过已完成的块。实际用下来,上传一张 50MB 的原图,比我想象中稳得多。不过对图片社区来说,更优雅的做法是前端直传,后端只发密钥,这正是 3.3 篇的内容。
上传报错排查两次实录
上传链路刚接通时,我遇到最多的是两类报错,顺手记一下,省得后来人踩同一个坑。
第一类是签名错误,后端日志里一串认证失败的提示。原因是我把 Endpoint 的地域拼错了:SDK 配置里的 Region 必须和存储桶实际地域一字不差,写成别的地域,签名计算就和实际链路对不上。第二类是权限拒绝,通常是存储桶权限为私有读写但代码里用了无签名的裸请求,或者子账号没有被授予对应桶的操作权限。遇到上传失败,先按这三步排查:查地域拼写、查权限配置、查密钥状态,基本覆盖九成问题。
三种方式怎么选
| 方式 | 适合场景 | 优点 | 局限 |
|---|---|---|---|
| 控制台 | 验证链路、零星补图 | 零门槛、直观 | 有数量和大小限制 |
| COSCMD | 一次性批量迁移 | 命令行好写脚本、支持同步跳过 | 不适合嵌进业务 |
| Python SDK | 生产后端、程序化上传 | 完整 API、可签名、可分块 | 需要写代码管密钥 |
正式上线后,我的组合是 SDK 负责用户上传,COSCMD 负责运维和搬家,控制台负责偶尔瞄一眼文件列表。如果你对命令行批量操作感兴趣,也可以看我另一本DeepSeek 工具链实战,里面有更多把脚本和 AI 工具串起来用的思路,处理大批量文件的时候特别顺手。日常索取、整理这些本地素材,可以参考笔记知识库整理指南。
延伸阅读
- COS 快速入门:上面所有操作的官方教程入口。
- DeepSeek 工具链实战:脚本化批量处理文件的更多玩法。
- 笔记知识库整理指南:图片文件多了之后的本地管理方案。
