Loading...
Azure File Upload
Author Cloudapp
E.G.

Next.js 14 - Dateiupload mit Dropzone, gestylt mit TailwindCss

2. September 2024
Inhaltsverzeichnis

Azure Blob Storage bietet 5 GB lokal redundanten Speicher (LRS) Hot Block mit 20.000 Lese- und 10.000 Schreibvorgängen jeden Monat für 12 Monate kostenlos. Also, lassen Sie uns ein hübsches Upload-Formular mit Next.js 14 erstellen. Wir werden eine Dropzone und die Möglichkeit, mehrere Dateien gleichzeitig hochzuladen, mit einer schicken Upload-Fortschrittsleiste integrieren.

Hier ist das GitHub Repo mit dem gesamten Code

Verwendeter Stack

Ich werde mit meinem Standard-Stack beginnen:

  • Next.js 14 als Web-Framework

  • TailwindCss for Styling

  • Contentful CMS (Kostenloses Abo)

  • Microsoft Azure für das Erstellen des Storage Accounts

  • Vercel für das Hosting

Hier ist bereits ein Spoiler für das Ergebnis.

Uploadform
Uploadform

Speicherkonten (Azure, Aws, Google)

Azure Blob Storage, AWS S3 (Simple Storage Service) und Google Cloud Storage sind die beliebtesten Cloud-basierten Objektspeicherlösungen, die von Microsoft Azure, Amazon Web Services bzw. Google Cloud Platform bereitgestellt werden. Hier finden Sie eine Aufschlüsselung der Unterschiede zwischen ihnen:

Übersicht

  • Azure Blob Storage: Als Teil von Microsoft Azure ist Blob Storage eine Objektspeicherlösung für die Cloud, die unstrukturierte Daten wie Dokumente, Bilder, Videos und Backups speichert. Es werden verschiedene Arten von Blobs unterstützt, z. B. Block-Blobs (für diskrete Objekte wie Bilder oder PDFs), Append-Blobs (optimiert für Anhängevorgänge wie die Protokollierung) und Page-Blobs (optimiert für zufällige Lese-/Schreibvorgänge).

  • AWS S3 (Einfacher Speicherdienst): Amazon S3 ist der Objektspeicherservice von AWS, der hoch skalierbare, dauerhafte und sichere Speicherung für jede Art von Daten bietet. Er unterstützt eine Vielzahl von Anwendungsfällen, darunter Data Lakes, Backups und Inhaltsspeicher. S3 ist für seine Einfachheit und Robustheit bekannt und bietet verschiedene Speicherklassen wie S3 Standard, S3 Intelligent-Tiering, S3 Glacier, usw.

  • Google Cloud-Speicher: Google Cloud Storage ist Googles einheitlicher Objektspeicher für Entwickler und Unternehmen, der die Speicherung und den Zugriff auf beliebige Datenmengen zu jeder Zeit ermöglicht. Er bietet mehrere Speicherklassen, darunter Standard, Nearline, Coldline und Archive, die auf unterschiedliche Zugriffsanforderungen und Kostenvorlieben abgestimmt sind.

Langlebigkeit und Verfügbarkeit

Alle drei Dienste zeichnen sich durch eine lange Lebensdauer und eine unterschiedlich hohe Verfügbarkeit aus:

Azure Blob Storage: Dauerhaftigkeit: 99,999999999% (11 Neunen) Verfügbarkeit: Zu den Optionen gehören 99,9 % für Hot und Cool und 99,99 % für Read-Access Geo-Redundant Storage (RA-GRS).

AWS S3: Dauerhaftigkeit: 99,999999999% (11 Neunen) Verfügbarkeit: 99,9 % für Standard und 99,99 % für Intelligent-Tiering und Standard.

Google Cloud Storage: Dauerhaftigkeit: 99,999999999% (11 Neunen) Verfügbarkeit: 99,9 % bis 99,95 % je nach Speicherklasse und Redundanztyp.

Modelle zur Preisgestaltung

