在对象存储系统中,元数据(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();
通过上述代码,我们定义了三个模型:基础模型、任务模型和对象元数据模型。这三个模型分别用于存储上传任务信息和文件的元数据信息。在接下来的章节中,我们将继续探讨如何利用这些模型来完善我们的对象存储系统。
更新接口以支持元数据
在这一部分中,我们将更新文件上传、下载和预览等接口,以支持元数据的管理和操作。
文件上传
单文件和多文件上传
-
更新文件名生成逻辑
- 在上传文件时,生成唯一的存储文件名,并将其与原始文件名一起保存。
-
保存文件的元数据信息
- 在文件上传成功后,保存文件的元数据,包括文件ID、原始文件名、存储文件名、文件路径、文件大小、文件类型、文件的MD5值、创建时间、修改时间、创建人和修改人等信息。
-
更新文件上传接口的代码示例
/* 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" });
}
});
};
分片上传
-
初始化上传任务
- 创建上传任务并生成唯一的任务ID。
- 计算文件的总分片数量,并为每个任务创建一个目录来存储分片文件。
-
分片上传逻辑
- 每个分片上传时,验证其MD5值以确保数据完整性。
- 在所有分片上传完成后,合并分片文件生成最终的文件,并保存文件的元数据信息。
-
更新分片上传接口的代码示例
/* 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 });
}
};
文件下载
修改文件列表
-
返回文件的元数据信息
- 在获取文件列表时,返回每个文件的元数据信息,包括文件ID、原始文件名、存储文件名、文件路径、文件大小、文件类型、创建时间和修改时间等信息。
-
更新文件列表接口的代码示例
/* 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 });
}
};
修改文件下载
-
通过ID下载文件
- 在下载文件时,通过文件ID查找文件的元数据信息,并根据文件路径返回文件内容。
-
更新文件下载接口的代码示例
/* 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 });
}
};
修改文件预览
-
通过ID预览文件
- 在预览文件时,通过文件ID查找文件的元数据信息,并根据文件路径返回文件内容。
-
更新文件预览接口的代码示例
/* 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>