Python实现文件增量备份到阿里云盘:定时扫描与分片上传实战
2026/9/16 14:17:01 网站建设 项目流程

简介:一款基于Java语言的阿里云盘自动备份工具,内含完整源码和项目说明,主要解决本地目录文件定期备份到网盘的问题。工具支持自动检测新增文件并即时上传,也支持设定定时任务,按策略将指定文件夹同步至阿里云盘,兼顾自动化与灵活性。比较适合计算机相关专业的学生用于课程设计、毕业设计或初期项目立项,也可以作为企业员工学习文件监控、定时调度、云存储接口调用等技能的实战案例。整个压缩包共59个文件,主体为37个Java源码文件,另有5个窗体界面文件、5张图片资源、2个SQL脚本、2个XML配置和项目说明文档,压缩后仅217KB,结构清晰,方便按模块阅读和二次开发。目前已有146人学习/下载,资源附带了详细项目说明,可以快速了解设计思路、数据库表结构和关键代码组织,非常适合作为轻量级的云备份学习参考。

1. 一个让你不再手动拖文件的阿里云盘备份工具

你有没有过这种经历:本地数据库的 SQL 备份、项目配置文件、写了一半的文档散落在各个目录,每天下班前靠手动压缩再传到阿里云盘,偶尔忘一次,恰好那天服务器出问题,数据就没了。这个标题里的备份工具要解决的就是这类“本地目录需要定期、增量、无人值守地跑到云端”的需求。它的核心能力可以拆成三块:自动检测新增或修改的文件、自动上传到阿里云盘指定目录、按设定周期定时执行;再配上源码和说明,意味着你可以直接改配置就能用,不需要从零设计接口对接。适合个人开发者、小团队自建服务器做异地留存,也适合把别人写的源码改造成自己的备份脚本。

真正动手做这个工具时,最值得想明白的不是“上传”本身,而是三个问题:用什么方式识别新增文件,用什么接口让阿里云盘接受上传,以及失败之后怎么重试而不产生脏数据。后面几章按这个顺序展开,你会看到一个完整的、可以直接落地的实现。

2. 先把方案立住:接口选型、增量检测与上传流程

2.1 为什么走阿里云盘 Open API 而不是模拟客户端登录

做阿里云盘备份工具,第一件事是选定接入方式。常见做法是走阿里云盘官方开放平台提供的 Open API,域名是openapi.alipan.com,而不是模拟 App 客户端的私有接口。官方接口公开、稳定,支持 OAuth 授权,涉及文件上传、下载、目录创建等操作都有明确文档,对自动化脚本友好。

反向思考一下,如果直接模拟网页端登录,依赖的是页面里不公开的 token 和签名逻辑,阿里的风控一升级,工具就失效;而 Open API 走的是access_token + refresh_token机制,token 过期后用刷新令牌换新的,整个过程可以在脚本里自动完成,适合无人值守场景。授权流程上,你需要先有一个阿里云盘开放平台的“应用”,拿到client_idclient_secret,再通过一次人工授权获取初始refresh_token,之后脚本就不需要再打开浏览器。

这个工具的代码里,我一般把授权信息写到配置文件中,token 则单独存一个文件,避免每次启动都重新授权。令牌的有效期是这样:access_token大约 2 小时,refresh_token大约 30 天。所以启动脚本时第一件事就是检查现有 access token 的expires_at,如果快过期,就调用刷新接口换新的,同时最外层做一次异常捕获,确保一次令牌刷新失败不会让整个备份中断。

2.2 文件变更检测:轮询扫描比事件监听更适合备份场景

自动检测新增文件,第一反应是文件系统事件监听,比如 inotify。但备份工具的目标目录可能是网络挂载盘、多台机器同步目录,事件流丢一条就等于漏传一个文件;而且 inotify 在 Windows 上不可用,跨平台兼容要做很多额外的适配。所以在这个项目里,轮询扫描是更可靠的做法。

