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

Next.js 13上传文件至Azure Blob Storage时Blob名称异常求助

问题解决:Next.js 13上传文件到Azure Blob Storage时保留原文件名并避免覆盖

问题分析

当前代码存在两个核心问题:

  • 前端未将原文件名传递给后端,导致后端无法获取正确的文件名作为Blob名称
  • 后端错误尝试访问request.blob.name(Request对象无此属性),Blob名称异常,重复上传时会覆盖旧文件

前端组件修改(Next.js 13)

修改思路:保留选中的原始文件对象,用FormData传递文件和文件名,避免使用DataURL(会增加体积且丢失原始文件信息)

"use client";

import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useToast } from "@/components/ui/use-toast";
import { DialogClose } from "@radix-ui/react-dialog";
import { useState } from "react";

export default function UploadData() {
  // 保存选中的文件对象,而非DataURL
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
  const { toast } = useToast();

  // 处理文件选择
  const handleSelectFile = (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (file) {
      setSelectedFile(file);
    }
  };

  // 处理文件提交
  const handleSubmit = async () => {
    if (!selectedFile) {
      toast({ variant: "destructive", title: "请先选择文件" });
      return;
    }

    try {
      const formData = new FormData();
      formData.append("file", selectedFile);
      formData.append("fileName", selectedFile.name);

      const response = await fetch("/api/ingest", {
        method: "POST",
        body: formData,
        // 无需手动设置Content-Type,浏览器会自动生成multipart/form-data边界
      });

      if (!response.ok) throw new Error("上传失败");

      toast({ title: "文件上传成功" });
      setSelectedFile(null);
    } catch (error: any) {
      toast({
        variant: "destructive",
        title: "上传失败",
        description: error.message || "未知错误",
      });
    }
  };

  return (
    <>
      <Dialog>
        <DialogTrigger asChild>
          <Button>上传文件</Button>
        </DialogTrigger>
        <DialogContent className="sm:max-w-[425px]">
          <DialogHeader>
            <DialogTitle>上传文件</DialogTitle>
            <DialogDescription>选择要上传的文本或PDF文件</DialogDescription>
          </DialogHeader>
          <div className="grid gap-4 py-4">
            <div className="grid grid-cols-4 items-center gap-4">
              <Label htmlFor="file">选择文件</Label>
              <Input
                id="file"
                type="file"
                accept=".txt,.pdf" // 限制文件类型
                onChange={handleSelectFile}
                className="col-span-full"
              />
              {/* 可选:显示选中的文件名 */}
              {selectedFile && (
                <p className="col-span-full text-sm text-gray-500">
                  已选择:{selectedFile.name}
                </p>
              )}
            </div>
          </div>
          <DialogFooter>
            <DialogClose asChild>
              <Button type="button" onClick={handleSubmit} disabled={!selectedFile}>
                提交上传
              </Button>
            </DialogClose>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  );
}

后端API修改(Next.js 13 Route Handler)

修改思路:从FormData中获取文件和文件名,创建BlobClient时使用原文件名;同时添加防覆盖逻辑(文件名后加时间戳)

import { NextResponse } from "next/server";
import { BlobServiceClient } from "@azure/storage-blob";

// 连接Azure Storage账户
const blobService = new BlobServiceClient(
  `https://${process.env.AZURE_STORAGE_ACCOUNT}.blob.core.windows.net/?${process.env.AZURE_STORAGE_SAS_TOKEN}`
);
// 连接容器
const containerClient = blobService.getContainerClient(
  process.env.AZURE_BLOB_STORAGE_CONTAINER
);

// 辅助函数:处理FormData
async function parseFormData(request: Request) {
  const formData = await request.formData();
  const file = formData.get("file") as File;
  const fileName = formData.get("fileName") as string;
  return { file, fileName };
}

// 生成带时间戳的文件名,避免覆盖
function generateUniqueFileName(originalName: string) {
  const [name, ext] = originalName.split(".");
  const timestamp = Date.now();
  return `${name}_${timestamp}.${ext}`;
}

export async function POST(request: Request) {
  try {
    const { file, fileName } = await parseFormData(request);
    // 可选:使用原文件名或带时间戳的唯一文件名
    const blobName = generateUniqueFileName(fileName); // 防覆盖
    // 若不需要防覆盖,直接用 fileName 即可:const blobName = fileName;

    const blobClient = containerClient.getBlockBlobClient(blobName);
    // 将File转为ArrayBuffer
    const buffer = await file.arrayBuffer();

    await blobClient.uploadData(buffer, {
      blobHTTPHeaders: {
        blobContentType: file.type || "",
      },
    });

    return NextResponse.json({
      success: true,
      blobName: blobName,
      message: "文件上传成功",
    });
  } catch (e) {
    console.error("上传错误:", e);
    return NextResponse.json(
      { success: false, message: "上传失败" },
      { status: 500 }
    );
  }
}

关键修改说明

  1. 前端:

    • 保存原始File对象,保留文件名和文件类型信息
    • 使用FormData传递文件,符合HTTP文件上传规范
    • 添加文件类型限制和选中文件提示,提升用户体验
  2. 后端:

    • 从FormData中正确提取文件和文件名
    • 新增generateUniqueFileName函数,通过时间戳生成唯一文件名,彻底解决覆盖问题
    • 正确设置Blob的Content-Type,确保Azure存储的文件类型正确

内容的提问来源于stack exchange,提问作者Bhavesh Kumar Sharma

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 13:05:56