最近在给客户做知识管理体系的时候,遇到一个很实际的需求:每天下午六点,研发服务器上生成的日报、测试报告、构建日志,需要自动归档到企业云盘指定的项目目录下,保留90天,超时的自动清理掉。
听起来是个常规的定时任务,但做的时候踩了几个坑,记录一下,供有类似需求的朋友参考。
核心流程是四步:认证 → 列举 → 下载 → 归档。用巴别鸟的开放API配合Python实现,整体代码量不大,但有几个地方容易出错。
第一步:认证,这个坑踩得最久
巴别鸟开放平台支持API Key认证,获取方式是在管理后台的"开放平台"菜单下创建应用,拿到App ID和App Secret。这两个东西要保管好,不要提交到代码仓库。
import requests
import time
import hashlib
import os
from datetime import datetime, timedelta
# 认证配置
APP_ID = "your_app_id"
APP_SECRET = "your_app_secret"
BASE_URL = "https://api.babelbird.com/v1"
def get_access_token():
"""
获取访问令牌,支持缓存,过期前自动刷新
"""
# 简单文件缓存,实际生产环境建议用Redis
token_file = "/tmp/babelbird_token.json"
if os.path.exists(token_file):
with open(token_file) as f:
token_data = json.load(f)
# 提前5分钟刷新,避免边界情况
if token_data['expires_at'] > time.time() + 300:
return token_data['access_token']
# 申请新Token
resp = requests.post(f"{BASE_URL}/auth/token", json={
"app_id": APP_ID,
"app_secret": APP_SECRET,
"grant_type": "client_credentials"
})
resp.raise_for_status()
token_data = resp.json()
with open(token_file, 'w') as f:
json.dump({
'access_token': token_data['access_token'],
'expires_at': time.time() + token_data['expires_in']
}, f)
return token_data['access_token']
踩坑提醒:Token的有效期各平台不一样,巴别鸟默认是7200秒,但建议不要硬编码这个数值,每次响应里会返回expires_in,以返回值为准。另外,Token要缓存,不要每次请求都去申请一次,高频调用会触发频率限制。
第二步:列举文件,找到需要归档的内容
拿到Token之后,先列举研发服务器指定目录下的文件。假设我们有一个目录存放每天的构建日志,目录结构是/build-reports/2024/11/18/这样的。
def list_files(folder_path, access_token):
"""
递归列举文件夹下的所有文件
"""
headers = {"Authorization": f"Bearer {access_token}"}
files = []
page_token = None
while True:
params = {
"path": folder_path,
"page_size": 200
}
if page_token:
params["page_token"] = page_token
resp = requests.get(
f"{BASE_URL}/files/list",
headers=headers,
params=params
)
resp.raise_for_status()
data = resp.json()
files.extend(data.get('files', []))
page_token = data.get('next_page_token')
if not page_token:
break
return files
def should_archive(file_info, retention_days=90):
"""
判断文件是否需要归档
- 超过保留期限的标记为待删除
- 最近7天内的标记为待归档(避免归档进行中的文件)
"""
modified = datetime.fromtimestamp(file_info['modified_at'])
now = datetime.now()
age_days = (now - modified).days
if age_days > retention_days:
return 'delete', age_days
elif age_days >= 7:
return 'archive', age_days
else:
return None, age_days
这里有个容易忽略的点:文件列表接口通常是分页的,每次返回的数量有上限,需要用next_page_token循环拉取全部。忘记处理分页的话,只能拿到第一页数据,后面的文件全部漏掉。
第三步:下载文件到本地临时目录
找到了需要归档的文件之后,先下载到本地,再上传到云盘。不能跨云盘直接流转,因为API不支持远程文件拷贝。
def download_file(file_path, local_dir="/tmp/pending_archive"):
"""
下载单个文件到本地临时目录
"""
os.makedirs(local_dir, exist_ok=True)
local_path = os.path.join(local_dir, os.path.basename(file_path))
token = get_access_token()
headers = {"Authorization": f"Bearer {token}"}
# 流式下载,大文件不分块加载到内存
resp = requests.get(
f"{BASE_URL}/files/download",
headers=headers,
params={"path": file_path},
stream=True
)
resp.raise_for_status()
with open(local_path, 'wb') as f:
for chunk in resp.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
return local_path
踩坑提醒:大文件一定要用流式下载,stream=True加iter_content,不然文件先整个加载到内存,1GB的日志文件直接OOM。requests默认行为是把整个响应体存在内存里等你去读,大文件必挂。
第四步:上传归档,更新元数据
本地下载完了,接下来上传到云端的归档目录。归档路径按日期组织,比如/归档库/研发日报/2024/11/18/。
def upload_archive(local_path, target_folder, access_token):
"""
上传文件到归档目录,并设置正确的修改时间
"""
filename = os.path.basename(local_path)
target_path = f"{target_folder}/{filename}"
headers = {"Authorization": f"Bearer {token}"}
with open(local_path, 'rb') as f:
resp = requests.post(
f"{BASE_URL}/files/upload",
headers=headers,
data={"path": target_path, "overwrite": True},
files={"file": f}
)
resp.raise_for_status()
# 设置创建时间与源文件一致,保持时间线可追溯
file_stat = os.stat(local_path)
requests.patch(
f"{BASE_URL}/files/{resp.json()['file_id']}",
headers=headers,
json={"modified_at": int(file_stat.st_mtime)}
)
return target_path
def cleanup_old_files(archive_folder, retention_days=90, dry_run=True):
"""
清理超过保留期的归档文件
dry_run=True时只打印不删除,方便确认
"""
token = get_access_token()
files = list_files(archive_folder, token)
deleted_count = 0
for f in files:
action, age = should_archive(f, retention_days)
if action == 'delete':
print(f"{'【将删除】' if dry_run else '【已删除】'} {f['path']} (创建后{age}天)")
if not dry_run:
requests.delete(
f"{BASE_URL}/files/{f['file_id']}",
headers={"Authorization": f"Bearer {token}"}
)
deleted_count += 1
print(f"共处理 {len(files)} 个文件,待删除 {deleted_count} 个")
整个流程串起来就是这样:
def main():
source_folder = "/build-reports/2024"
archive_folder = "/归档库/研发日报"
token = get_access_token()
files = list_files(source_folder, token)
print(f"扫描到 {len(files)} 个文件")
archived = 0
for f in files:
action, age = should_archive(f)
if action == 'archive':
local_path = download_file(f['path'])
upload_archive(local_path, archive_folder, token)
archived += 1
elif action == 'delete':
# 生产环境取消dry_run
pass
cleanup_old_files(archive_folder, dry_run=False)
print(f"归档完成:{archived} 个文件")
if __name__ == "__main__":
main()
配合crontab设置每天下午六点执行:
0 18 * * * cd /opt/archive-bot && /usr/bin/python3 main.py >> /var/log/archive.log 2>&1
加>> /var/log/archive.log 2>&1是必须的,不然出问题了不知道,看不到任何输出。
整体体验下来,巴别鸟的开放API设计得比较清晰,认证用OAuth2标准,分页机制规范,文件上传下载都有对应的接口。踩的两个主要坑一个是Token缓存没做好导致触发限频,一个是没注意大文件的流式下载。解决之后稳定跑了三个月没出过问题。
如果你也有类似的自动化归档需求,建议先把认证和Token刷新机制做好,这是最容易出问题的环节,也是最难排查的——你以为逻辑跑通了,实际上Token过期了,请求全返回401,但你的代码没有打印错误日志,就默默失败了。