手写对象存储:基础模型封装和元数据管理

83 阅读10分钟

image.png

在对象存储系统中,元数据(Metadata)扮演着至关重要的角色。它不仅帮助我们更好地描述和管理数据,还在数据的检索和使用中起到了关键作用。本章将带您深入了解元数据的概念及其在对象存储系统中的应用。

元数据的概念

元数据,简单来说,就是关于数据的数据。它提供了数据的背景信息,使我们能够更好地理解和使用这些数据。例如,在一个文件存储系统中,文件的元数据可能包括文件名、文件大小、创建时间、修改时间、文件类型等。

元数据在对象存储系统中的重要性

在对象存储系统中,元数据的作用尤为重要。它不仅帮助我们高效地管理和检索存储的对象,还能提供丰富的上下文信息,从而提升系统的整体性能和用户体验。通过元数据,我们可以实现更精细的数据管理和更快捷的数据访问。

元数据概念与作用

什么是元数据?

定义

元数据是描述数据的附加信息。它为数据提供了上下文,使得数据本身更有意义。元数据可以是结构化的(如数据库表中的字段描述)或非结构化的(如文档中的元信息)。

示例

在一个文件存储系统中,文件的元数据可能包括以下信息:

  • 文件名(originalFileName)
  • 存储文件名(storageFileName)
  • 文件路径(filePath)
  • 文件大小(fileSize)
  • 文件类型(fileType)
  • 文件的MD5值(fileMd5)
  • 创建时间(createdAt)
  • 修改时间(updatedAt)
  • 创建人(createdBy)
  • 修改人(updatedBy)
  • 版本号(version)

元数据在对象存储中的作用

数据描述

元数据可以详细描述存储对象的属性,使得每个对象都有其独特的标识和描述信息。这有助于用户快速了解对象的基本信息,而不必读取对象本身的数据。

数据管理

通过元数据,我们可以实现对存储对象的有效管理。元数据提供的创建时间、修改时间、文件大小等信息,可以帮助我们进行数据的分类、排序和筛选,从而提高数据管理的效率。

数据检索

元数据在数据检索中起到了关键作用。通过索引元数据,我们可以快速定位和访问存储对象,而不必遍历整个存储系统。这大大提升了数据检索的速度和准确性。

定义元数据模型

基础模型

作用

基础模型 (BaseModel) 提供了一个通用的、可复用的基础数据结构和方法,以便在不同的业务模型中继承和使用。它包括常见的字段和 CRUD 操作方法,减少重复代码,提高开发效率和代码一致性。

字段
  • createdAt: 创建时间
  • updatedAt: 修改时间
  • createdBy: 创建人
  • updatedBy: 修改人
  • version: 版本号
代码
/* eslint-disable @typescript-eslint/no-explicit-any */
import mongoose, {
  Schema,
  Document,
  Model,
  FilterQuery,
  QueryOptions,
} from "mongoose";

// 定义接口
export interface IBaseDocument extends Document {
  deletedAt?: Date;
  createdAt: Date;
  updatedAt: Date;
  createdBy?: mongoose.Types.ObjectId;
  updatedBy?: mongoose.Types.ObjectId;
  version: number;
}
// 扩展 FilterQuery 类型
type ExtendedFilterQuery<T> = FilterQuery<T> & {
  deletedAt?: { $exists: boolean };
};

export const baseSchemaDict = {
  createdAt: {
    type: Date,
    default: Date.now,
  },
  updatedAt: {
    type: Date,
    default: Date.now,
  },
  deletedAt: {
    type: Date,
  },
  createdBy: {
    type: mongoose.Types.ObjectId,
    ref: "User",
  },
  updatedBy: {
    type: mongoose.Types.ObjectId,
    ref: "User",
  },
  version: {
    type: Number,
    default: 0,
  },
};

// 定义基础Schema
const baseSchema = new Schema<IBaseDocument>(baseSchemaDict);

// 中间件:保存前更新updatedAt和版本号
baseSchema.pre<IBaseDocument>("save", function (next) {
  this.updatedAt = new Date();
  this.version += 1;
  next();
});

// 封装基础Model类
class BaseModel<T extends Document> {
  private model: Model<T>;

