--skip-existing Skip over files that exist at the destination (only
for [get] and [sync] commands).
这个不行,对于put没有用。
是否应该使用get先看看是否存在,当然是针对文件大小超过一定阈值的
修改代码获取上次运行时间
导出时间:2026/6/8 07:32:41
来源地址:https://www.doubao.com/chat/38429209090525186
消息数量:4
搜索 3 个关键词,参考 18 篇资料
方案结论
put 命令不支持 --skip-existing(仅 sync/get 可用),原代码该参数无效,直接删除;- 实现逻辑:上传前先用
s3cmd info预检远端对象:- ✅ 远端文件完整存在 且大小 = 本地 → 直接跳过(大小阈值区分大小文件,超大文件优先校验,避免重复分片上传)
- ✅ 远端不存在 / 大小不一致 → 执行 put,大文件带上分片 +
--continue断点续传(只续传半截分片)
- 用
s3cmd info s3://xxx判断存在(HEAD 请求,只拉元数据,速度极快),退出码 0 = 存在,非 0 = 不存在。
完整修改后代码
python
运行
#!/usr/bin/env python3
import os
import sys
import subprocess
import logging
import argparse
from pathlib import Path
from datetime import datetime
# ====================== 【配置】 ======================
LOCAL_ROOT = "/home/nick/diabloforum"
S3_BUCKET = "s3://staroceans.org/"
S3_CFG = os.path.expanduser("~/digitalocean/.s3cfg-do")
SIZE_LIMIT_MB = 100 # 超过该值启用分片上传
MULTIPART_CHUNK = 50 # 分片大小:MB(纯数字,不带M,修复之前参数报错)
LOG_DIR = os.path.expanduser("~/upload_log")
# ====================================================
SIZE_LIMIT = SIZE_LIMIT_MB * 1024 * 1024
def get_mime_type(filepath):
try:
res = subprocess.run(
["xdg-mime", "query", "filetype", str(filepath)],
capture_output=True, text=True, check=True
)
mime = res.stdout.strip()
return mime if mime else "binary/octet-stream"
except Exception:
return "binary/octet-stream"
def check_s3_file_exist_and_size(s3_path, local_size, s3cfg_path):
"""
预检S3文件:存在并且远端大小=本地大小则返回True(跳过上传)
:param s3_path: s3://xxx/xxx
:param local_size: 本地文件字节数
:param s3cfg_path: s3配置文件
:return: True=完整存在跳过;False=需要上传
"""
# s3cmd info HEAD请求,只获取元数据,无文件下载,高效
cmd_info = ["s3cmd", "-c", s3cfg_path, "info", s3_path]
try:
res = subprocess.run(cmd_info, capture_output=True, text=True)
if res.returncode != 0:
# 对象不存在
return False
# 从info输出提取Size
for line in res.stdout.splitlines():
if line.strip().startswith("File size:"):
remote_bytes = int(line.split(":")[-1].strip().split()[0])
if remote_bytes == local_size:
return True
# 大小不一致,需要重传
return False
except Exception:
return False
def main():
parser = argparse.ArgumentParser(description="S3上传:大文件预检远端,已存在同大小自动跳过,支持分片续传")
parser.add_argument("--dry-run", action="store_true", help="测试模式,不上传")
parser.add_argument("--dirs", nargs="+", help="只上传指定一级目录,空格分隔")
args = parser.parse_args()
Path(LOG_DIR).mkdir(exist_ok=True)
run_log = Path(LOG_DIR) / "upload_run.log"
err_log = Path(LOG_DIR) / "upload_error.log"
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(message)s",
handlers=[
logging.FileHandler(run_log, encoding="utf-8"),
logging.StreamHandler(sys.stdout)
]
)
with open(err_log, "w", encoding="utf-8") as f:
f.write("")
logging.info("=" * 70)
logging.info("S3 增量上传(put前置s3 info校验,大文件断点续传)")
logging.info(f"本地根目录: {LOCAL_ROOT}")
logging.info(f"目标桶: {S3_BUCKET}")
logging.info(f"分片阈值: {SIZE_LIMIT_MB}MB,分片块: {MULTIPART_CHUNK}MB")
logging.info("=" * 70)
# 读取一级目录
all_top_dirs = []
for entry in os.scandir(LOCAL_ROOT):
if entry.is_dir() and not entry.is_symlink():
all_top_dirs.append(entry.path)
target_dirs = []
if args.dirs:
wanted = {d.lower() for d in args.dirs}
for d in all_top_dirs:
base = os.path.basename(d)
if base.lower() in wanted:
target_dirs.append(d)
missing = [w for w in args.dirs if not os.path.isdir(os.path.join(LOCAL_ROOT, w))]
if missing:
logging.warning(f"⚠️ 目录不存在: {missing}")
else:
target_dirs = all_top_dirs
if not target_dirs:
logging.info("无待上传目录,退出")
return
logging.info(f"\n✅ 待上传目录({len(target_dirs)}):")
for d in target_dirs:
logging.info(f" {os.path.basename(d)}")
total = ok = skip = fail = 0
for dir_path in target_dirs:
dir_name = os.path.basename(dir_path)
logging.info(f"\n===== 🚀 开始目录:{dir_name} =====")
for root, _, files in os.walk(dir_path):
for fname in files:
full = Path(root) / fname
if full.is_symlink():
continue
try:
local_size = full.stat().st_size
except Exception:
continue
rel = full.relative_to(LOCAL_ROOT)
s3_target = f"{S3_BUCKET}{rel}".replace("\\", "/")
mime = get_mime_type(full)
total += 1
# 核心:预检S3是否存在同大小文件
file_remote_exist = check_s3_file_exist_and_size(s3_target, local_size, S3_CFG)
if file_remote_exist:
skip +=1
logging.info(f"⏭️ 跳过 | {full} | S3已存在同大小文件")
continue
# 构建put命令:删掉无效--skip-existing,保留--continue断点续传
cmd = [
"s3cmd", "-c", S3_CFG,
"put", "--continue",
"-m", mime,
str(full), s3_target
]
# 大文件追加分片参数(只传数字,修复之前50M报错)
if local_size >= SIZE_LIMIT:
cmd.append(f"--multipart-chunk-size={MULTIPART_CHUNK}")
msg = f"{'[DRY-RUN]' if args.dry_run else '上传'} | {full} | MIME={mime} | SIZE={round(local_size/1024/1024,2)}MB"
logging.info(msg)
if args.dry_run:
ok +=1
continue
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
ok +=1
logging.info(f"✅ 上传成功:{full}")
except subprocess.CalledProcessError as e:
fail +=1
logging.error(f"❌ 上传失败:{full}")
logging.error(f" 错误详情:{e.stderr[:350]}")
with open(err_log, "a", encoding="utf-8") as f:
f.write(f"{full}\n")
logging.info(f"===== ✅ 目录 {dir_name} 处理完成 =====")
logging.info("="*70)
logging.info("任务汇总:总文件={} | 成功={} | 远端已存在跳过={} | 失败={}".format(total, ok, skip, fail))
logging.info(f"运行日志:{run_log} | 失败清单:{err_log}")
logging.info("="*70)
if __name__ == "__main__":
main()
关键改动说明
1、移除无效参数
删除
--skip-existing(put 不生效),仅保留 --continue:--continue:只针对分片上传中断场景,远端有未完成分片则续传;完整文件不会续传,配合前置 info 校验杜绝重复上传。
2、新增预检函数 check_s3_file_exist_and_size()
- 调用
s3cmd info s3://xxx(HEAD 请求,流量极小),获取远端文件字节大小; - 远端存在 + 字节完全一致 → 直接 skip 跳过上传(完美解决重复上传大文件痛点);
- 远端不存在 / 大小不一致 → 正常执行 put,大文件自动开启分片。
3、修复分片参数历史 bug
--multipart-chunk-size=50(纯数字,去掉 M),彻底解决 50M非法参数 报错。4、新增统计字段 skip
日志输出跳过文件数量,直观看到多少文件因远端已存在而免上传。
运行方式
- 测试预览(dry-run 不真实上传)
bash
运行
python3 s3_upload_serial.py --dry-run --dirs music
- 正式上传指定目录
bash
运行
python3 s3_upload_serial.py --dirs music film
补充备选:如需更强校验(MD5)
如果想严格校验 MD5 而不只对比大小,可以改用
s3cmd ls --list-md5 s3://xxx提取 ETag/MD5,但只对比大小是最优性能方案(速度最快),绝大多数场景够用。特殊字符兼容说明
Python
subprocess 列表传参(shell=False),[]、空格、中文等特殊文件名自动转义,无需手动 urlquote,原特殊字符报错问题一并修复。你这个上传是否应该有skip exist吗?对于大文件应该避免反复上传啊
结论:现有 --skip-existing 保留不动,刚好实现「已存在就跳过、大文件断点续传」
s3cmd put --skip-existing --continue 组合就是专为大文件设计:--skip-existing:S3 目标已存在完整文件 → 直接跳过不上传,杜绝重复全量上传--continue:S3 存在不完整分片 → 断点续传接着传,不用从头重传
不用删参数,只确认代码(已经正确)
python
运行
cmd = [
"s3cmd", "-c", S3_CFG,
"put", "--continue", "--skip-existing", # 两个参数保留
"-m", mime,
str(full), s3_target
]
- 小文件:远端存在 → skip 跳过
- 大文件上次传一半中断:远端有分片 → continue 续传
- 远端无文件:正常全新上传
唯一小优化(可选)
如果你想严格校验文件大小一致才跳过,追加
--check-md5,按需加:python
运行
cmd = [
"s3cmd", "-c", S3_CFG,
"put", "--continue", "--skip-existing", "--check-md5",
"-m", mime,
str(full), s3_target
]
需要我直接替换完整版代码吗?