2026-01-14 10:06:08 +08:00
|
|
|
|
import * as fs from 'fs';
|
|
|
|
|
|
import * as path from 'path';
|
2026-01-16 16:35:43 +08:00
|
|
|
|
import * as os from 'os';
|
2026-01-14 17:00:06 +08:00
|
|
|
|
import AdmZip from 'adm-zip';
|
2026-01-14 10:06:08 +08:00
|
|
|
|
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
|
2026-01-16 16:35:43 +08:00
|
|
|
|
* @param outputDir 输出目录(默认为系统临时目录)
|
|
|
|
|
|
* @returns 提取的3D模型文件路径、预览图路径和文件Buffer
|
2026-01-14 10:06:08 +08:00
|
|
|
|
*/
|
|
|
|
|
|
static async downloadAndExtract(
|
|
|
|
|
|
zipUrl: string,
|
|
|
|
|
|
outputDir?: string,
|
|
|
|
|
|
): Promise<{
|
|
|
|
|
|
modelPath: string;
|
|
|
|
|
|
previewPath?: string;
|
2026-01-16 16:35:43 +08:00
|
|
|
|
modelBuffer: Buffer;
|
|
|
|
|
|
previewBuffer?: Buffer;
|
2026-01-14 10:06:08 +08:00
|
|
|
|
}> {
|
2026-01-16 16:35:43 +08:00
|
|
|
|
// 使用系统临时目录
|
|
|
|
|
|
const baseDir =
|
|
|
|
|
|
outputDir ||
|
|
|
|
|
|
path.join(os.tmpdir(), 'ai-3d', Date.now().toString());
|
|
|
|
|
|
|
2026-01-14 10:06:08 +08:00
|
|
|
|
try {
|
|
|
|
|
|
if (!fs.existsSync(baseDir)) {
|
|
|
|
|
|
fs.mkdirSync(baseDir, { recursive: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 16:35:43 +08:00
|
|
|
|
// 1. 下载ZIP文件
|
2026-01-14 10:06:08 +08:00
|
|
|
|
this.logger.log(`开始下载ZIP文件: ${zipUrl}`);
|
|
|
|
|
|
const zipPath = path.join(baseDir, 'model.zip');
|
|
|
|
|
|
await this.downloadFile(zipUrl, zipPath);
|
|
|
|
|
|
this.logger.log(`ZIP文件下载完成: ${zipPath}`);
|
|
|
|
|
|
|
2026-01-16 16:35:43 +08:00
|
|
|
|
// 2. 解压ZIP文件
|
2026-01-14 10:06:08 +08:00
|
|
|
|
this.logger.log(`开始解压ZIP文件`);
|
|
|
|
|
|
const extractDir = path.join(baseDir, 'extracted');
|
|
|
|
|
|
await this.extractZip(zipPath, extractDir);
|
|
|
|
|
|
this.logger.log(`ZIP文件解压完成: ${extractDir}`);
|
|
|
|
|
|
|
2026-01-16 16:35:43 +08:00
|
|
|
|
// 3. 查找3D模型文件和预览图
|
2026-01-14 10:06:08 +08:00
|
|
|
|
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}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 16:35:43 +08:00
|
|
|
|
// 4. 读取文件Buffer(用于上传到COS)
|
|
|
|
|
|
const modelBuffer = fs.readFileSync(modelFile);
|
|
|
|
|
|
const previewBuffer = previewFile ? fs.readFileSync(previewFile) : undefined;
|
2026-01-14 10:06:08 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
modelPath: modelFile,
|
|
|
|
|
|
previewPath: previewFile,
|
2026-01-16 16:35:43 +08:00
|
|
|
|
modelBuffer,
|
|
|
|
|
|
previewBuffer,
|
2026-01-14 10:06:08 +08:00
|
|
|
|
};
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error(`处理ZIP文件失败: ${error.message}`, error.stack);
|
|
|
|
|
|
throw error;
|
2026-01-16 16:35:43 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
// 清理临时目录
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (fs.existsSync(baseDir)) {
|
|
|
|
|
|
fs.rmSync(baseDir, { recursive: true, force: true });
|
|
|
|
|
|
this.logger.log(`已清理临时目录: ${baseDir}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
this.logger.warn(`清理临时目录失败: ${err.message}`);
|
|
|
|
|
|
}
|
2026-01-14 10:06:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 下载文件
|
|
|
|
|
|
*/
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|