  constructor(
    modelName: string,
    schemaDef: mongoose.SchemaDefinition,
    options: mongoose.SchemaOptions = {}
  ) {
    // 合并基础Schema和自定义Schema
    const schema = new Schema(
      {
        ...schemaDef,
        ...baseSchema.obj,
      },
      options
    );

    this.model = mongoose.model<T>(modelName, schema);
  }

  // 提供对 model 的访问方法
  protected getModel(): Model<T> {
    return this.model;
  }

  // 创建文档
  async create(
    doc: Partial<T>,
    userId?: mongoose.Types.ObjectId | null
  ): Promise<T> {
    if (userId) {
      (doc as any).createdBy = userId;
      (doc as any).updatedBy = userId;
    }
    return await this.model.create(doc);
  }

  // 更新文档
  async update(
    id: mongoose.Types.ObjectId,
    update: Partial<T>,
    userId: mongoose.Types.ObjectId
  ): Promise<T | null> {
    (update as any).updatedBy = userId;
    return await this.model.findByIdAndUpdate(id, update, { new: true }).exec();
  }

  // 删除文档
  async delete(
    id: mongoose.Types.ObjectId,
    userId?: mongoose.Types.ObjectId
  ): Promise<T | null> {
    return await this.model
      .findByIdAndUpdate(
        id,
        {
          deletedAt: new Date(),
          updatedBy: userId,
        },
        { new: true }
      )
      .exec();
  }

  // 查询文档
  async find(query: ExtendedFilterQuery<T>, options: QueryOptions = {}) {
    query.deletedAt = { $exists: false };
    return await this.model.find(query, options).exec();
  }

  // 根据ID查询文档
  async findById(
    id: mongoose.Types.ObjectId,
    options: QueryOptions = {}
  ): Promise<T | null> {
    const query: ExtendedFilterQuery<T> = {
      _id: id,
      deletedAt: { $exists: false },
    };
    return await this.model.findOne(query, options).exec();
  }

  // 根据查询条件查找一个文档
  async findOne(
    query: ExtendedFilterQuery<T>,
    options: QueryOptions = {}
  ): Promise<T | null> {
    query.deletedAt = { $exists: false };
    return await this.model.findOne(query, options).exec();
  }

  // 查询所有文档,包括逻辑删除的文档
  async findAllIncludingDeleted(
    query: FilterQuery<T>,
    options: QueryOptions = {}
  ) {
    return await this.model.find(query, options).exec();
  }
}

export default BaseModel;

任务模型

作用

任务模型 (TaskModel) 用于存储文件上传任务的信息。它记录了每个上传任务的详细信息,包括任务ID、文件名、文件大小、分片数量、创建时间、修改时间、创建人、修改人和版本号等。

字段
  • taskId: 任务ID,唯一标识一个上传任务
  • fileName: 文件名
  • fileSize: 文件大小
  • totalChunks: 文件的分片数量
代码
import mongoose from "mongoose";
import BaseModel, { IBaseDocument } from "./BaseModel";

// 定义任务接口
export interface ITask extends IBaseDocument {
  taskId: string;
  fileName: string;
  fileSize: number;
  totalChunks: number;
}

// 定义任务Schema
const taskSchemaDefinition: mongoose.SchemaDefinition = {
  taskId: { type: String, required: true, unique: true },
  fileName: { type: String, required: true },
  fileSize: { type: Number, required: true },
  totalChunks: { type: Number, required: true },
};

// 创建任务模型类
class TaskModel extends BaseModel<ITask> {
  constructor() {
    super("Task", taskSchemaDefinition, { timestamps: true });
  }
}

export default new TaskModel();

对象元数据模型

作用

对象元数据模型 (ObjectMetadataModel) 用于存储文件的元数据信息。它记录了每个文件的详细信息,包括文件的原始名称、存储名称、文件路径、文件大小、文件类型、文件的MD5值、创建时间、修改时间、创建人、修改人和版本号等。

定义对象元数据模型的字段
  • fileId: 文件ID,唯一标识一个文件
  • originalFileName: 原始文件名
  • storageFileName: 存储文件名
  • filePath: 文件路径
  • fileSize: 文件大小
  • fileType: 文件类型
  • fileMd5: 文件的MD5值
创建对象元数据模型的代码示例
import mongoose from "mongoose";
import BaseModel, { IBaseDocument } from "./BaseModel";

// 定义对象元数据接口
export interface IObjectMetadata extends IBaseDocument {
  originalFileName: string;
  storageFileName: string;
  filePath: string;
  fileSize: number;
  fileType: string;
  fileMd5: string;
}

