Python调用企业云盘SDK自动归档:一次实战踩坑全记录

3 阅读5分钟

最近在给客户做知识管理体系的时候,遇到一个很实际的需求:每天下午六点,研发服务器上生成的日报、测试报告、构建日志,需要自动归档到企业云盘指定的项目目录下,保留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=Trueiter_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,但你的代码没有打印错误日志,就默默失败了。