Die Preise variieren je nach Speicherklasse, Zugriffshäufigkeit und Datenübertragungsanforderungen erheblich:

  • Azure Blob-Speicher: Die Preisgestaltung basiert auf dem Speichertyp (Hot, Cool, Archive), der Redundanzoption, dem Datenabruf und den Vorgängen.

  • AWS S3: Gebühren für Speicher (pro GB), Anforderungskosten (PUT, GET) und Datentransfer. Außerdem gibt es verschiedene Preisstufen für unterschiedliche Speicherklassen.

  • Google Cloud Storage: Ähnlich wie bei AWS richtet sich die Preisgestaltung nach der Speicherklasse (Standard, Nearline, Coldline, Archive), den Vorgängen und dem Netzwerkausgang.

Sicherheit und Compliance

Alle drei Plattformen bieten robuste Sicherheitsfunktionen wie Verschlüsselung im Ruhezustand und bei der Übertragung, Identitäts- und Zugriffsmanagement (IAM), Datenrichtlinien und die Einhaltung globaler Standards (z. B. GDPR, HIPAA).

  • Azure Blob-Speicher: Bietet Azure Active Directory-Integration, Managed Service Identity und Advanced Threat Protection.

  • AWS S3: Bietet S3 Bucket Policies, AWS IAM, AWS KMS für Verschlüsselung und Funktionen wie MFA Delete.

  • Google Cloud-Speicher: Integriert sich mit Cloud IAM, Cloud KMS und unterstützt Object-Level IAM für eine fein abgestufte Zugriffskontrolle.

Einzigartige Merkmale

  • Azure Blob-Speicher: Funktionen wie Soft Delete, Snapshot und Blob Versioning.

  • AWS S3: Bietet S3-Übertragungsbeschleunigung, Glacier Vault Lock und S3 Select für die Abfrage von Daten in S3.

  • Google Cloud Storage: Bietet Object Lifecycle Management, Requester Pays und Signierte URLs.

Beginnen wir mit der Entwicklung

Für dieses Beispiel habe ich mich für Azure entschieden. Die erste Aktion besteht darin, ein Konto und ein entsprechendes Azure-Abonnement zu erstellen.

Azure Kontoerstellung

Am einfachsten ist es, ein kostenloses GitHub-Konto zu erstellen, das Sie für die Erstellung weiterer Konten verwenden können. In diesem Beispiel werden wir ein Azure-Konto mit einem Abonnement zur Erstellung eines Speicherkontos erstellen. Die Erstellung des Azure-Kontos ist kostenlos, aber Sie müssen eine Zahlungsmethode angeben (ich habe eine Kreditkarte verwendet), um den Erstellungsprozess abzuschließen. Gehen Sie auf https://signup.live.com/signup und geben Sie Ihre E-Mail-Adresse, Ihren Namen, Ihre Region und Ihr Geburtsdatum an. Danach erhalten Sie eine Bestätigungsmail/einen OTP-Code, und der Vorgang ist abgeschlossen.

Microsoft-account-Home
Microsoft-account-Home

Dann gehen wir zu portal.azure.com, und Sie sind bereits angemeldet. Microsoft hat bereits ein Abonnement für Sie erstellt, das mit einem neuen Azure AD Directory verbunden ist.

Settings-Microsoft-Azure
Settings-Microsoft-Azure

Klicken Sie nun auf das Hamburger-Menü in der oberen linken Ecke und wählen Sie „Home“, was uns zum folgenden Bildschirm führt.

Home-Azure-Create-Entraid
Home-Azure-Create-Entraid
Storage Account creation
Storage Account creation
Create-a-storage-account-Microsoft-Azure
Create-a-storage-account-Microsoft-Azure

CORS Einstellungen

Nach der Erstellung des Speicherkontos öffnen Sie das Speicherkonto und wählen „Einstellungen“->„Ressourcenfreigabe (CORS)“.

cors
cors

Storage Account Key

Prüfen Sie bitte unter „Einstellungen“->„Konfiguration“, dass „Zugriff auf Speicherkonto-Schlüssel zulassen“ aktiviert und „Version 1.2“ als Mindest-Tls-Version ausgewählt ist. Als letzte Aktion gehen Sie zu „Sicherheit + Netzwerk“ -> „Zugriffsschlüssel“ und klicken Sie auf „Schlüssel1“ anzeigen. Danach sehen Sie das „Kopieren“-Symbol, um den Schlüssel zu erhalten, den wir in unserem Next.js 14-Projekt benötigen.