// 定义对象元数据Schema
const objectMetadataSchemaDefinition: mongoose.SchemaDefinition = {
  originalFileName: { type: String, required: true },
  storageFileName: { type: String, required: true },
  filePath: { type: String, required: true },
  fileSize: { type: Number, required: true },
  fileType: { type: String, required: true },
  fileMd5: { type: String, required: true },
};

// 创建对象元数据模型类
class ObjectMetadataModel extends BaseModel<IObjectMetadata> {
  constructor() {
    super("ObjectMetadata", objectMetadataSchemaDefinition, {
      timestamps: true,
    });
  }
}

export default new ObjectMetadataModel();

通过上述代码,我们定义了三个模型:基础模型、任务模型和对象元数据模型。这三个模型分别用于存储上传任务信息和文件的元数据信息。在接下来的章节中,我们将继续探讨如何利用这些模型来完善我们的对象存储系统。

更新接口以支持元数据

在这一部分中,我们将更新文件上传、下载和预览等接口,以支持元数据的管理和操作。

文件上传

单文件和多文件上传
  1. 更新文件名生成逻辑

    • 在上传文件时,生成唯一的存储文件名,并将其与原始文件名一起保存。
  2. 保存文件的元数据信息

    • 在文件上传成功后,保存文件的元数据,包括文件ID、原始文件名、存储文件名、文件路径、文件大小、文件类型、文件的MD5值、创建时间、修改时间、创建人和修改人等信息。
  3. 更新文件上传接口的代码示例

/* eslint-disable @typescript-eslint/no-explicit-any */
// src/controllers/files/uploadController.ts
import path from "path";
import fs from "fs";
import crypto from "crypto";
import { Request, Response } from "express";
import { v4 as uuidv4 } from "uuid";
import TaskModel from "../../models/TaskModel";
import ObjectMetadataModel from "../../models/FileModel";
import mongoose from "mongoose";
import upload from "../../config/multerConfig";

const UPLOAD_DIR = path.resolve(__dirname, "../../uploads");
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB


// 单文件上传
export const uploadSingleFile = (req: Request, res: Response) => {
  upload.single("file")(req, res, async (err) => {
    if (err) {
      return res
        .status(500)
        .json({ message: "File upload failed", error: err.message });
    }
    const file = req.file;
    if (file) {
      try {
        const userId = req.user
          ? new mongoose.Types.ObjectId(req.user.userId)
          : undefined;
        const fileInfo = {
          originalFileName: file.originalname,
          storageFileName: file.filename,
          filePath: file.path,
          fileSize: file.size,
          fileType: file.mimetype,
          fileMd5: "", // 可以在后续计算并更新
          createdBy: userId,
          updatedBy: userId,
        };

        const createdFile = await ObjectMetadataModel.create(fileInfo, userId);
        return res
          .status(200)
          .json({ message: "File uploaded successfully", file: createdFile });
      } catch (error) {
        return res.status(500).json({
          message: "File upload failed",
          error: (error as Error).message,
        });
      }
    } else {
      return res.status(400).json({ message: "No file uploaded" });
    }
  });
};

// 多文件上传
export const uploadMultipleFiles = (req: Request, res: Response) => {
  upload.array("files", 10)(req, res, async (err) => {
    if (err) {
      return res
        .status(500)
        .json({ message: "File upload failed", error: err.message });
    }

    const files = req.files as Express.Multer.File[];
    if (files && files.length > 0) {
      try {
        const userId = req.user
          ? new mongoose.Types.ObjectId(req.user.userId)
          : undefined;
        const fileInfos = await Promise.all(
          files.map(async (file) => {
            const fileInfo = {
              originalFileName: file.originalname,
              storageFileName: file.filename,
              filePath: file.path,
              fileSize: file.size,
              fileType: file.mimetype,
              fileMd5: "", // 可以在后续计算并更新
            };
            return await ObjectMetadataModel.create(fileInfo, userId);
          })
        );
        return res
          .status(200)
          .json({ message: "Files uploaded successfully", files: fileInfos });
      } catch (error) {
        return res.status(500).json({
          message: "Files upload failed",
          error: (error as Error).message,
        });
      }
    } else {
      return res.status(400).json({ message: "No files uploaded" });
    }
  });
};
分片上传
  1. 初始化上传任务

    • 创建上传任务并生成唯一的任务ID。
    • 计算文件的总分片数量,并为每个任务创建一个目录来存储分片文件。
  2. 分片上传逻辑

    • 每个分片上传时,验证其MD5值以确保数据完整性。
    • 在所有分片上传完成后,合并分片文件生成最终的文件,并保存文件的元数据信息。
  3. 更新分片上传接口的代码示例