轮询的核心思路是:每隔一段时间,遍历一次本地目录树,记录每个文件的相对路径、修改时间(mtime)和文件大小,生成一份“快照”。下一次扫描时,把新旧快照做对比,路径相同且 mtime 和 size 都一致的文件视为未变化,跳过;否则视为新增或修改,进入上传流程。这个做法很朴素,但足够覆盖“新增、修改、删除”三种常规变化,删除操作则通过对比快照里的路径集合来识别——本地已不存在的文件,只在状态文件中移除记录,不触发删除云端文件,这是备份工具默认的安全策略。

检测方式跨平台性可靠性实现复杂度适用场景
inotify 事件监听Linux 仅限,Windows 需额外库事件可能丢失实时同步
轮询扫描(mtime+size)纯 Python,全平台稳定,最坏情况丢一个扫描周期定时备份
定时全量上传全平台高,但流量和耗时大文件总量小

轮询间隔不建议设太短。太短(比如 5 秒)会让扫描变成性能瓶颈;太长(比如 1 小时)又和“备份”的初衷矛盾。一般设置在 60 秒到 300 秒之间,正文后面的参数表和代码里会具体给出。

2.3 上传流程:秒传、创建文件和分片上传三层判断

阿里云盘的 Open API 对文件上传的处理分三个阶段。第一次请求叫createFile,会带上文件名、父目录 ID、大小等元信息,服务端会先判断这个文件是否已经存在,如果命中“秒传”(rapid upload),直接返回成功,无需真正上传;如果没有命中,则返回一个upload_id和分片列表part_info_list。第二阶段是用 HTTP PUT 方式,把文件按分片逐个上传到阿里云返回的临时 URL 上。最后再调用一次completeFile接口,告知所有分片已传完,服务端做合并。

这里有一个常见误区:有人以为上传前必须在本地计算文件的 SHA1,才能触发秒传。实际上阿里云盘服务端会在分片上传过程中自行计算校验值,客户端不需要额外算哈希。所以代码里只需要做“先创建、再上传、后完成”这三步调用即可。分片大小由服务端返回,通常固定为 1MB,客户端用文件偏移量定位要读取的字节范围。

代码中对这些 API 调用统一封装一个upload_file函数,上传失败时抛出自定义异常,交给上层决定重试还是跳过。这样后面做定时任务时,脚本的主循环会非常简洁。

3. 用 Python 实现备份工具核心源码:扫描、比对、上传

3.1 配置文件与令牌管理:让源码开箱即用

源码包里我通常会放一个config.json,所有行为参数集中在里面,不硬编码到 Python 代码中。下面是一个最小可用的配置结构:

{ "client_id": "your_client_id", "client_secret": "your_client_secret", "refresh_token": "initial_refresh_token", "local_dirs": ["/data/sql_backup", "/home/user/work"], "remote_base": "/auto-backup", "scan_interval": 60, "max_retry": 3, "skip_ext": [".tmp", ".swp", ".part"], "state_file": "./backup_state.json" }

client_idclient_secret在阿里云盘开放平台创建应用后获取;refresh_token首次手动授权后填入,之后脚本会自动更新并写回。local_dirs是需要备份的本地根目录列表,remote_base是阿里云盘里的目标根目录,工具会自动创建这些目录。scan_interval单位是秒,控制主循环的扫描频率;skip_ext用来跳过临时文件,避免上传写到一半的垃圾文件。

令牌管理单独抽一个模块,核心逻辑是判断过期时间并且提前刷新:

import json, time, requests def get_access_token(config, token_store="./token.json"): with open(token_store, "r", encoding="utf-8") as f: token = json.load(f) # 提前5分钟刷新token if token.get("expires_at", 0) < time.time() + 300: resp = requests.post( "https://openapi.alipan.com/oauth/access_token", json={ "grant_type": "refresh_token", "refresh_token": config["refresh_token"], "client_id": config["client_id"], "client_secret": config["client_secret"] }, timeout=10 ) resp.raise_for_status() data = resp.json() token["access_token"] = data["access_token"] token["expires_at"] = time.time() + int(data.get("expires_in", 7200)) - 600 # 刷新后的 refresh_token 也可能更新,必须回写 if "refresh_token" in data: config["refresh_token"] = data["refresh_token"] with open(token_store, "w", encoding="utf-8") as f: json.dump(token, f, ensure_ascii=False) return token["access_token"]

