library-picturebook-activity/backend/src/ai-3d/utils/zip-handler.ts

187 lines
5.2 KiB
TypeScript
Raw Normal View History

2026-01-14 10:06:08 +08:00
import * as fs from 'fs';
import * as path from 'path';
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
* @param outputDir backend/uploads/ai-3d
* @returns 3D模型文件路径和预览图路径
*/
static async downloadAndExtract(
zipUrl: string,
outputDir?: string,
): Promise<{
modelPath: string;
previewPath?: string;
modelUrl: string;
previewUrl?: string;
}> {
try {
// 1. 设置输出目录
const baseDir =
outputDir ||
path.join(process.cwd(), 'uploads', 'ai-3d', Date.now().toString());
if (!fs.existsSync(baseDir)) {
fs.mkdirSync(baseDir, { recursive: true });
}
// 2. 下载ZIP文件
this.logger.log(`开始下载ZIP文件: ${zipUrl}`);
const zipPath = path.join(baseDir, 'model.zip');
await this.downloadFile(zipUrl, zipPath);
this.logger.log(`ZIP文件下载完成: ${zipPath}`);
// 3. 解压ZIP文件
this.logger.log(`开始解压ZIP文件`);
const extractDir = path.join(baseDir, 'extracted');
await this.extractZip(zipPath, extractDir);
this.logger.log(`ZIP文件解压完成: ${extractDir}`);
// 4. 查找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}`);
}
// 5. 生成可访问的URL
// 假设模型文件在 uploads/ai-3d/timestamp/extracted/model.glb
// URL应该是 /api/uploads/ai-3d/timestamp/extracted/model.glb
const relativeModelPath = path.relative(
path.join(process.cwd(), 'uploads'),
modelFile,
);
const modelUrl = `/api/uploads/${relativeModelPath.replace(/\\/g, '/')}`;
let previewUrl: string | undefined;
if (previewFile) {
const relativePreviewPath = path.relative(
path.join(process.cwd(), 'uploads'),
previewFile,
);
previewUrl = `/api/uploads/${relativePreviewPath.replace(/\\/g, '/')}`;
}
// 6. 删除原始ZIP文件以节省空间
try {
fs.unlinkSync(zipPath);
this.logger.log(`已删除原始ZIP文件: ${zipPath}`);
} catch (err) {
this.logger.warn(`删除ZIP文件失败: ${err.message}`);
}
return {
modelPath: modelFile,
previewPath: previewFile,
modelUrl,
previewUrl,
};
} catch (error) {
this.logger.error(`处理ZIP文件失败: ${error.message}`, error.stack);
throw error;
}
}
/**
*
*/
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;
}
}