library-picturebook-activity/backend/src/ai-3d/utils/zip-handler.ts
2026-01-16 16:35:43 +08:00

177 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}