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

在Deno的Oak路由中处理文件上传时的用户中断问题

Deno Oak 文件上传中断时删除不完整文件的解决方案

问题背景

在Node.js中,我们可以通过监听请求对象的相关事件处理上传中断:

req.on("data", (data) => console.log(`on data: ${data.length}`));
req.on("close", () => console.log("req closed"));
req.on("aborted", () => console.log("req aborted"));
req.on("error", (err) => console.log("req error", err));

但在Deno + Oak框架实现文件上传时,若用body.value.read()直接将文件存储到磁盘,用户中途中断上传或出现网络问题时,磁盘会残留不完整文件,需要实现自动清理逻辑。

解决方案

核心思路是手动处理流式上传,监听请求中断信号,用临时文件存储上传中的数据,上传完成后再确认保留,中断则删除临时文件,具体实现如下:

修改后的上传路由代码

//imports
import { serve } from "https://deno.land/std@0.166.0/http/server.ts";
import { Application, Router } from 'https://deno.land/x/oak/mod.ts';

// ...其他代码

router.post("/upload", async (ctx) => {
  let tempFilePath: string | undefined;
  try {
    const body = ctx.request.body({ type: "form-data" });
    const formReader = body.value;

    console.log("File upload started");

    // 监听请求中断信号
    const abortController = new AbortController();
    ctx.request.signal.addEventListener("abort", () => {
      abortController.abort();
      // 中断时删除已创建的临时文件
      if (tempFilePath) {
        Deno.remove(tempFilePath).catch(console.error);
      }
    });

    let hasValidFile = false;
    while (true) {
      const part = await formReader.read({ signal: abortController.signal });
      if (part.done) break;

      if (part.value.type === "file") {
        const filePart = part.value;
        // 生成唯一临时文件名
        tempFilePath = `./uploads/temp_${crypto.randomUUID()}_${filePart.filename}`;
        const fileWriter = await Deno.open(tempFilePath, { write: true, create: true });
        
        // 流式写入文件,绑定中断信号
        await filePart.value.pipeTo(fileWriter.writable, { signal: abortController.signal });
        await fileWriter.close();

        // 上传完成,重命名为正式文件
        const finalFilePath = `./uploads/${filePart.filename}`;
        await Deno.rename(tempFilePath, finalFilePath);
        tempFilePath = undefined; // 重置临时路径,避免误删正式文件

        ctx.response.status = 200;
        ctx.response.body = { path: filePart.filename };
        hasValidFile = true;
      }
      // 若需要处理表单字段,可在此添加逻辑
    }

    if (!hasValidFile) {
      ctx.response.status = 400;
      ctx.response.body = { msg: "No file provided" };
    }

  } catch (error) {
    console.log(error);
    // 处理中断错误
    if (error.name === "AbortError" && tempFilePath) {
      await Deno.remove(tempFilePath).catch(console.error);
      ctx.response.status = 499; // 自定义客户端中断状态码
      ctx.response.body = { msg: "Upload aborted by client" };
    } else {
      // 其他错误:清理临时文件并返回500
      if (tempFilePath) {
        Deno.remove(tempFilePath).catch(console.error);
      }
      ctx.response.status = 500;
      ctx.response.body = { msg: error.message };
    }
  }
});

// ...启动服务器代码

关键逻辑说明

  • 监听中断信号:通过ctx.request.signal监听请求的中止事件,触发时调用abortController.abort()终止文件流写入,并删除临时文件。
  • 临时文件机制:上传过程中先将文件写入临时文件,上传完成后再重命名为正式文件,避免中断时留下不完整的正式文件。
  • 错误处理分支:在catch块中区分中断错误(AbortError)和其他服务器错误,统一清理临时文件并返回对应状态码。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 11:33:17