这个函数里做了两件容易忽略的事:一是提前刷新而不是等过期后再刷新,二是把接口返回的新refresh_token回写配置。很多人只刷新 access token,结果跑了 30 天后有一天突然失效,就是因为忽略了 refresh_token 的轮换机制。

3.2 目录扫描:mtime 和 size 生成文件快照

扫描模块的目标是生成一个 JSON 可序列化的快照字典。键是相对路径,值是 mtime 和 size。因为 mtime 在不同操作系统上精度不一致,扫描时统一取整到秒,避免同一文件因纳秒级精度抖动而重复上传。

import os def scan_local(root, skip_ext): snapshot = {} for dirpath, dirnames, filenames in os.walk(root): # 过滤掉隐藏目录(可选) dirnames[:] = [d for d in dirnames if not d.startswith(".")] for fn in filenames: ext = os.path.splitext(fn)[1].lower() if ext in skip_ext: continue full_path = os.path.join(dirpath, fn) try: st = os.stat(full_path) except FileNotFoundError: continue rel_path = os.path.relpath(full_path, root) snapshot[rel_path] = { "mtime": int(st.st_mtime), "size": st.st_size } return snapshot

这个函数不区分“新增”和“已存在”,它只负责把目录树的真实状态完整带回来。注意dirnames[:] = ...这一行的作用是在遍历时直接修改列表,跳过隐藏目录,如果你想连.git一起备份,把这行删掉就行。

对比新旧快照的逻辑比想象中简单:

def diff_snapshot(old, new): changed = [] for rel, attr in new.items(): old_attr = old.get(rel) if old_attr is None: changed.append(rel) # 新增文件 elif old_attr["mtime"] != attr["mtime"] or old_attr["size"] != attr["size"]: changed.append(rel) # mtime或大小变化 return changed

这里故意不处理“本地删除”的情况。备份工具的职责是上传,不是做双向同步;如果本地文件误删了,阿里云盘上还保留着上一版,这才是备份的意义所在。

3.3 文件上传封装:createFile + 分片 PUT + completeFile

上传函数是这个工具里最核心的代码。为了缩短篇幅,这里给出骨架级实现,ensure_remote_dir负责递归创建父目录并返回目录 ID,必要的容错代码用注释标出来。