/* eslint-disable @typescript-eslint/no-explicit-any */
// src/controllers/files/uploadController.ts
import path from "path";
import fs from "fs";
import crypto from "crypto";
import { Request, Response } from "express";
import { v4 as uuidv4 } from "uuid";
import TaskModel from "../../models/TaskModel";
import ObjectMetadataModel from "../../models/FileModel";
import mongoose from "mongoose";
import upload from "../../config/multerConfig";
import { getFileExtension } from "../../utils/file";

const UPLOAD_DIR = path.resolve(__dirname, "../../../uploads");
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB

// 开始任务
export const initiateUpload = async (req: Request, res: Response) => {
  const { fileName, fileSize } = req.body;
  const taskId = uuidv4();
  const totalChunks = Math.ceil(fileSize / CHUNK_SIZE);
  const userId = req.user
    ? new mongoose.Types.ObjectId(req.user.userId)
    : undefined;
  try {
    const task = await TaskModel.create(
      {
        taskId,
        fileName,
        fileSize,
        totalChunks,
      },
      userId
    );

    // 创建任务目录
    const taskDir = path.join(UPLOAD_DIR, taskId);
    if (!fs.existsSync(taskDir)) {
      fs.mkdirSync(taskDir, { recursive: true });
    }

    res.status(200).json({ taskId, totalChunks, task: task.version });
  } catch (error: any) {
    res
      .status(500)
      .json({ message: "Failed to initiate upload", error: error.message });
  }
};

// 分片上传
export const uploadChunk = async (req: Request, res: Response) => {
  const { taskId, chunkIndex, totalChunks, fileName, chunkMd5 } = req.body;
  const userId = req.user
    ? new mongoose.Types.ObjectId(req.user.userId)
    : undefined;
  const chunk = req.file;
  if (!chunk) {
    res.status(400).send({ message: "File upload failed" });
    return;
  }

  try {
    const task = await TaskModel.findOne({ taskId });
    if (!task) {
      res.status(400).send({ message: "Invalid task ID" });
      return;
    }

    const chunkDir = path.join(UPLOAD_DIR, taskId);
    const chunkPath = path.join(chunkDir, `${fileName}-chunk-${chunkIndex}`);
    const fileBuffer = fs.readFileSync(chunk.path);
    const computedMd5 = crypto
      .createHash("md5")
      .update(fileBuffer)
      .digest("hex");

    if (computedMd5 !== chunkMd5) {
      res.status(400).send({ message: "MD5 mismatch" });
      return;
    }

    fs.renameSync(chunk.path, chunkPath);

    // 检查所有分片是否都已上传完毕
    const uploadedChunks = fs
      .readdirSync(chunkDir)
      .filter((file) => file.startsWith(fileName + "-chunk-")).length;

    if (uploadedChunks === Number(totalChunks)) {
      // 所有分片上传完成,开始合并文件
      const storageFileName = uuidv4().toString() + getFileExtension(fileName);
      const filePath = path.join(UPLOAD_DIR, storageFileName);
      const writeStream = fs.createWriteStream(filePath);

      const mergeChunks = () => {
        let currentChunk = 0;

        const appendNextChunk = () => {
          if (currentChunk < totalChunks) {
            const chunkFilePath = path.join(
              chunkDir,
              `${fileName}-chunk-${currentChunk}`
            );
            const readStream = fs.createReadStream(chunkFilePath);

            readStream.pipe(writeStream, { end: false });
            readStream.on("end", () => {
              currentChunk++;
              appendNextChunk();
            });
            readStream.on("error", (err) => {
              res
                .status(500)
                .send({ message: "Error merging chunks", error: err.message });
              writeStream.end();
            });
          } else {
            writeStream.end();
          }
        };

        appendNextChunk();
      };

      mergeChunks();

      writeStream.on("finish", async () => {
        fs.rm(chunkDir, { recursive: true, force: true }, async (err) => {
          if (err) {
            res.status(500).send({
              message: "Failed to remove chunk directory",
              error: err.message,
            });
            return;
          }
          // 创建对象元数据记录
          const objectMetadata = await ObjectMetadataModel.create(
            {
              originalFileName: fileName,
              storageFileName,
              filePath,
              fileSize: task.fileSize,
              fileType: chunk.mimetype,
              fileMd5: computedMd5,
            },
            userId
          );

          res.status(200).json({
            message: "File uploaded successfully",
            fileId: objectMetadata._id,
          });
        });
      });

      writeStream.on("error", (err) => {
        res
          .status(500)
          .send({ message: "Error merging chunks", error: err.message });
      });
    } else {
      res.status(200).json({ message: "Chunk uploaded successfully" });
    }
  } catch (error: any) {
    res
      .status(500)
      .json({ message: "File upload failed", error: error.message });
  }
};