copy access key
copy access key

Erstellen eines neuen "Container"

Öffnen Sie „Datenspeicher“ und wählen Sie „Container“. Oben links „+ Container“, rechts im Flyout einen Namen festlegen und unten auf „Erstellen“ klicken.

Neue Env Variablen

Für dieses Beispiel benötigen wir drei neue env-Variablen.

# Azure Storage
AZURE_STORAGE_ACCOUNT_NAME=name_of_the_storage_account
AZURE_STORAGE_ACCOUNT_KEY=copied_access_key1
AZURE_STORAGE_CONTAINER_NAME=name_of_the_container

Neue FileUploader Komponente

Unter components/azure/storageaccounts habe ich eine neue Komponente namens „fileuploader.component.tsx“ erstellt.

Neue NPM Pakete

npm i @azure/storage-blob chart.js react-chartjs-2 react-dropzone

Oben befindet sich ein mit „react-chartjs-2“ erstelltes Donut-Diagramm, das uns den „belegten“ und „freien“ Speicherplatz anzeigt. Darunter befindet sich die Dropzone, die auch als Dateiselektor verwendet werden kann. Darunter befinden sich die Schaltfläche „Hochladen“ und drei verschiedene Schaltflächen zur Selbsterklärung, gefolgt von der Liste der hochgeladenen Dateien.

Uploadform
Uploadform
"use client";

import { useState, useEffect, useCallback } from "react";
import { useDropzone } from "react-dropzone";
import { Doughnut } from "react-chartjs-2";
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";

ChartJS.register(ArcElement, Tooltip, Legend);

// Helper function to format file sizes in MB
const formatFileSizeMB = (size: number) => {
  return `${(size / (1024 * 1024)).toFixed(2)} MB`;
};

// Helper function to format dates in a readable format
const formatDate = (dateString: string) => {
  const date = new Date(dateString);
  return date.toLocaleDateString() + " " + date.toLocaleTimeString();
};

interface FileWithProgress {
  file: File;
  progress: number;
  uploaded: boolean;
}

interface UploadedFile {
  name: string;
  size: number;
  url: string;
  createdAt: string; // Added createdAt for displaying creation date
}

const MAX_DISK_SPACE_MB = 5 * 1024; // 5 GB in MB

