文件上传业务
- 前端发送文件名、MD5,、文件大小等信息提交后端文件服务。多文件多请求。
const { getUploadUrl } = useFileUpload();
async function onFileSelection(file: File) {
// ... 一些字段处理
await getUploadUrl(file);
}
- 后端文件服务从 OBS 获取对应文件上传 URL,该 URL 短期有效,且只能上传该 MD5、文件名的文件,上传目录为临时目录。
// file.service.ts
@Controller('file')
export class FileController {
constructor(private readonly fileService: FileService) {}
}
async getUploadUrl(fileName: string, fileSize: number, md5: string) {
// ...生成上传URL逻辑...
return {
url,
key
}
}
- 后端文件服务返回上传URL与后端数据库唯一 Key 给前端。
- 前端通过 URL 上传文件,通过 key 提交后到后端,通知文件上传完成。
- 前端通过业务表单,表单中文件部分用文件 key 表示。
- 后端获取 key 后,发送请求值文件服务,文件服务请求 OBS 把文件从临时文件夹移动到到具体业务文件夹。
文件查看业务
- 前端请求业务内容,业务内容包括文件内容。
- 后端通过数据库中存储的 Key 调用文件服务获取 URL。
- 文件服务通过 key 查询具体存放路径和文件名,从 OBS 获取临时 URL,URL 会在一定时间内过期。
- 文件服务通过控制器返回 URL 给前端。
- 前端通过 URL 获取内容。
一些伪代码
// useFileUpload.ts
import { ref } from "vue";
import axios from "axios";
import { MD5 } from "crypto";
export function useFileUpload() {
const uploadUrl = ref<string>("");
const fileKey = ref<string>("");
const isUploading = ref<boolean>(false);
// 计算文件的MD5值
async function calculateFileMD5(file: File): Promise<string> {
const arrayBuffer = await file.arrayBuffer();
const wordArray = MD5(arrayBuffer);
return wordArray.toString();
}
// 获取上传URL和文件Key
async function getUploadUrl(file: File) {
const { name, size } = file;
const md5 = await calculateFileMD5(file);
const response = await axios.post("/api/file/upload-url", {
fileName: name,
fileSize: size,
md5: md5,
});
uploadUrl.value = response.data.uploadUrl;
fileKey.value = response.data.key;
}
// 上传文件到获取的URL
async function uploadFile(file: File) {
if (!uploadUrl.value) return;
isUploading.value = true;
const formData = new FormData();
formData.append("file", file);
await axios.put(uploadUrl.value, formData, {
headers: {
"Content-Type": file.type,
},
});
isUploading.value = false;
}
// 通知后端文件上传完成
async function notifyUploadCompletion() {
if (!fileKey.value) return;
await axios.post("/api/file/upload-complete", {
key: fileKey.value,
});
}
return {
getUploadUrl,
uploadFile,
notifyUploadCompletion,
fileKey,
isUploading,
};
}
// dto.ts
export class GetUploadUrlDto {
fileName: string;
fileSize: number;
md5: string;
}
export class UploadCompleteDto {
key: string;
}
// file.service.ts
import { Injectable, Inject } from "@nestjs/common";
import { Client, InjectMinio } from "minio";
@Injectable()
export class FileService {
constructor(
@InjectMinio(MINIO_CONNECTION) private readonly minioClient: Minio.Client
) {}
// 获取上传URL
async getUploadUrl({ fileName, fileSize, md5 }: GetUploadUrlD) {
const bucketName = "temporary-bucket";
const objectName = `temp/${md5}_${fileName}`;
const metaData = {
"Content-Type": "application/octet-stream",
"Content-Length": fileSize.toString(),
"Content-MD5": md5,
};
// 生成临时上传URL
const uploadUrl = await this.minioClient.presignedPutObject(
bucketName,
objectName,
300
); // 5分钟有效期
const key = `${md5}_${fileName}`;
// 假设你有一个方法存储Key到数据库
await this.storeKeyInDatabase(key);
return { uploadUrl, key };
}
// 处理文件上传完成
async handleUploadCompletion(key: string) {
const srcBucket = "temporary-bucket";
const destBucket = "business-bucket";
const srcObject = `temp/${key}`;
const destObject = `business/${key}`;
// 进行文件复制和移动 (伪代码)
await this.minioClient.copyObject(
destBucket,
destObject,
`/${srcBucket}/${srcObject}`,
{
// 复制对象时也可以设置一些元数据
}
);
// 删除源文件
await this.minioClient.removeObject(srcBucket, srcObject);
// 更新数据库状态
await this.updateFileStatusInDatabase(key, "moved");
}
// 存储Key到数据库(伪代码)
async storeKeyInDatabase(key: string) {
// 存储到数据库逻辑
}
// 更新文件状态到数据库(伪代码)
async updateFileStatusInDatabase(key: string, status: string) {
// 更新数据库逻辑
}
}
<template>
<div>
<input type="file" @change="handleFileChange" />
<button @click="submitForm" :disabled="isUploading">Submit</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { useFileUpload } from "./useFileUpload";
export default defineComponent({
setup() {
const {
getUploadUrl,
uploadFile,
notifyUploadCompletion,
fileKey,
isUploading,
} = useFileUpload();
const selectedFile = ref<File | null>(null);
// 处理文件选择
const handleFileChange = async (event: Event) => {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
selectedFile.value = input.files[0];
await getUploadUrl(selectedFile.value);
}
};
// 提交表单逻辑
const submitForm = async () => {
if (selectedFile.value) {
await uploadFile(selectedFile.value);
await notifyUploadCompletion();
// 提交业务表单逻辑
const formData = {
// 其他业务表单数据
key: fileKey.value,
};
await axios.post("/api/xxx", formData);
}
};
return {
handleFileChange,
submitForm,
isUploading,
};
},
});
</script>