文件下载

修改文件列表
  1. 返回文件的元数据信息

    • 在获取文件列表时,返回每个文件的元数据信息,包括文件ID、原始文件名、存储文件名、文件路径、文件大小、文件类型、创建时间和修改时间等信息。
  2. 更新文件列表接口的代码示例

/* eslint-disable @typescript-eslint/no-explicit-any */
// src/controllers/files/listController.ts
import { Request, Response } from "express";
import ObjectMetadataModel from "../../models/FileModel";

export const getFileList = async (req: Request, res: Response) => {
  try {
    const files = await ObjectMetadataModel.find({});
    res.status(200).json(files);
  } catch (error: any) {
    res
      .status(500)
      .json({ message: "Unable to retrieve files", error: error.message });
  }
};
修改文件下载
  1. 通过ID下载文件

    • 在下载文件时,通过文件ID查找文件的元数据信息,并根据文件路径返回文件内容。
  2. 更新文件下载接口的代码示例

/* eslint-disable @typescript-eslint/no-explicit-any */
// src/controllers/files/listController.ts
import { Request, Response } from "express";
import path from "path";
import fs from "fs";
import ObjectMetadataModel from "../../models/FileModel";

export const downloadFile = async (req: Request, res: Response) => {
  const { fileId } = req.params;

  try {
    const fileMetadata = await ObjectMetadataModel.findOne({
      fileId,
    });
    if (!fileMetadata) {
      res.status(404).json({ message: "File not found" });
      return;
    }

    const filePath = fileMetadata.filePath;
    if (fs.existsSync(filePath)) {
      const safeFileName = path.basename(fileMetadata.originalFileName);
      res.setHeader(
        "Content-Disposition",
        `attachment; filename*=UTF-8''${encodeURIComponent(safeFileName)}`
      );
      const fileStream = fs.createReadStream(filePath);
      fileStream.pipe(res);
      fileStream.on("error", (err) => {
        res
          .status(500)
          .json({ message: "File download failed", error: err.message });
      });
    } else {
      res.status(404).json({ message: "File not found" });
    }
  } catch (error: any) {
    res
      .status(500)
      .json({ message: "File download failed", error: error.message });
  }
};
修改文件预览
  1. 通过ID预览文件

    • 在预览文件时,通过文件ID查找文件的元数据信息,并根据文件路径返回文件内容。
  2. 更新文件预览接口的代码示例

/* eslint-disable @typescript-eslint/no-explicit-any */
// src/controllers/files/listController.ts
import { Request, Response } from "express";
import path from "path";
import fs from "fs";
import ObjectMetadataModel from "../../models/FileModel";

export const previewFile = async (req: Request, res: Response) => {
  const { fileId } = req.params;

  try {
    const fileMetadata = await ObjectMetadataModel.findOne({
      fileId,
    });
    if (!fileMetadata) {
      res.status(404).json({ message: "File not found" });
      return;
    }

    const filePath = fileMetadata.filePath;
    if (fs.existsSync(filePath)) {
      const fileStream = fs.createReadStream(filePath);
      fileStream.pipe(res);
      fileStream.on("error", (err) => {
        res
          .status(500)
          .json({ message: "File preview failed", error: err.message });
      });
    } else {
      res.status(404).json({ message: "File not found" });
    }
  } catch (error: any) {
    res
      .status(500)
      .json({ message: "File preview failed", error: error.message });
  }
};

通过这些更新,文件上传、下载和预览接口将能够处理和返回文件的元数据信息,从而实现更高效和精细的文件管理。

附件

