feat: add HTTP/HTTPS URL input support for STP files

Add capability to download STP files directly from HTTP/HTTPS URLs in both CLI and HTTP server modes.

Changes:
- Add http_downloader module for downloading files from URLs
- Extend GlobalConfig to track downloaded files
- Update CLI parameter processing to detect and handle URLs
- Enhance HTTP server to accept URL parameter alongside file upload
- Implement automatic cleanup of downloaded temporary files
- Add comprehensive usage documentation (USAGE.md)

Usage examples:
  CLI: STP2GLB.exe --stp https://example.com/model.stp --glb output.glb
  API: curl -X POST http://localhost:8080/convert -F "url=https://example.com/model.stp"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sladro 2025-10-11 17:19:35 +08:00
parent bc9165803b
commit 096812b7d2
9 changed files with 416 additions and 19 deletions

View File

@ -72,6 +72,7 @@ set(SOURCES
src/main.cpp
src/config_utils.cpp
src/http_server.cpp
src/http_downloader.cpp
src/geom/Color.cpp
src/cadit/occt/step_tree.cpp
src/cadit/occt/debug.cpp
@ -87,6 +88,7 @@ set(HEADERS
src/config_utils.h
src/config_structs.h
src/http_server.h
src/http_downloader.h
src/geom/Color.h
src/cadit/occt/step_tree.h
src/cadit/occt/convert.h

180
USAGE.md Normal file
View File

@ -0,0 +1,180 @@
# STP2GLB 使用说明
STP2GLB 支持两种运行模式:**CLI命令行模式** 和 **HTTP服务器模式**
---
## CLI 命令行模式
### 基本用法
#### 1. 本地文件转换
```bash
STP2GLB.exe --stp input.stp --glb output.glb
```
#### 2. HTTP URL转换新功能
```bash
STP2GLB.exe --stp https://example.com/model.stp --glb output.glb
```
### 主要参数
| 参数 | 说明 | 默认值 | 范围 |
|------|------|--------|------|
| `--stp` | STP文件路径或HTTP/HTTPS URL必填 | - | 本地路径或URL |
| `--glb` | 输出GLB文件路径必填 | - | 本地路径 |
| `--lin-defl` | 线性偏差 | 0.5 | 0.0-1.0 |
| `--ang-defl` | 角度偏差 | 0.8 | 0.0-1.0 |
| `--rel-defl` | 相对偏差模式 | false | - |
| `--debug` | 调试模式(更详细的错误信息) | false | - |
| `--solid-only` | 仅转换实体 | false | - |
| `--max-geometry-num` | 最大几何体数量0=无限制) | 0 | ≥0 |
| `--tessellation-timeout` | 网格化超时时间(秒) | 30 | >0 |
### 使用示例
```bash
# 本地文件转换,调整精度
STP2GLB.exe --stp model.stp --glb output.glb --lin-defl 0.1 --ang-defl 0.5
# 从URL下载并转换
STP2GLB.exe --stp http://example.com/files/part.step --glb result.glb
# 调试模式转换
STP2GLB.exe --stp model.stp --glb output.glb --debug
# 限制几何体数量
STP2GLB.exe --stp large.stp --glb output.glb --max-geometry-num 100
```
---
## HTTP 服务器模式
### 启动服务器
```bash
STP2GLB.exe --server --port 8080 --host 0.0.0.0
```
### 服务器参数
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--server` | 启动服务器模式 | - |
| `--port` | 服务器端口 | 8080 |
| `--host` | 监听地址 | 0.0.0.0 |
| `--max-file-size` | 最大文件大小MB | 500 |
### API 端点
#### 1. 健康检查
```bash
GET /health
```
**响应示例:**
```json
{
"status": "ok",
"service": "STP2GLB"
}
```
#### 2. 文件转换
```bash
POST /convert
```
**支持两种输入方式:**
##### 方式一:文件上传
```bash
curl -X POST http://localhost:8080/convert \
-F "file=@model.stp" \
-F "linearDeflection=0.1" \
-F "angularDeflection=0.5"
```
##### 方式二HTTP URL新功能
```bash
curl -X POST http://localhost:8080/convert \
-F "url=https://example.com/model.stp" \
-F "linearDeflection=0.1"
```
### 转换参数
| 参数名 | 说明 | 默认值 |
|--------|------|--------|
| `file` | STP文件multipart上传 | - |
| `url` | STP文件HTTP/HTTPS URL | - |
| `linearDeflection` | 线性偏差 | 0.5 |
| `angularDeflection` | 角度偏差 | 0.8 |
| `relativeDeflection` | 相对偏差true/false | false |
| `debug` | 调试模式true/false | false |
| `solidOnly` | 仅实体true/false | false |
| `maxGeometryNum` | 最大几何体数量 | 0 |
**注意:** `file``url` 二选一,`url` 优先级更高
### 响应示例
**成功响应:**
```json
{
"success": true,
"output_file": "1234567890_5678.glb",
"output_path": "./output/1234567890_5678.glb",
"conversion_time": 2.35
}
```
**错误响应:**
```json
{
"success": false,
"error": "Failed to download from URL: HTTP error: 404 Not Found"
}
```
---
## 注意事项
1. **URL下载**
- 仅支持HTTP和HTTPS协议
- URL必须直接指向.stp或.step文件
- 下载的临时文件会在转换完成后自动清理
2. **文件大小限制**
- HTTP服务器模式默认限制500MB
- 可通过 `--max-file-size` 参数调整
3. **超时设置**
- 默认网格化超时30秒
- 大型复杂模型建议增加超时时间
4. **调试模式**
- 提供详细的STEP实体转换失败信息
- 会显著降低转换速度
---
## 快速测试
```bash
# CLI模式测试
STP2GLB.exe --stp https://example.com/test.stp --glb test.glb
# 服务器模式测试
STP2GLB.exe --server --port 8080
# 测试健康检查
curl http://localhost:8080/health
# 测试URL转换
curl -X POST http://localhost:8080/convert \
-F "url=https://example.com/model.stp"
```

View File

@ -42,6 +42,10 @@ struct GlobalConfig {
BuildConfig buildConfig;
ServerConfig serverConfig;
// HTTP download tracking
bool is_downloaded_from_url = false;
std::string original_url;
};

View File

@ -12,6 +12,7 @@
#include "config_structs.h"
#include "cadit/occt/helpers.h"
#include "CLI/App.hpp"
#include "http_downloader.h"
// Helper function to process filter names from input or file
std::vector<std::string> process_filter_names(const std::string& input, const std::string& file_name)
@ -79,19 +80,40 @@ GlobalConfig process_parameters(CLI::App& app)
const auto filter_names_include = process_filter_names(filter_names_include_input, filter_names_file_include);
const auto filter_names_exclude = process_filter_names(filter_names_exclude_input, filter_names_file_exclude);
const std::string stpFilename = app.get_option("--stp")->results()[0];
const std::string glbFilename = app.get_option("--glb")->results()[0];
std::string stpFilename = app.get_option("--stp")->results()[0];
std::string glbFilename = app.get_option("--glb")->results()[0];
bool isDownloadedFromUrl = false;
std::string originalUrl;
std::filesystem::path stpFilePath;
std::filesystem::path glbFilePath;
// Check if input is HTTP URL
if (is_http_url(stpFilename)) {
std::cout << "Detected HTTP URL, downloading..." << std::endl;
try {
std::string downloaded_path = download_file_from_url(stpFilename, "./temp");
stpFilePath = std::filesystem::path(downloaded_path);
isDownloadedFromUrl = true;
originalUrl = stpFilename;
std::cout << "URL download completed: " << downloaded_path << std::endl;
} catch (const std::exception& ex) {
throw std::runtime_error("Failed to download from URL: " + std::string(ex.what()));
}
} else {
stpFilePath = std::filesystem::path(stpFilename);
// Check if local file exists
if (!exists(stpFilePath)) {
throw std::invalid_argument("Invalid --stp filename \"" + stpFilename + "\". File does not exist.");
}
}
glbFilePath = std::filesystem::path(glbFilename);
// Validate extensions
const bool isStpValid = endsWithCaseInsensitive(stpFilename, ".stp") || endsWithCaseInsensitive(stpFilename, ".step");
const bool isGlbValid = endsWithCaseInsensitive(glbFilename, ".glb");
auto stpFilePath = std::filesystem::path(stpFilename);
auto glbFilePath = std::filesystem::path(glbFilename);
bool isStpValid = endsWithCaseInsensitive(stpFilePath.string(), ".stp") || endsWithCaseInsensitive(stpFilePath.string(), ".step");
bool isGlbValid = endsWithCaseInsensitive(glbFilename, ".glb");
// check if file paths exists
if (!exists(stpFilePath)) {
throw std::invalid_argument("Invalid --stp filename \"" + stpFilename + "\". File does not exist.");
}
if (exists(glbFilePath)) {
std::cout << "Warning: --glb filename \"" << glbFilename << "\" already exists and will be overwritten.\n";
}
@ -106,7 +128,7 @@ GlobalConfig process_parameters(CLI::App& app)
// Create configuration
return {
.stpFile = stpFilename,
.stpFile = stpFilePath,
.glbFile = glbFilename,
.debug_mode = app.get_option("--debug")->as<bool>(),
.linearDeflection = app.get_option("--lin-defl")->as<double>(),
@ -126,6 +148,8 @@ GlobalConfig process_parameters(CLI::App& app)
.host = app.get_option("--host")->as<std::string>(),
.max_file_size_mb = app.get_option("--max-file-size")->as<size_t>(),
.temp_dir = "./temp"
}
},
.is_downloaded_from_url = isDownloadedFromUrl,
.original_url = originalUrl
};
}

15
src/config_utils.h Normal file
View File

@ -0,0 +1,15 @@
//
// Created by ofskrand on 13.01.2025.
//
#ifndef CONFIG_UTILS_H
#define CONFIG_UTILS_H
#include <CLI/App.hpp>
#include "config_structs.h"
GlobalConfig process_parameters(CLI::App& app);
#endif //CONFIG_UTILS_H

120
src/http_downloader.cpp Normal file
View File

@ -0,0 +1,120 @@
#include "http_downloader.h"
#include "third_party/httplib.h"
#include <filesystem>
#include <fstream>
#include <iostream>
#include <algorithm>
#include <chrono>
#include <random>
#include <sstream>
namespace fs = std::filesystem;
bool is_http_url(const std::string& path) {
std::string lower_path = path;
std::transform(lower_path.begin(), lower_path.end(), lower_path.begin(), ::tolower);
return lower_path.substr(0, 7) == "http://" || lower_path.substr(0, 8) == "https://";
}
std::string generate_download_filename(const std::string& extension) {
auto now = std::chrono::system_clock::now();
auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(1000, 9999);
std::ostringstream oss;
oss << "download_" << timestamp << "_" << dis(gen) << extension;
return oss.str();
}
std::string download_file_from_url(const std::string& url, const std::string& temp_dir) {
std::cout << "Downloading file: " << url << std::endl;
// Ensure temp directory exists
fs::create_directories(temp_dir);
// Parse URL
size_t protocol_end = url.find("://");
if (protocol_end == std::string::npos) {
throw std::runtime_error("Invalid URL format: " + url);
}
std::string protocol = url.substr(0, protocol_end);
std::string remaining = url.substr(protocol_end + 3);
size_t host_end = remaining.find('/');
std::string host;
std::string path;
if (host_end == std::string::npos) {
host = remaining;
path = "/";
} else {
host = remaining.substr(0, host_end);
path = remaining.substr(host_end);
}
// Extract file extension from URL path
std::string extension = ".stp";
size_t last_dot = path.find_last_of('.');
if (last_dot != std::string::npos) {
std::string url_ext = path.substr(last_dot);
// Ensure it's a STP-related extension
std::string lower_ext = url_ext;
std::transform(lower_ext.begin(), lower_ext.end(), lower_ext.begin(), ::tolower);
if (lower_ext == ".stp" || lower_ext == ".step") {
extension = lower_ext;
}
}
// Generate local filename
std::string filename = generate_download_filename(extension);
fs::path local_path = fs::path(temp_dir) / filename;
std::cout << "Target host: " << host << std::endl;
std::cout << "Request path: " << path << std::endl;
std::cout << "Saving to: " << local_path << std::endl;
// Create HTTP client
std::unique_ptr<httplib::Client> cli;
if (protocol == "https") {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
cli = std::make_unique<httplib::SSLClient>(host);
#else
throw std::runtime_error("HTTPS not supported in this build");
#endif
} else {
cli = std::make_unique<httplib::Client>(host);
}
// Set timeout
cli->set_read_timeout(300, 0); // 300 seconds
cli->set_write_timeout(300, 0);
// Execute GET request
auto res = cli->Get(path.c_str());
if (!res) {
throw std::runtime_error("HTTP request failed: " + httplib::to_string(res.error()));
}
if (res->status != 200) {
throw std::runtime_error("HTTP error: " + std::to_string(res->status) + " " + res->reason);
}
// Save to local file
std::ofstream ofs(local_path, std::ios::binary);
if (!ofs) {
throw std::runtime_error("Cannot create file: " + local_path.string());
}
ofs.write(res->body.c_str(), res->body.size());
ofs.close();
std::cout << "Download completed, file size: " << res->body.size() << " bytes" << std::endl;
return local_path.string();
}

13
src/http_downloader.h Normal file
View File

@ -0,0 +1,13 @@
#ifndef HTTP_DOWNLOADER_H
#define HTTP_DOWNLOADER_H
#include <string>
// 判断路径是否为HTTP/HTTPS URL
bool is_http_url(const std::string& path);
// 从URL下载文件到指定目录返回本地文件路径
// 下载失败会抛出std::runtime_error异常
std::string download_file_from_url(const std::string& url, const std::string& temp_dir);
#endif // HTTP_DOWNLOADER_H

View File

@ -2,6 +2,7 @@
#include "third_party/httplib.h"
#include "cadit/occt/convert.h"
#include "cadit/occt/debug.h"
#include "http_downloader.h"
#include <iostream>
#include <filesystem>
#include <fstream>
@ -48,13 +49,30 @@ void start_http_server(const GlobalConfig& base_config) {
svr.Post("/convert", [&base_config](const httplib::Request& req, httplib::Response& res) {
std::cout << "Received conversion request" << "\n";
if (!req.is_multipart_form_data() || !req.form.has_file("file")) {
bool use_url = false;
std::string url_param;
// Check if URL parameter is provided
if (req.form.has_field("url")) {
url_param = req.form.get_field("url");
if (!url_param.empty() && is_http_url(url_param)) {
use_url = true;
std::cout << "Using URL parameter: " << url_param << "\n";
}
}
// If no URL, check file upload
if (!use_url && (!req.is_multipart_form_data() || !req.form.has_file("file"))) {
res.status = 400;
res.set_content("{\"error\":\"No file uploaded. Use 'file' field.\"}", "application/json");
res.set_content("{\"error\":\"No file uploaded or URL provided. Use 'file' field or 'url' parameter.\"}", "application/json");
return;
}
const auto& file = req.form.get_file("file", 0);
std::string file_content;
if (!use_url) {
const auto& file = req.form.get_file("file", 0);
file_content = file.content;
}
GlobalConfig config = base_config;
@ -81,13 +99,22 @@ void start_http_server(const GlobalConfig& base_config) {
std::string stp_filename = base_filename + ".stp";
std::string glb_filename = base_filename + ".glb";
fs::path stp_path = fs::path(config.serverConfig.temp_dir) / stp_filename;
fs::path stp_path;
fs::path glb_path = fs::path(config.serverConfig.output_dir) / glb_filename;
try {
std::ofstream ofs(stp_path, std::ios::binary);
ofs.write(file.content.c_str(), file.content.size());
ofs.close();
// If using URL, download file; otherwise save uploaded file
if (use_url) {
std::string downloaded_path = download_file_from_url(url_param, config.serverConfig.temp_dir);
stp_path = fs::path(downloaded_path);
config.is_downloaded_from_url = true;
config.original_url = url_param;
} else {
stp_path = fs::path(config.serverConfig.temp_dir) / stp_filename;
std::ofstream ofs(stp_path, std::ios::binary);
ofs.write(file_content.c_str(), file_content.size());
ofs.close();
}
config.stpFile = stp_path;
config.glbFile = glb_path;

View File

@ -140,5 +140,17 @@ int main(int argc, char* argv[])
const double seconds = static_cast<double>(duration.count()) / 1e6;
std::cout << "STP converted in: " << std::fixed << std::setprecision(2) << seconds << " seconds" << "\n";
// Clean up downloaded temporary file
if (config.is_downloaded_from_url) {
try {
if (std::filesystem::exists(config.stpFile)) {
std::filesystem::remove(config.stpFile);
std::cout << "Cleaned up downloaded file: " << config.stpFile << "\n";
}
} catch (const std::exception& ex) {
std::cerr << "Warning: Failed to clean up downloaded file: " << ex.what() << "\n";
}
}
return 0;
}