const FileUploader = () => {
  const [files, setFiles] = useState<FileWithProgress[]>([]);
  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
  // const [uploadedFiles, setUploadedFiles] = useState<
  //   { name: string; size: number; url: string }[]
  // >([]);

  const [totalDiskSpace, setTotalDiskSpace] = useState<number>(0);

  // Fetch the list of uploaded files from the server and calculate total disk space
  const fetchUploadedFiles = async () => {
    const response = await fetch("/api/azure/storageaccount/list");
    const data = await response.json();
    setUploadedFiles(data.files);

    const totalSize = data.files.reduce(
      (acc: number, file: { size: number }) => acc + file.size,
      0
    );
    setTotalDiskSpace(totalSize / (1024 * 1024)); // Convert total size to MB
  };

  // Clear Uploaded File List
  const clearuploadedFileList = () => {
    setFiles([]);
  };

  // Handle file drop
  const onDrop = useCallback((acceptedFiles: File[]) => {
    const newFiles = acceptedFiles.map((file) => ({
      file,
      progress: 0,
      uploaded: false,
    }));
    setFiles((prevFiles) => [...prevFiles, ...newFiles]);
  }, []);

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    multiple: true,
  });

  const uploadFileToAzure = async (file: File, index: number) => {
    const updatedFiles = [...files];

    // Step 1: Request SAS token from the API
    try {
      const response = await fetch("/api/azure/storageaccount/sastoken", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          fileName: file.name,
        }),
      });

      const { uploadUrl, blobUrl } = await response.json();

      // Step 2: Upload the file directly to Azure Blob Storage using the SAS URL
      const xhr = new XMLHttpRequest();
      xhr.open("PUT", uploadUrl, true);
      xhr.setRequestHeader("x-ms-blob-type", "BlockBlob");
      xhr.setRequestHeader("Content-Type", file.type);

      xhr.upload.onprogress = (event) => {
        if (event.lengthComputable) {
          const percentComplete = (event.loaded / event.total) * 100;
          updatedFiles[index].progress = percentComplete;
          setFiles([...updatedFiles]);
        }
      };

      xhr.onload = () => {
        if (xhr.status === 201) {
          // console.log(`File ${file.name} uploaded successfully!`);
          setFiles((prevFiles) => {
            const updatedFiles = [...prevFiles];
            updatedFiles[index].uploaded = true;
            return updatedFiles;
          });
          fetchUploadedFiles(); // Refresh the uploaded files list
        } else {
          console.error(`Error uploading file ${file.name}`);
        }
      };

      xhr.onerror = () => {
        console.error(`File ${file.name} upload failed due to a network error`);
      };

      await new Promise((resolve) => {
        xhr.onloadend = resolve;
        xhr.send(file);
      });
    } catch (error) {
      console.error(`Failed to upload file ${file.name}:`, error);
    }
  };

  const handleUpload = () => {
    files.forEach((fileWithProgress, index) => {
      if (!fileWithProgress.uploaded) {
        uploadFileToAzure(fileWithProgress.file, index);
      }
    });
  };

  // Handle file deletion before upload
  const handleDeleteBeforeUpload = (index: number) => {
    const updatedFiles = [...files];
    updatedFiles.splice(index, 1);
    setFiles(updatedFiles);
  };

  // Handle file deletion after upload
  const handleDeleteAfterUpload = async (fileName: string) => {
    try {
      const response = await fetch(
        `/api/azure/storageaccount/filedelete?fileName=${encodeURIComponent(
          fileName
        )}`,
        {
          method: "DELETE",
        }
      );

      if (response.ok) {
        // console.log(`File ${fileName} deleted successfully`);
        fetchUploadedFiles(); // Refresh the uploaded files list after deletion
      } else {
        console.error(`Failed to delete file ${fileName}`);
      }
    } catch (error) {
      console.error(`Error deleting file ${fileName}:`, error);
    }
  };

  // Handle deletion of all files
  const handleDeleteAll = async () => {
    try {
      const response = await fetch("/api/azure/storageaccount/deleteall", {
        method: "DELETE",
      });

      if (response.ok) {
        // console.log("All files deleted successfully");
        fetchUploadedFiles(); // Refresh the uploaded files list after deletion
      } else {
        console.error("Failed to delete all files");
      }
    } catch (error) {
      console.error("Error deleting all files:", error);
    }
  };

  // Fetch files on component mount
  useEffect(() => {
    fetchUploadedFiles();
  }, []);

  // Data for the donut chart
  const donutChartData = {
    labels: ["Used Space", "Free Space"],
    datasets: [
      {
        data: [totalDiskSpace, MAX_DISK_SPACE_MB - totalDiskSpace],
        backgroundColor: ["#3b82f6", "#e5e7eb"], // Blue for used space, grey for free space
        hoverBackgroundColor: ["#2563eb", "#d1d5db"],
        borderWidth: 1,
      },
    ],
  };

  // Options for the donut chart to place text in the middle
  const donutChartOptions: any = {
    cutout: "70%", // Creates space in the center of the donut
    plugins: {
      tooltip: {
        callbacks: {
          label: function (tooltipItem: any) {
            return `${tooltipItem.label}: ${tooltipItem.raw.toFixed(2)} MB`;
          },
        },
      },
    },
    elements: {
      center: {
        text: `${totalDiskSpace.toFixed(2)} MB / ${MAX_DISK_SPACE_MB.toFixed(
          2
        )} MB`,
        color: "#333", // Center text color
        fontStyle: "Arial", // Center text font style
        sidePadding: 5, // Padding around the center text
      },
    },
  };

  return (
    <div className="max-w-2xl mx-auto mt-8 dark:bg-gray-800 p-6 rounded-lg shadow-md">
      <h1 className="text-3xl font-bold text-center mb-8">
        Multi-File Upload to Azure Storage
        {totalDiskSpace > 0 && (
          <span className="text-lg font-normal">
            {" "}
            ({formatFileSizeMB(totalDiskSpace * 1024 * 1024)} used)
          </span>
        )}
      </h1>

      {/* Donut Chart for Disk Space */}
      <div className="relative mb-8 flex justify-center">
        <div className="h-64 w-64">
          {/* Adjust these values to control the size */}
          <Doughnut data={donutChartData} options={donutChartOptions} />
        </div>

        <div className="absolute inset-0 flex flex-col items-center justify-center">
          <span className="text-lg font-bold dark:text-white text-gray-800">
            {totalDiskSpace.toFixed(2)} MB
          </span>
          <span className="text-sm text-gray-500">
            of {MAX_DISK_SPACE_MB.toFixed(2)} MB
          </span>
        </div>
      </div>

      {/* File Upload Section with Dropzone */}
      <div>
        <h2 className="text-xl font-bold mb-4">Upload Files</h2>
        <div
          {...getRootProps()}
          className={`p-6 border-2 border-dashed rounded-lg cursor-pointer ${
            isDragActive ? "border-blue-500" : "border-gray-300"
          }`}
        >
          <input {...getInputProps()} />
          {isDragActive ? (
            <p className="text-center text-gray-600">Drop the files here...</p>
          ) : (
            <p className="text-center text-gray-600">
              Drag & drop files here, or click to select files
            </p>
          )}
        </div>
        <button
          onClick={handleUpload}
          className={`mt-4 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition ${
            files.length === 0 ? "opacity-50 cursor-not-allowed" : ""
          }`}
          disabled={files.length === 0}
        >
          {files.length === 0 ? "Select files to upload" : "Upload All Files"}
        </button>

        {/* Display Selected Files with Progress and Delete Option */}
        {files.length > 0 && (
          <div className="mt-4">
            <h3 className="text-lg font-bold mb-2">Selected Files</h3>
            <ul className="space-y-2">
              {files.map((fileWithProgress, index) => (
                <li key={index} className="flex items-center space-x-4">
                  {/* File name */}
                  <span className="dark:text-white text-gray-700 w-1/3 break-words">
                    {fileWithProgress.file.name}
                  </span>

                  {/* Progress bar with percentage */}
                  <div className="flex items-center w-1/2">
                    <div className="relative w-full bg-gray-200 rounded-full h-4">
                      <div
                        className="bg-blue-600 h-4 rounded-full"
                        style={{ width: `${fileWithProgress.progress}%` }}
                      ></div>
                      <span className="absolute inset-0 flex justify-center items-center text-white text-sm">
                        {fileWithProgress.progress.toFixed(0)}%
                      </span>
                    </div>
                  </div>

                  {/* Delete button */}
                  <button
                    onClick={() => handleDeleteBeforeUpload(index)}
                    className="text-red-600 hover:text-red-800"
                  >
                    Delete
                  </button>
                </li>
              ))}
            </ul>
          </div>
        )}
      </div>

      {/* Uploaded Files Section with Delete Option */}
      <div className="mt-8">
        <h2 className="text-xl font-bold mb-4">Uploaded Files</h2>
        {uploadedFiles.length > 0 && (
          <>
            <button
              onClick={handleDeleteAll}
              className="mb-4 bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition"
            >
              Delete All Files
            </button>
            <button
              onClick={fetchUploadedFiles}
              className="ml-4 mb-4 bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition"
            >
              Refresh File List
            </button>
            <button
              onClick={clearuploadedFileList}
              className="ml-4 mb-4 bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition"
            >
              Clear Uploaded File List
            </button>
            <ul className="list-disc  pl-6 space-y-2">
              {uploadedFiles.map((file, index) => (
                <li
                  key={index}
                  className="flex items-center justify-between dark:text-white text-gray-700"
                >
                  <span>
                    {file.name} - {formatFileSizeMB(file.size)}
                  </span>
                  <br />
                  <span>Created At: {formatDate(file.createdAt)}</span>{" "}
                  <button
                    onClick={() => handleDeleteAfterUpload(file.name)}
                    className="text-red-600 hover:text-red-800"
                  >
                    Delete
                  </button>
                </li>
              ))}
            </ul>
          </>
        )}
      </div>
    </div>
  );
};