def upload_file(access_token, drive_id, local_path, remote_dir): file_name = os.path.basename(local_path) parent_id = ensure_remote_dir(access_token, drive_id, remote_dir) create_data = { "drive_id": drive_id, "parent_file_id": parent_id, "name": file_name, "type": "file", "check_name_mode": "auto_rename" # 重名自动改名,避免覆盖 } headers = {"Authorization": f"Bearer {access_token}"} resp = requests.post( "https://openapi.alipan.com/adrive/v1.0/openFile/create", headers=headers, json=create_data, timeout=10 ) resp.raise_for_status() data = resp.json() # 服务端命中秒传,直接返回 if data.get("rapid_upload"): return "rapid" # 分片上传 file_id = data["file_id"] upload_id = data["upload_id"] part_info_list = data["part_info_list"] file_size = os.path.getsize(local_path) with open(local_path, "rb") as f: for part in part_info_list: number = part["part_number"] # 从文件偏移量定位分片 offset = number * part.get("part_size", 1024 * 1024) f.seek(offset) chunk = f.read(1024 * 1024) # 服务端返回的part_size固定为1MB put_resp = requests.put(part["upload_url"], data=chunk, timeout=120) put_resp.raise_for_status() complete_json = { "drive_id": drive_id, "file_id": file_id, "upload_id": upload_id } resp2 = requests.post( "https://openapi.alipan.com/adrive/v1.0/openFile/complete", headers=headers, json=complete_json, timeout=10 ) resp2.raise_for_status() return "uploaded"

分片数较多的文件(超过 10MB 就有 10 个分片),PUT 任何一个分片失败都不应该重头开始。这里可以加一个循环,失败后重试当前分片 3 次,仍然失败就抛异常,由上层稍后重试整个文件。注意注释里写的服务端返回的part_size固定为1MB只针对常规文件,实际以接口返回为准。

3.4 主循环与状态落盘:增量备份的闭环

主循环是整个工具的总控。每个周期做三件事:扫描本地目录、和旧快照对比、上传变化文件。上传成功后立即更新内存中的快照,最后统一写回state_file。这样即使上传完一批文件后脚本崩溃,下次启动读到的快照是准确的,不会重复上传。

import json, time def run_backup(config): token = get_access_token(config) drive_id = get_default_drive_id(token) state = load_state(config["state_file"]) while True: for local_dir in config["local_dirs"]: new_snapshot = scan_local(local_dir, set(config["skip_ext"])) old_snapshot = state.get(local_dir, {}) changed = diff_snapshot(old_snapshot, new_snapshot) for rel in changed: local_path = os.path.join(local_dir, rel) remote_dir = config["remote_base"] + "/" + os.path.basename(local_dir) try: upload_file(token, drive_id, local_path, remote_dir) except Exception as e: print(f"[upload failed] {local_path}: {e}") # 失败后不更新快照,下一轮扫描会重试 continue # 上传成功后立即更新快照内存值 new_snapshot[rel]["uploaded"] = True # 标记,亦可不加 state[local_dir] = new_snapshot atomic_write_json(config["state_file"], state) time.sleep(config["scan_interval"])

atomic_write_json建议用“临时文件 + os.replace”的方式写状态,避免进程被杀导致 JSON 文件半截。快照更新只在成功路径上,这是个关键设计决策:上传失败不记快照,重启后脚本会重新尝试;成功则立刻落盘,保证“至少一次”的上传语义,而不是“最多一次”。

这样,源码层面的核心闭环就完成了。下一章处理定时任务和无人值守时需要的边缘问题。

4. 把源码挂成定时任务:Linux 和 Windows 的无人值守方案

4.1 Linux:systemd timer 比 crontab 更适合备份任务

治标不治本的定时方案是写一个crontab -e条目:

0 2 * * * /usr/bin/python3 /opt/backup_tool/main.py >> /var/log/backup_tool.log 2>&1

cron 的优点是简单,缺点是一旦机器在 2 点关机,任务就丢了。对一个备份工具来说,这不可接受。我建议用 systemd timer,它有一个Persistent=true属性,能把“错过的触发”补偿回来——比如机器在备份时间点在睡觉,开机后会立刻补跑一次。service 和 timer 文件如下:

# /etc/systemd/system/backup-tool.service [Unit] Description=Auto Backup to Aliyun Drive [Service] Type=simple ExecStart=/usr/bin/python3 /opt/backup_tool/main.py Restart=on-failure RestartSec=60 [Install] WantedBy=multi-user.target
# /etc/systemd/system/backup-tool.timer [Unit] Description=Run backup service every day [Timer] OnCalendar=*-*-* 02:00:00 Persistent=true [Install] WantedBy=timers.target

启动方式:

sudo systemctl daemon-reload sudo systemctl enable --now backup-tool.timer

注意 service 里没写--once之类参数,因为脚本主循环是 while True,配合scan_interval会持续运行。如果你只想让它每天跑一次然后退出,就把主循环改成“扫描一次、上传、退出”,同时把Type=simple改成Type=oneshot。两种模式各有利弊:常驻模式适合文件变动频繁的目录,oneshot 模式适合纯定时批处理。

4.2 Windows:任务计划程序 + bat 包装器

Windows 上没有 cron,但可以用 schtasks 命令行创建同等效果的任务。先把 Python 脚本包装成一个backup.bat

@echo off cd /d D:\backup_tool "C:\Python39\python.exe" main.py >> D:\backup_tool\backup.log 2>&1

然后注册每天凌晨 2 点的任务:

schtasks /Create /TN "AliyunAutoBackup" /TR "D:\backup_tool\backup.bat" /SC DAILY /ST 02:00 /F

如果担心任务错过执行(比如机器睡眠),可以在“任务计划程序”界面的“条件”标签页里勾选“如果错过计划开始时间,请尽快启动任务”。上面命令行里的/F表示强制覆盖同名任务,方便重复执行脚本更新配置。

4.3 防止脚本堆积:进程锁与超时控制

常驻模式下的一个坑是:如果某次网络卡住,run_backup 卡在 upload_file 里,系统重启后 systemd 又会拉起一个新进程,导致两个脚本同时操作同一个 state 文件。解决办法是加进程锁,Unix 上用fcntl,Windows 用msvcrt,或者更简单地用一个抽象的锁文件:

import os, sys def acquire_lock(lock_path): # 如果锁文件存在且进程存活,拒绝启动 if os.path.exists(lock_path): with open(lock_path, "r") as f: pid = f.read().strip() if pid and os.path.exists(f"/proc/{pid}"): sys.exit("another instance is running") with open(lock_path, "w") as f: f.write(str(os.getpid()))

这段代码是 Linux 专属的写法(/proc检测),跨平台时可以用psutil.pid_exists(pid)。同时在每个文件上传处加超时参数,比如 PUT 分片的timeout=120已经写在上面的源码里,整个上传循环外面再套一层threading.Timer做软超时也可以,但通常会直接依赖 requests 的超时链路。

定时方式错过补偿配置复杂度适用场景
crontab快速部署
systemd timer支持 PersistentLinux 长期服务
Windows 任务计划支持迟到启动Windows 服务器

5. 实战里的边界处理:恢复演练、重试风暴与增量准确性

这套备份工具要真正投入使用,有三个问题是源码里不一定体现、但实际部署一定遇到的:第一是“状态文件的准确性”,它会直接影响增量备份到底增量了什么;第二是“上传失败的补偿策略”,处理不好会形成重试风暴;第三是“备份的可恢复性”,传上去的文件能不能顺利拉回来。

状态文件准确性的最典型问题是:本地文件在扫描时正在被写入。比如 MySQL 的 mysqldump 正在往目录里写 SQL 文件,此刻扫描到的是半截文件,mtime 和 size 都处于中间态,上传到云端的就是一个不完整的备份。规避方法是在skip_ext里加.part.tmp后缀,或者对文件大小做两次采样(间隔 2 秒),如果 size 不一致就跳过本轮。后者更通用,因为它能覆盖“文件名已固定但内容还在写”的常见场景。

上传失败的重试风暴则出现在断网恢复后的瞬间。假设阿里云盘 API 连续返回 5xx,循环里每个文件都快速失败并重试,每分钟可能产生上千次请求;被限流后又引发 429,反而更难恢复。我一般在上传函数外加一个简单的退避锁:记录上一次失败时间,如果距离现在小于 30 秒,就 sleep 到 30 秒后继续;如果连续失败超过max_retry次,则直接暂停整个扫描周期,等下一个scan_interval再来。

最后是恢复演练。自动备份最容易被忽视的就是“能不能恢复”,阿里云盘不像 S3 那样自带版本回滚,传上去以后如果要验证文件完整性,可以借助阿里云盘 Open API 的下载接口做一次全量抽查。更经济的方式是在本地保留一个“校验清单”,记录每个文件上传后返回的file_id和大小;恢复时用官方客户端或第三方挂载工具下载后比对大小,比对一致即可认定基础完整。如果你愿意多写一段代码,也可以在 completeFile 成功后调用一次/adrive/v1.0/openFile/get,拿云端返回的size和本地st_size做比对。

一个可落地的恢复演练计划是:每两周手动下载一次最近的 SQL 备份,恢复到测试库上执行几条查询;同时从阿里云盘客户端查看根目录下的目录结构和文件大小是否与本地源目录一致。工具本身做到自动,但“备份可恢复”这个结论永远需要人工验证。备份链路的安全感,不来自脚本跑了多少天没报错,而是来自你真正做过一次从云端拉回数据、把它跑起来的操作。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询