这次带了登录和状态等功能,分片上传和列表浏览都有

ore.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>File Management System</title>
    <!-- 引入 Element Plus 样式 -->
    <link
      rel="stylesheet"
      href="https://unpkg.com/element-plus@2.7.6/dist/index.css"
    />
    <style>
      body {
        font-family: Arial, sans-serif;
        background-color: #f0f2f5;
      }
      #app {
        max-width: 800px;
        margin: 50px auto;
        background-color: white;
        border-radius: 8px;
        box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
        overflow: hidden;
      }
      .el-header {
        background-color: #409eff;
        color: white;
        padding: 20px;
        text-align: center;
        font-size: 24px;
      }
      .el-main {
        padding: 20px;
      }
      .file-name {
        text-align: center;
        margin-top: 10px;
        font-size: 16px;
        color: #333;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>

    <!-- 引入 Vue 3 -->
    <script src="https://unpkg.com/vue@3.2.47/dist/vue.global.prod.js"></script>
    <!-- 引入 Element Plus -->
    <script src="https://unpkg.com/element-plus@2.7.6/dist/index.full.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
    <script>
      const { createApp, ref } = Vue;
      const {
        ElUpload,
        ElButton,
        ElContainer,
        ElHeader,
        ElMain,
        ElTable,
        ElTableColumn,
        ElMessage,
        ElRow,
        ElCol,
        ElIcon,
        ElForm,
        ElFormItem,
        ElInput,
        UploadFilled,
      } = ElementPlus;

      const App = {
        components: {
          ElUpload,
          ElButton,
          ElContainer,
          ElHeader,
          ElMain,
          ElTable,
          ElTableColumn,
          ElMessage,
          ElRow,
          ElCol,
          ElIcon,
          ElForm,
          ElFormItem,
          ElInput,
          UploadFilled,
        },
        template: `
          <el-container>
            <el-header>文件管理系统</el-header>
            <el-main>
              <el-row v-if="!isLoggedIn">
                <el-col :span="24">
                  <h2>登录</h2>
                  <el-form :model="loginForm" @submit.native.prevent="handleLogin">
                    <el-form-item label="邮箱">
                      <el-input v-model="loginForm.email"></el-input>
                    </el-form-item>
                    <el-form-item label="密码">
                      <el-input type="password" v-model="loginForm.password"></el-input>
                    </el-form-item>
                    <el-form-item>
                      <el-button type="primary" @click="handleLogin">登录</el-button>
                    </el-form-item>
                  </el-form>
                </el-col>
              </el-row>

              <el-row v-else>
                <el-col :span="24">
                  <h2>分片上传</h2>
                  <el-upload
                    class="upload-demo"
                    drag
                    :before-upload="beforeUpload"
                    :show-file-list="false"
                    multiple
                  >
                    <el-icon><upload-filled /></el-icon>
                    <div class="el-upload__text">
                      拖拽到此处 或者 <em>点击选择文件</em>
                    </div>
                  </el-upload>
                  <div class="file-name" v-if="fileName">
                    当前选中的文件: {{ fileName }}
                  </div>
                  <div style="width:100%;text-align: center;">
                    <el-button type="success" @click="handleUpload" style="margin-top:20px;">上传</el-button>
                  </div>
                </el-col>
              </el-row>

              <el-row v-if="isLoggedIn">
                <el-col :span="24">
                  <h2>文件列表</h2>
                  <el-table :data="files" style="width: 100%">
                    <el-table-column prop="originalFileName" label="文件名"></el-table-column>
                    <el-table-column label="操作">
                      <template #default="scope">
                        <el-button
                          type="primary"
                          size="small"
                          @click="downloadFile(scope.row._id, scope.row.originalFileName)"
                        >下载</el-button>
                        <el-button
                          type="success"
                          size="small"
                          @click="previewFile(scope.row._id)"
                        >预览</el-button>
                      </template>
                    </el-table-column>
                  </el-table>
                </el-col>
              </el-row>
              
            </el-main>
          </el-container>
        `,
        setup() {
          const file = ref(null);
          const fileName = ref("");
          const files = ref([]);
          const isLoggedIn = ref(false);
          const loginForm = ref({
            email: "",
            password: "",
          });

          const beforeUpload = (rawFile) => {
            file.value = rawFile;
            fileName.value = rawFile.name;
            return false; // 阻止默认的上传行为
          };

          const handleUpload = async () => {
            if (!file.value) {
              ElMessage.warning("请先选择一个文件。");
              return;
            }

            console.log("文件名: " + file.value.name);
            console.log("文件大小: " + file.value.size + " bytes");
            console.log("文件类型: " + file.value.type);
            console.log("最后修改日期: " + file.value.lastModifiedDate);

            const initResponse = await fetch(
              "http://localhost:3000/api/files/upload/initiate",
              {
                method: "POST",
                headers: {
                  "Content-Type": "application/json",
                  Authorization: `Bearer ${localStorage.getItem("token")}`,
                },
                body: JSON.stringify({
                  fileName: file.value.name,
                  fileSize: file.value.size,
                }),
              }
            );

            if (!initResponse.ok) {
              ElMessage.error("初始化上传任务失败。");
              return;
            }

            console.log("上传任务成功启动");
            const initData = await initResponse.json();
            const { taskId, totalChunks } = initData;

            const chunkSize = 5 * 1024 * 1024; // 5MB
            for (let i = 0; i < totalChunks; i++) {
              const start = i * chunkSize;
              const end = Math.min(file.value.size, start + chunkSize);
              const chunk = file.value.slice(start, end);

              const chunkMd5 = await calculateMd5(chunk);

              const formData = new FormData();
              formData.append("taskId", taskId);
              formData.append("chunkIndex", i.toString());
              formData.append("totalChunks", totalChunks.toString());
              formData.append("fileName", file.value.name);
              formData.append("chunkMd5", chunkMd5);
              formData.append("chunk", chunk);

              const chunkResponse = await fetch(
                "http://localhost:3000/api/files/upload/chunk",
                {
                  method: "POST",
                  headers: {
                    Authorization: `Bearer ${localStorage.getItem("token")}`,
                  },
                  body: formData,
                }
              );

              if (!chunkResponse.ok) {
                ElMessage.error(`分片 ${i + 1}/${totalChunks} 上传失败。`);
                return;
              }

              console.log(`分片 ${i + 1}/${totalChunks} 已上传`);
            }
            ElMessage.success("上传成功!");
          };

          const calculateMd5 = (fileChunk) => {
            return new Promise((resolve, reject) => {
              const reader = new FileReader();
              reader.onload = function (event) {
                const spark = new SparkMD5.ArrayBuffer();
                spark.append(event.target.result);
                resolve(spark.end());
              };
              reader.onerror = function (event) {
                reject(event.target.error);
              };
              reader.readAsArrayBuffer(fileChunk);
            });
          };

          const fetchFiles = async () => {
            try {
              const response = await fetch(
                "http://localhost:3000/api/files/list",
                {
                  headers: {
                    Authorization: `Bearer ${localStorage.getItem("token")}`,
                  },
                }
              );
              const data = await response.json();
              files.value = data; // 假设返回的就是文件元数据数组
            } catch (error) {
              ElMessage.error("获取文件列表失败");
            }
          };

          const downloadFile = (_id, originalFileName) => {
            const link = document.createElement("a");
            link.href = `http://localhost:3000/api/files/download/${_id}`;
            link.setAttribute("download", originalFileName);
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
          };

          const previewFile = (_id) => {
            window.open(`http://localhost:3000/api/files/preview/${_id}`);
          };

          const handleLogin = async () => {
            try {
              const response = await fetch(
                "http://localhost:3000/api/auth/login",
                {
                  method: "POST",
                  headers: {
                    "Content-Type": "application/json",
                  },
                  body: JSON.stringify(loginForm.value),
                }
              );

              if (!response.ok) {
                ElMessage.error("登录失败,请检查您的凭证。");
                return;
              }

              const data = await response.headers;
              localStorage.setItem("token", data.get("token"));
              isLoggedIn.value = true;
              ElMessage.success("登录成功!");
              fetchFiles();
            } catch (error) {
              ElMessage.error("登录失败,请重试。");
            }
          };

          if (localStorage.getItem("token")) {
            isLoggedIn.value = true;
            fetchFiles();
          }

          return {
            file,
            fileName,
            files,
            isLoggedIn,
            loginForm,
            beforeUpload,
            handleUpload,
            downloadFile,
            previewFile,
            handleLogin,
          };
        },
      };

      const app = createApp(App);
      app.use(ElementPlus);
      app.mount("#app");
    </script>
  </body>
</html>