export default FileUploader;

Neue API-Routen für die verschiedenen Aktionen

Wir benötigen 4 neue API-Routen für die Logik von delete, deleteall, list und sastoken. Wir erstellen sie unter app/api/azure/storageaccount

deleteall/route.ts

import { NextResponse, NextRequest } from "next/server";
import {
  BlobServiceClient,
  StorageSharedKeyCredential,
} from "@azure/storage-blob";
import { getToken } from "next-auth/jwt";

export async function DELETE(req: NextRequest) {
  const token = await getToken({ req });

  if (!token) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const {
    AZURE_STORAGE_ACCOUNT_NAME,
    AZURE_STORAGE_ACCOUNT_KEY,
    AZURE_STORAGE_CONTAINER_NAME,
  } = process.env;

  if (
    !AZURE_STORAGE_ACCOUNT_NAME ||
    !AZURE_STORAGE_ACCOUNT_KEY ||
    !AZURE_STORAGE_CONTAINER_NAME
  ) {
    return NextResponse.json(
      { error: "Azure Storage credentials are missing" },
      { status: 500 }
    );
  }

  try {
    const blobServiceClient = new BlobServiceClient(
      `https://${AZURE_STORAGE_ACCOUNT_NAME}.blob.core.windows.net`,
      new StorageSharedKeyCredential(
        AZURE_STORAGE_ACCOUNT_NAME,
        AZURE_STORAGE_ACCOUNT_KEY
      )
    );

    const containerClient = blobServiceClient.getContainerClient(
      AZURE_STORAGE_CONTAINER_NAME
    );

    for await (const blob of containerClient.listBlobsFlat()) {
      const blockBlobClient = containerClient.getBlockBlobClient(blob.name);
      await blockBlobClient.deleteIfExists();
    }

    return NextResponse.json({ message: "All files deleted successfully" });
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to delete all files" },
      { status: 500 }
    );
  }
}

