欢迎阅读手写Express+TS手写对象存储专栏。这系列文章旨在为您提供一个全面的指南,带您一步步构建一个功能齐全的对象存储系统。对象存储系统在大数据时代发挥着重要的作用,能够高效地管理和存储大量非结构化数据,如图片、视频和文档等。
这是专栏中的第四节,本文将带您深入了解文件上传与下载功能的实现。我们将从基础的文件上传与下载开始,逐步介绍如何配置和使用multer处理文件上传,并实现支持分片上传和断点续传的高级功能。此外,我们还将编写高效的文件下载接口,优化文件读取和传输过程,确保用户体验的流畅和系统的稳定。
通过本章的学习,读者将能够掌握文件处理的核心技术,应用于各种实际项目中。
接口文档
文件上传接口
| 名称 | 地址 | 方法 | 功能说明 | 角色 | 是否要求登录 |
|---|---|---|---|---|---|
| 单文件上传 | /api/files/upload/single | POST | 上传单个文件 | 登录用户 | 是 |
| 多文件上传 | /api/files/upload/multiple | POST | 上传多个文件 | 登录用户 | 是 |
| 分片上传 | /api/files/upload/chunk | POST | 上传文件分片 | 登录用户 | 是 |
文件下载接口
| 名称 | 地址 | 方法 | 功能说明 | 角色 | 是否要求登录 |
|---|---|---|---|---|---|
| 文件下载 | /api/files/download/:fileName | GET | 下载指定文件 | 登录用户 | 是 |
| 文件列表 | /api/files/list | GET | 获取文件列表 | 登录用户 | 是 |
| 文件预览 | /api/files/preview/:fileName | GET | 预览指定文件 | 登录用户 | 是 |
上传接口实现
配置中间件
安装multer
首先,我们需要安装multer库。multer是一个Node.js中间件,用于处理multipart/form-data类型的表单数据,主要用于文件上传。
npm install multer
npm i --save-dev @types/multer
配置multer
接下来,我们需要配置multer以支持文件上传。我们会创建一个上传文件夹来存储上传的文件,并配置multer中间件。
// src/config/multerConfig.ts
import multer from 'multer';
// 设置上传文件夹
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`);
},
});
const upload = multer({ storage });
export default upload;
单/多文件上传
这部分我们在前两天的文章@2024年了,express中文件上传有变化吗?中讲过就不多赘述了,基本就是一个逻辑
创建上传接口
我们将创建一个文件上传的API接口,并使用multer中间件处理文件上传。
// src/controllers/fileController.ts
import { Request, Response } from 'express';
import upload from '../config/multerConfig';
// 单文件上传
export const uploadSingleFile = (req: Request, res: Response) => {
upload.single('file')(req, res, (err) => {
if (err) {
return res.status(500).json({ message: 'File upload failed', error: err.message });
}
return res.status(200).json({ message: 'File uploaded successfully', file: req.file });
});
};
// 多文件上传
export const uploadMultipleFiles = (req: Request, res: Response) => {
upload.array('files', 10)(req, res, (err) => {
if (err) {
return res.status(500).json({ message: 'File upload failed', error: err.message });
}
return res.status(200).json({ message: 'Files uploaded successfully', files: req.files });
});
};
创建文件上传路由
我们需要在Express应用中创建路由来处理文件上传请求。这里使用了authMiddleware中间件来保护路由
// src/routes/fileRoutes.ts
import express from 'express';
import { uploadSingleFile, uploadMultipleFiles } from '../controllers/fileController';
const router = express.Router();
router.post('/upload/single', authMiddleware, uploadSingleFile);
router.post('/upload/multiple', authMiddleware, uploadMultipleFiles);
export default router;
在主应用中使用文件上传路由
最后,我们需要在主应用中引入并使用文件上传路由。
import express from "express";
import cors from "cors";
import morgan from "morgan";
import helloRoute from "./routes/helloRoute";
import { initializeMongoose } from "./dao/mongodb";
import authRoutes from "./routes/authRoutes";
import userRoutes from "./routes/userRoutes";
import fileRoutes from "./routes/fileRoutes";
const app = express();
const port = process.env.PORT || 3000;
// 初始化
initializeMongoose();
// 配置中间件
app.use(express.json()); // 使用 Express 内置的 JSON 解析中间件
app.use(express.urlencoded({ extended: true })); // 使用 Express 内置的 URL 编码解析中间件
app.use(cors());
app.use(morgan("dev"));
// 配置路由
app.use("/api", helloRoute);
app.use("/api/auth", authRoutes);
app.use("/api/user", userRoutes);
app.use('/api/files', fileRoutes);
app.get("/", (req, res) => {
res.send("Hello, Object Storage System!");
});
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
export { app };
分片上传
这里是我们本章的重点——分片上传。这是一种将大型文件分割成多个小片段(分片),然后分别上传这些片段的技术。这种方法可以有效地提高上传大文件的效率和可靠性。下面是分片上传的基本原理及其实现步骤:
基本原理
- 文件分割:将一个大文件分割成多个较小的片段(分片)。
- 并行上传:同时上传多个分片,以提高上传速度。
- 断点续传:如果上传过程中出现网络中断或其他问题,可以从中断点继续上传,而不需要重新上传整个文件。
- 合并分片:所有分片上传完成后,服务器端将这些分片重新合并成一个完整的文件。
实现步骤
- 分割文件:
- 客户端将大文件按固定大小(例如1MB或5MB)分割成若干个小片段。
- 每个分片都会被赋予一个唯一的标识符(如序号)。
- 上传分片:
- 客户端通过多个并行的HTTP请求上传这些分片。
- 每个请求包含分片的内容以及相关的元数据信息(如文件ID、分片序号、总分片数等)。
- 断点续传:
- 在上传过程中,客户端会记录已成功上传的分片信息。
- 如果上传过程中断,客户端可以查询已上传的分片,并从未完成的分片开始继续上传。
- 分片合并:
- 当所有分片上传完成后,服务器端接收到“完成上传”的请求。
- 服务器端根据分片的顺序将其合并成一个完整的文件。
优点
- 提高上传效率:通过并行上传多个分片,可以显著减少上传时间。
- 增强可靠性:即使网络中断,只需重新上传失败的分片,而不是整个文件。
- 适应大文件上传:能够处理超出单次上传限制的大文件。
接口设计
理论上应该写三个接口,创建任务/查询任务、上传分片、合并分片。这里比较懒上传分片和合并分片放在一个里面了,当然现在怎么写一起的后面就得怎么拆开~
-
初始化上传任务
- 客户端调用此接口来创建一个新的分片上传任务。
- 服务器生成一个唯一的任务ID,并返回给客户端。
POST /upload/initiate Content-Type: application/json { "fileName": "largefile.zip", "fileSize": 104857600 // 文件大小(字节) }响应示例:
{ "taskId": "unique-task-id", "totalChunks": 100 } -
上传分片
- 客户端上传每个分片时,携带任务ID、分片索引、总分片数、文件名和当前分片的MD5值等信息。
- 服务器接收并保存每个分片,并验证分片的完整性。
- 检查任务是否所有分片都已经完成,已完成的自动合并
POST /upload/chunk Content-Type: multipart/form-data { "taskId": "unique-task-id", "chunkIndex": 0, "totalChunks": 100, "fileName": "largefile.zip", "chunkMd5": "md5-value-of-chunk", "chunk": <file chunk data> }响应示例:
{ "message": "Chunk uploaded successfully" }
代码实现
npm i uuid
npm i --save-dev @types/uuid
初始化上传任务的控制器逻辑
// src/controllers/fileController.ts
import { Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
import path from 'path';
import fs from 'fs';
const UPLOAD_DIR = path.resolve(__dirname, "../../uploads");
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
// 开始任务
export const initiateUpload = (req: Request, res: Response) => {
const { fileName, fileSize } = req.body;
const taskId = uuidv4();
const totalChunks = Math.ceil(fileSize / CHUNK_SIZE);
console.log(`Initiating upload for ${fileName}, taskId: ${taskId}`);
// 创建任务目录
const taskDir = path.join(UPLOAD_DIR, taskId);
if (!fs.existsSync(taskDir)) {
fs.mkdirSync(taskDir, { recursive: true });
}
res.status(200).json({ taskId, totalChunks });
};
上传分片的控制器逻辑
// src/controllers/fileController.ts
import { Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
const UPLOAD_DIR = path.resolve(__dirname, "../../uploads");
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
// 分片上传
export const uploadChunk = (req: Request, res: Response) => {
const { taskId, chunkIndex, totalChunks, fileName, chunkMd5 } = req.body;
const chunk = req.file;
if (!chunk) {
res.status(400).send({ message: "File upload failed" });
return;
}
const chunkDir = path.join(UPLOAD_DIR, taskId);
if (!fs.existsSync(chunkDir)) {
res.status(400).send({ message: "Invalid task ID" });
return;
}
const chunkPath = path.join(chunkDir, `${fileName}-chunk-${chunkIndex}`);
const fileBuffer = fs.readFileSync(chunk.path);
const computedMd5 = createHash("md5").update(fileBuffer).digest("hex");
if (computedMd5 !== chunkMd5) {
res.status(400).send({ message: "MD5 mismatch" });
return;
}
fs.renameSync(chunk.path, chunkPath);
console.log(`Chunk ${chunkIndex} of ${totalChunks} uploaded`);
// 检查所有分片是否都已上传完毕
const uploadedChunks = fs
.readdirSync(chunkDir)
.filter((file) => file.startsWith(fileName + "-chunk-")).length;
console.log(
`Uploaded chunks: ${uploadedChunks}/${totalChunks}`,
uploadedChunks === totalChunks
);
if (uploadedChunks === Number(totalChunks)) {
console.log(`All chunks uploaded, starting merge for ${fileName}`);
const filePath = path.join(UPLOAD_DIR, fileName);
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", () => {
console.log(`Merged chunk ${currentChunk}`);
currentChunk++;
appendNextChunk();
});
readStream.on("error", (err) => {
console.error(`Error reading chunk ${currentChunk}: ${err}`);
res.status(500).send({ message: "Error merging chunks" });
writeStream.end();
});
} else {
writeStream.end();
}
};
appendNextChunk();
};
mergeChunks();
writeStream.on("finish", () => {
fs.rm(chunkDir, { recursive: true, force: true }, (err) => {
if (err) {
console.error(`Failed to remove chunk directory: ${err}`);
} else {
console.log(`Chunk directory ${chunkDir} removed successfully`);
}
});
console.log(`File ${fileName} uploaded successfully`);
res.status(200).json({ message: "File uploaded successfully" });
});
writeStream.on("error", (err) => {
console.error(`Error writing file: ${err}`);
res.status(500).send({ message: "Error merging chunks" });
});
} else {
res.status(200).json({ message: "Chunk uploaded successfully" });
}
};
添加路由
// src/routes/fileRoutes.ts
import { Router } from 'express';
import multer from 'multer';
import { initiateUpload, uploadChunk } from '../controllers/fileController';
import authMiddleware from '../middleware/authMiddleware';
const upload = multer({ dest: 'uploads/' });
const router = Router();
router.post('/upload/initiate', authMiddleware, initiateUpload);
router.post('/upload/chunk', authMiddleware, upload.single('chunk'), uploadChunk);
export default router;
// 添加分片上传路由 src/routes/fileRoutes.ts
router.post('/upload/chunk', authMiddleware, upload.single('chunk'), uploadChunk);
下载接口实现
文件列表
我们需要创建一个控制器来处理获取文件列表的请求,通过fs.readdir读取存储上传文件的目录,返回目录下的文件列表。
// src/controllers/fileController.ts
import { Request, Response } from 'express';
import path from 'path';
import fs from 'fs';
const UPLOAD_DIR = path.resolve(__dirname, '../../uploads');
export const getFileList = (req: Request, res: Response) => {
fs.readdir(UPLOAD_DIR, (err, files) => {
if (err) {
res.status(500).json({ message: 'Unable to retrieve files', error: err.message });
} else {
res.status(200).json({ files });
}
});
};
关键代码解释:
fs.readdir(UPLOAD_DIR, (err, files) => {...}): 使用 Node.js 的文件系统模块读取指定目录。
文件下载
我们需要创建一个文件下载的API接口,支持文件的读取和传输,通过文件名参数读取文件并将其传输给客户端,实现文件下载功能。
// src/controllers/fileController.ts
import { Request, Response } from 'express';
import path from 'path';
import fs from 'fs';
const UPLOAD_DIR = path.resolve(__dirname, '../../uploads');
export const downloadFile = (req: Request, res: Response) => {
const { fileName } = req.params;
const filePath = path.join(UPLOAD_DIR, fileName);
if (fs.existsSync(filePath)) {
const safeFileName = path.basename(fileName); // 确保文件名安全
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" });
}
};
关键代码解释:
const { fileName } = req.params: 从请求参数中获取文件名。const filePath = path.join(UPLOAD_DIR, fileName): 构建文件的完整路径。if (fs.existsSync(filePath)) {...}: 检查文件是否存在。res.setHeader("Content-Disposition",attachment; filename*=UTF-8''${encodeURIComponent(safeFileName)}): 设置响应头,提示浏览器下载文件。const fileStream = fs.createReadStream(filePath): 创建文件读取流。fileStream.pipe(res): 将文件流传输给响应对象。
文件预览
我们需要创建一个文件预览的API接口,支持文件的读取和在线展示。通过文件名参数读取文件并将其传输给客户端,实现文件预览功能。
// src/controllers/fileController.ts
import { Request, Response } from 'express';
import path from 'path';
import fs from 'fs';
const UPLOAD_DIR = path.resolve(__dirname, '../../uploads');
export const previewFile = (req: Request, res: Response) => {
const { fileName } = req.params;
const filePath = path.join(UPLOAD_DIR, fileName);
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" });
}
};
关键代码解释:
if (fs.existsSync(filePath)) {...}: 检查文件是否存在。const fileStream = fs.createReadStream(filePath): 创建文件读取流。fileStream.pipe(res): 将文件流传输给响应对象。
更新文件路由
// src/routes/fileRoutes.ts
import { Router } from 'express';
import { getFileList, downloadFile, previewFile } from '../controllers/fileController';
const router = Router();
router.get('/list', getFileList);
router.get('/download/:fileName', downloadFile);
router.get('/preview/:fileName', previewFile);
export default router;
通过以上步骤,我们已经实现了文件列表获取、文件下载和文件预览的基本功能。
前端实现
上传和下载目前的前端时候都会比较简单,因为暂时没有经历写前端ha
分片上传
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chunk Upload Example</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: 600px;
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,
ElMessage,
ElRow,
ElCol,
ElIcon,
UploadFilled,
} = ElementPlus;
const App = {
components: {
ElUpload,
ElButton,
ElContainer,
ElHeader,
ElMain,
ElMessage,
ElRow,
ElCol,
ElIcon,
UploadFilled,
},
template: `
<el-container>
<el-header>分片上传示例</el-header>
<el-main>
<el-row>
<el-col :span="24" class="upload-section">
<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-main>
</el-container>
`,
setup() {
const file = ref(null);
const fileName = ref("");
const beforeUpload = (rawFile) => {
file.value = rawFile;
fileName.value = rawFile.name;
return false; // 阻止默认的上传行为
};
const handleUpload = async () => {
if (!file.value) {
ElMessage.warning("Please select a file first.");
return;
}
console.log("File Name: " + file.value.name);
console.log("File Size: " + file.value.size + " bytes");
console.log("File Type: " + file.value.type);
console.log("Last Modified Date: " + file.value.lastModifiedDate);
const initResponse = await fetch(
"http://localhost:3000/api/files/upload/initiate",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
fileName: file.value.name,
fileSize: file.value.size,
}),
}
);
console.log("Upload task started successfully");
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);
await fetch("http://localhost:3000/api/files/upload/chunk", {
method: "POST",
body: formData,
});
console.log(`Chunk ${i + 1}/${totalChunks} uploaded`);
}
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);
});
};
return {
file,
fileName,
beforeUpload,
handleUpload,
};
},
};
const app = createApp(App);
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>
代码逻辑
-
文件选择逻辑:
beforeUpload方法在文件选择时被调用。它将选中的文件保存到file变量中,并将文件名保存到fileName变量中。返回false以阻止默认的上传行为。
-
文件上传逻辑:
handleUpload方法执行文件上传操作:- 如果没有选中文件,显示警告消息。
- 打印文件信息。
- 向服务器发送初始化上传任务的请求,获取任务 ID 和总分片数。
- 以 5MB 为一个分片大小,循环处理文件分片。
- 计算每个分片的 MD5 值。
- 将分片数据和其他必要信息通过
FormData发送到服务器的上传分片接口。 - 上传完成后显示成功消息。
-
计算文件分片的 MD5 值:
calculateMd5方法使用FileReader读取文件分片的内容,并使用SparkMD5库计算 MD5 值。
通过以上步骤,代码实现了一个简单的文件分片上传功能,并在选择文件后显示文件名。
文件下载
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>File List and Download</title>
<!-- 引入 Vue 3 -->
<script src="https://unpkg.com/vue@3.2.47/dist/vue.global.prod.js"></script>
<!-- 引入 Element Plus -->
<link
rel="stylesheet"
href="https://unpkg.com/element-plus/dist/index.css"
/>
<script src="https://unpkg.com/element-plus/dist/index.full.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { createApp } = Vue;
const {
ElContainer,
ElHeader,
ElMain,
ElTable,
ElTableColumn,
ElButton,
ElMessage,
} = ElementPlus;
const App = {
template: `
<el-container>
<el-header>
<h2>文件列表</h2>
</el-header>
<el-main>
<el-table :data="files" style="width: 100%">
<el-table-column prop="name" label="文件名"></el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button
type="primary"
size="small"
@click="downloadFile(scope.row.name)"
>下载</el-button>
<el-button
type="success"
size="small"
@click="previewFile(scope.row.name)"
>预览</el-button>
</template>
</el-table-column>
</el-table>
</el-main>
</el-container>
`,
data() {
return {
files: [],
};
},
methods: {
async fetchFiles() {
try {
const response = await fetch(
"http://localhost:3000/api/files/list"
);
const data = await response.json();
this.files = data.files.map((file) => ({ name: file }));
} catch (error) {
ElMessage.error("获取文件列表失败");
}
},
downloadFile(fileName) {
const link = document.createElement("a");
link.href = `http://localhost:3000/api/files/download/${fileName}`;
link.setAttribute("download", fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
previewFile(fileName) {
window.open(`http://localhost:3000/api/files/preview/${fileName}`);
},
},
mounted() {
this.fetchFiles();
},
};
const app = createApp(App);
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>
代码解释
fetchFiles:异步获取文件列表数据。向服务器发送请求获取文件列表,并将返回的数据保存在files数组中。如果请求失败,显示错误消息。downloadFile:创建一个隐藏的链接元素,设置其href属性为文件的下载地址,并模拟点击该链接以触发下载,然后移除该链接。previewFile:打开一个新窗口,预览指定文件。
更新主文件,加入静态资源
src/index.ts内更新,加入express.static中间件。
上传页面:http://localhost:3000/up.html
文件列表:http://localhost:3000/list.html
import express from "express";
import cors from "cors";
import morgan from "morgan";
import helloRoute from "./routes/helloRoute";
import { initializeMongoose } from "./dao/mongodb";
import authRoutes from "./routes/authRoutes";
import userRoutes from "./routes/userRoutes";
import fileRoutes from "./routes/fileRoutes";
const app = express();
const port = process.env.PORT || 3000;
// 初始化
initializeMongoose();
// 配置中间件
app.use(express.json()); // 使用 Express 内置的 JSON 解析中间件
app.use(express.urlencoded({ extended: true })); // 使用 Express 内置的 URL 编码解析中间件
app.use(cors());
app.use(morgan("dev"));
// 静态文件
app.use(express.static('public')); // ------------------------------配置静态文件目录
app.use(express.static('web')); // ------------------------------配置web文件目录
// 配置路由
app.use("/api", helloRoute);
app.use("/api/auth", authRoutes);
app.use("/api/user", userRoutes);
app.use('/api/files', fileRoutes);
app.get("/", (req, res) => {
res.send("Hello, Object Storage System!");
});
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
export { app };
总结
本文详细探讨了如何在一个基于Node.js和TypeScript的Web应用中实现文件上传与下载功能。我们首先介绍了单文件和多文件上传的基本实现,然后深入探讨了分片上传的原理与实现步骤,确保大文件上传的高效性和可靠性。同时,我们也实现了文件列表、文件下载和文件预览功能,为用户提供了完整的文件管理体验。
在后续第5章中,我们将探讨对象存储系统中的元数据管理。元数据在对象存储系统中扮演着关键角色,它能够提供关于数据的丰富描述和管理功能。我们将介绍元数据的定义、存储和查询方法,并展示如何在实际项目中有效地使用元数据管理文件和对象。通过本章的学习,读者将进一步理解和掌握对象存储系统的核心概念和技术。