You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Python的RESTful API中中断shutil.copyfile的大文件复制进程?

实现可中断的大文件复制API

嘿,我来帮你搞定这个问题~你现在用的shutil.copyfile有个小局限:它是一次性把整个文件读入内存再写入,全程阻塞,一旦开始复制大文件,就没法中途停下——这也是为啥你没法中断特定API调用的原因。要解决这个,我们得换个思路:分块复制,每复制一小段就检查一次是否需要中断,这样既能及时响应取消请求,又不会影响服务本身。

第一步:写一个支持中断的分块复制函数

我们自己实现分块复制逻辑,代替shutil.copyfile,核心是在每块复制的间隙加入中断检查。代码如下:

import os

def copy_file_interruptible(source, destination, check_interrupt):
    """支持中断的分块复制函数
    check_interrupt: 无参回调函数,返回True时表示要中断复制
    """
    chunk_size = 1024 * 1024  # 用1MB的块来复制,你可以根据服务器性能调整大小
    # 先确保目标目录存在
    dest_dir = os.path.dirname(destination)
    if not os.path.exists(dest_dir):
        os.makedirs(dest_dir)
    
    try:
        with open(source, 'rb') as src_file, open(destination, 'wb') as dest_file:
            while True:
                # 每次复制前先检查是否需要中断
                if check_interrupt():
                    # 中断时记得删掉未完成的目标文件,避免留垃圾
                    os.remove(destination)
                    return False
                # 读取一块内容
                chunk = src_file.read(chunk_size)
                if not chunk:
                    break  # 读完了,复制完成
                # 写入一块内容
                dest_file.write(chunk)
        return True
    except Exception as e:
        # 出错时也清理未完成的文件
        if os.path.exists(destination):
            os.remove(destination)
        raise e

这个函数的关键是check_interrupt回调——我们可以在里面判断当前的API请求是不是被用户中断了。

第二步:在RESTful API里集成中断检测

假设你用的是Flask(其他框架比如Django逻辑类似,核心都是检测请求是否断开),Flask可以通过request.environ里的werkzeug.request_disconnected来判断客户端是不是断开了连接。我们把这个逻辑封装成检查函数传给上面的复制函数:

from flask import request, Flask, jsonify

app = Flask(__name__)

def check_if_request_disconnected():
    """检查当前请求是否被客户端取消/断开"""
    return request.environ.get('werkzeug.request_disconnected', False)

@app.route('/copy', methods=['POST'])
def copy_file_api():
    # 从请求里拿源文件和目标路径(这里假设用JSON传参)
    source = request.json.get('source')
    destination = request.json.get('destination')
    
    if not source or not destination:
        return jsonify({'status': 'error', 'message': '缺少source或destination参数'}), 400
    
    try:
        # 调用可中断的复制函数
        copy_success = copy_file_interruptible(source, destination, check_if_request_disconnected)
        if copy_success:
            return jsonify({'status': 'success', 'message': '文件复制完成'})
        else:
            return jsonify({'status': 'cancelled', 'message': '复制操作已被取消'}), 409
    except Exception as e:
        return jsonify({'status': 'error', 'message': str(e)}), 500

如果是Django的话,你可以通过request对象的连接状态来判断,或者结合前端的取消信号(比如AJAX的abort事件),核心都是在分块复制的间隙判断是否要终止操作。

额外优化点

  • 块大小调整:chunk_size可以根据你的服务器性能和文件大小调,太大的话中断响应会变慢,太小会增加IO次数,1MB是个比较均衡的选择。
  • 后台线程处理:如果你的API是同步阻塞的,大文件复制会占用请求线程,导致其他请求排队。这时候可以把复制操作放到后台线程里,同时用一个线程安全的标志位来控制中断,比如:
import threading
from flask import request, Flask, jsonify

app = Flask(__name__)
# 用字典存每个线程的中断标志,key用线程ID
interrupt_flags = {}

def copy_in_background(source, destination, thread_id):
    chunk_size = 1024 * 1024
    dest_dir = os.path.dirname(destination)
    if not os.path.exists(dest_dir):
        os.makedirs(dest_dir)
    
    try:
        with open(source, 'rb') as src_file, open(destination, 'wb') as dest_file:
            while True:
                # 检查当前线程的中断标志
                if interrupt_flags.get(thread_id, False):
                    os.remove(destination)
                    break
                chunk = src_file.read(chunk_size)
                if not chunk:
                    break
                dest_file.write(chunk)
    except Exception as e:
        if os.path.exists(destination):
            os.remove(destination)
        print(f"复制失败: {e}")
    finally:
        # 清理标志位
        interrupt_flags.pop(thread_id, None)

@app.route('/copy', methods=['POST'])
def start_copy():
    source = request.json.get('source')
    destination = request.json.get('destination')
    if not source or not destination:
        return jsonify({'status': 'error', 'message': '缺少必要参数'}), 400
    
    # 启动后台线程执行复制
    current_thread_id = threading.get_ident()
    interrupt_flags[current_thread_id] = False
    thread = threading.Thread(target=copy_in_background, args=(source, destination, current_thread_id))
    thread.start()
    
    return jsonify({'status': 'started', 'thread_id': current_thread_id})

@app.route('/copy/cancel/<int:thread_id>', methods=['POST'])
def cancel_copy(thread_id):
    # 设置对应线程的中断标志
    interrupt_flags[thread_id] = True
    return jsonify({'status': 'success', 'message': '已请求取消复制'})

这个版本允许用户通过调用/copy/cancel/<thread_id>来主动中断复制,适合需要用户手动取消的场景。


内容的提问来源于stack exchange,提问作者gtamorim

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 03:51:47