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