177 lines
4.8 KiB
TypeScript
177 lines
4.8 KiB
TypeScript
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import * as os from 'os';
|
||
import AdmZip from 'adm-zip';
|
||
import axios from 'axios';
|
||
import { Logger } from '@nestjs/common';
|
||
|
||
export class ZipHandler {
|
||
private static readonly logger = new Logger(ZipHandler.name);
|
||
|
||
/**
|
||
* 下载并解压.zip文件,提取3D模型文件
|
||
* @param zipUrl ZIP文件的URL
|
||
* @param outputDir 输出目录(默认为系统临时目录)
|
||
* @returns 提取的3D模型文件路径、预览图路径和文件Buffer
|
||
*/
|
||
static async downloadAndExtract(
|
||
zipUrl: string,
|
||
outputDir?: string,
|
||
): Promise<{
|
||
modelPath: string;
|
||
previewPath?: string;
|
||
modelBuffer: Buffer;
|
||
previewBuffer?: Buffer;
|
||
}> {
|
||
// 使用系统临时目录
|
||
const baseDir =
|
||
outputDir ||
|
||
path.join(os.tmpdir(), 'ai-3d', Date.now().toString());
|
||
|
||
try {
|
||
if (!fs.existsSync(baseDir)) {
|
||
fs.mkdirSync(baseDir, { recursive: true });
|
||
}
|
||
|
||
// 1. 下载ZIP文件
|
||
this.logger.log(`开始下载ZIP文件: ${zipUrl}`);
|
||
const zipPath = path.join(baseDir, 'model.zip');
|
||
await this.downloadFile(zipUrl, zipPath);
|
||
this.logger.log(`ZIP文件下载完成: ${zipPath}`);
|
||
|
||
// 2. 解压ZIP文件
|
||
this.logger.log(`开始解压ZIP文件`);
|
||
const extractDir = path.join(baseDir, 'extracted');
|
||
await this.extractZip(zipPath, extractDir);
|
||
this.logger.log(`ZIP文件解压完成: ${extractDir}`);
|
||
|
||
// 3. 查找3D模型文件和预览图
|
||
const files = this.getAllFiles(extractDir);
|
||
const modelFile = this.findModelFile(files);
|
||
const previewFile = this.findPreviewImage(files);
|
||
|
||
if (!modelFile) {
|
||
throw new Error('在ZIP文件中未找到3D模型文件(.glb, .gltf)');
|
||
}
|
||
|
||
this.logger.log(`找到3D模型文件: ${modelFile}`);
|
||
if (previewFile) {
|
||
this.logger.log(`找到预览图: ${previewFile}`);
|
||
}
|
||
|
||
// 4. 读取文件Buffer(用于上传到COS)
|
||
const modelBuffer = fs.readFileSync(modelFile);
|
||
const previewBuffer = previewFile ? fs.readFileSync(previewFile) : undefined;
|
||
|
||
return {
|
||
modelPath: modelFile,
|
||
previewPath: previewFile,
|
||
modelBuffer,
|
||
previewBuffer,
|
||
};
|
||
} catch (error) {
|
||
this.logger.error(`处理ZIP文件失败: ${error.message}`, error.stack);
|
||
throw error;
|
||
} finally {
|
||
// 清理临时目录
|
||
try {
|
||
if (fs.existsSync(baseDir)) {
|
||
fs.rmSync(baseDir, { recursive: true, force: true });
|
||
this.logger.log(`已清理临时目录: ${baseDir}`);
|
||
}
|
||
} catch (err) {
|
||
this.logger.warn(`清理临时目录失败: ${err.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 下载文件
|
||
*/
|
||
private static async downloadFile(
|
||
url: string,
|
||
outputPath: string,
|
||
): Promise<void> {
|
||
const response = await axios.get(url, {
|
||
responseType: 'arraybuffer',
|
||
timeout: 60000, // 60秒超时
|
||
});
|
||
|
||
fs.writeFileSync(outputPath, response.data);
|
||
}
|
||
|
||
/**
|
||
* 解压ZIP文件
|
||
*/
|
||
private static async extractZip(
|
||
zipPath: string,
|
||
outputDir: string,
|
||
): Promise<void> {
|
||
const zip = new AdmZip(zipPath);
|
||
zip.extractAllTo(outputDir, true);
|
||
}
|
||
|
||
/**
|
||
* 递归获取目录下的所有文件
|
||
*/
|
||
private static getAllFiles(dir: string): string[] {
|
||
const files: string[] = [];
|
||
|
||
const traverse = (currentDir: string) => {
|
||
const items = fs.readdirSync(currentDir);
|
||
|
||
for (const item of items) {
|
||
const fullPath = path.join(currentDir, item);
|
||
const stat = fs.statSync(fullPath);
|
||
|
||
if (stat.isDirectory()) {
|
||
traverse(fullPath);
|
||
} else {
|
||
files.push(fullPath);
|
||
}
|
||
}
|
||
};
|
||
|
||
traverse(dir);
|
||
return files;
|
||
}
|
||
|
||
/**
|
||
* 查找3D模型文件(.glb, .gltf)
|
||
*/
|
||
private static findModelFile(files: string[]): string | undefined {
|
||
// 优先查找.glb文件(二进制格式,更常用)
|
||
const glbFile = files.find((file) => file.toLowerCase().endsWith('.glb'));
|
||
if (glbFile) return glbFile;
|
||
|
||
// 其次查找.gltf文件
|
||
const gltfFile = files.find((file) =>
|
||
file.toLowerCase().endsWith('.gltf'),
|
||
);
|
||
if (gltfFile) return gltfFile;
|
||
|
||
// 其他可能的3D格式
|
||
const otherFormats = ['.obj', '.fbx', '.stl'];
|
||
for (const format of otherFormats) {
|
||
const file = files.find((f) => f.toLowerCase().endsWith(format));
|
||
if (file) return file;
|
||
}
|
||
|
||
return undefined;
|
||
}
|
||
|
||
/**
|
||
* 查找预览图(.jpg, .jpeg, .png)
|
||
*/
|
||
private static findPreviewImage(files: string[]): string | undefined {
|
||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp'];
|
||
|
||
for (const ext of imageExtensions) {
|
||
const imageFile = files.find((file) => file.toLowerCase().endsWith(ext));
|
||
if (imageFile) return imageFile;
|
||
}
|
||
|
||
return undefined;
|
||
}
|
||
}
|