filedelete/route.ts

import { NextResponse, NextRequest } from "next/server";
import {
  BlobServiceClient,
  StorageSharedKeyCredential,
} from "@azure/storage-blob";
import { getToken } from "next-auth/jwt";

export async function DELETE(req: NextRequest) {
  const token = await getToken({ req });

  if (!token) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { searchParams } = new URL(req.url);
  const fileName = searchParams.get("fileName");

  if (!fileName) {
    return NextResponse.json(
      { error: "No file name provided" },
      { status: 400 }
    );
  }

  const {
    AZURE_STORAGE_ACCOUNT_NAME,
    AZURE_STORAGE_ACCOUNT_KEY,
    AZURE_STORAGE_CONTAINER_NAME,
  } = process.env;

  if (
    !AZURE_STORAGE_ACCOUNT_NAME ||
    !AZURE_STORAGE_ACCOUNT_KEY ||
    !AZURE_STORAGE_CONTAINER_NAME
  ) {
    return NextResponse.json(
      { error: "Azure Storage credentials are missing" },
      { status: 500 }
    );
  }

  try {
    const blobServiceClient = new BlobServiceClient(
      `https://${AZURE_STORAGE_ACCOUNT_NAME}.blob.core.windows.net`,
      new StorageSharedKeyCredential(
        AZURE_STORAGE_ACCOUNT_NAME,
        AZURE_STORAGE_ACCOUNT_KEY
      )
    );

    const containerClient = blobServiceClient.getContainerClient(
      AZURE_STORAGE_CONTAINER_NAME
    );
    const blockBlobClient = containerClient.getBlockBlobClient(fileName);

    await blockBlobClient.deleteIfExists();

    return NextResponse.json({
      message: `File ${fileName} deleted successfully`,
    });
  } catch (error) {
    return NextResponse.json(
      { error: `Failed to delete file ${fileName}` },
      { status: 500 }
    );
  }
}

list/route.ts

import { NextResponse, NextRequest } from "next/server";
import {
  BlobServiceClient,
  StorageSharedKeyCredential,
} from "@azure/storage-blob";
import { getToken } from "next-auth/jwt";

export const dynamic = "force-dynamic";

export async function GET(req: NextRequest, res: NextResponse) {
  const token = await getToken({ req });

  if (!token) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const {
    AZURE_STORAGE_ACCOUNT_NAME,
    AZURE_STORAGE_ACCOUNT_KEY,
    AZURE_STORAGE_CONTAINER_NAME,
  } = process.env;

  if (
    !AZURE_STORAGE_ACCOUNT_NAME ||
    !AZURE_STORAGE_ACCOUNT_KEY ||
    !AZURE_STORAGE_CONTAINER_NAME
  ) {
    return NextResponse.json(
      { error: "Azure Storage credentials are missing" },
      { status: 500 }
    );
  }

  try {
    const blobServiceClient = new BlobServiceClient(
      `https://${AZURE_STORAGE_ACCOUNT_NAME}.blob.core.windows.net`,
      new StorageSharedKeyCredential(
        AZURE_STORAGE_ACCOUNT_NAME,
        AZURE_STORAGE_ACCOUNT_KEY
      )
    );

    const containerClient = blobServiceClient.getContainerClient(
      AZURE_STORAGE_CONTAINER_NAME
    );
    const blobs = [];

    for await (const blob of containerClient.listBlobsFlat()) {
      blobs.push({
        name: blob.name,
        size: blob.properties.contentLength || 0, // Size in bytes
        createdAt: blob.properties.createdOn,
      });
    }

    return NextResponse.json({ files: blobs });
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to list files" },
      { status: 500 }
    );
  }
}

sastoken/route.ts

Wir müssen einen SAS-Token-Link für den direkten Upload auf Azure generieren, da sonst der Upload von Dateien mit einer Größe von mehr als 4,5 MB fehlschlägt (Limit der Vercel Serverless-Funktion).

import {
  BlobServiceClient,
  StorageSharedKeyCredential,
  generateBlobSASQueryParameters,
  ContainerSASPermissions,
} from "@azure/storage-blob";
import { NextResponse, NextRequest } from "next/server";

export async function POST(req: NextRequest, res: NextResponse) {
  const body = await req.json();
  const fileName = body.fileName as string;

  const {
    AZURE_STORAGE_ACCOUNT_NAME,
    AZURE_STORAGE_ACCOUNT_KEY,
    AZURE_STORAGE_CONTAINER_NAME,
  } = process.env;

  if (
    !AZURE_STORAGE_ACCOUNT_NAME ||
    !AZURE_STORAGE_ACCOUNT_KEY ||
    !AZURE_STORAGE_CONTAINER_NAME
  ) {
    return NextResponse.json(
      { error: "Azure Storage credentials are missing" },
      { status: 500 }
    );
  }

  try {
    const blobServiceClient = new BlobServiceClient(
      `https://${AZURE_STORAGE_ACCOUNT_NAME}.blob.core.windows.net`,
      new StorageSharedKeyCredential(
        AZURE_STORAGE_ACCOUNT_NAME,
        AZURE_STORAGE_ACCOUNT_KEY
      )
    );

    const containerName = AZURE_STORAGE_CONTAINER_NAME;
    const containerClient = blobServiceClient.getContainerClient(
      AZURE_STORAGE_CONTAINER_NAME
    );

    const blobName = fileName;
    const blobClient = containerClient.getBlockBlobClient(blobName);

    const sasToken = generateBlobSASQueryParameters(
      {
        containerName,
        blobName,
        permissions: ContainerSASPermissions.parse("cwr"), // Create, Write, Read permissions
        startsOn: new Date(),
        expiresOn: new Date(new Date().valueOf() + 3600 * 1000), // 1 hour
      },
      blobServiceClient.credential as StorageSharedKeyCredential
    ).toString();

    return NextResponse.json({
      uploadUrl: `${blobClient.url}?${sasToken}`,
      blobUrl: blobClient.url,
    });
  } catch (error) {
    return NextResponse.json({ error: "File upload failed!" }, { status: 500 });
  }
}

Alle benötigten Routen wurden mit next-auth und Azure AD B2C gesichert. Genießen Sie das Widget, das großartig funktioniert und auch schön aussieht, und passen Sie es Ihren Bedürfnissen entsprechend an.

Cloudapp-dev und bevor Sie uns verlassen

Danke, dass Sie bis zum Ende gelesen haben. Noch eine Bitte bevor Sie gehen:

Wenn Ihnen gefallen hat was Sie gelesen haben oder wenn es Ihnen sogar geholfen hat, dann würden wir uns über einen "Clap" 👏 oder einen neuen Follower auf unseren Medium Account sehr freuen.

Oder folgen Sie uns auf Twitter -> Cloudapp.dev

Verwandte Artikel