Files
GitVer/GitVer/GitVer_upload.cpp
Jeff 6b0f602e57 1、加强注释
2、上传时,添加-f参数才会强制上传,否则需要根据当前Tag标签信息来判断是否需要上传。
2026-06-28 20:56:56 +08:00

1234 lines
31 KiB
C++
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.
#include "pch.h"
#include "GitVer_upload.h"
#include "gitver_version.h"
#include "httplib.h"
#include <memory>
#include <string>
#include <vector>
#include <map>
extern std::string ToAnsiString(LPCTSTR lpText);
extern BOOL ReadTextFileAsAnsi(LPCTSTR lpFile, std::string& strContent, std::string& strBomPrefix);
extern TCHAR g_szCurModuleDir[MAX_PATH];
static const LPCTSTR DEFAULT_BASE_URL = _T("http://tools-center.mokadisplay.com/");
struct UploadInfoConfig
{
CString strBaseUrl;
CString strPackageFile;
CString strPackageName;
CString strPackageTypeId;
CString strProductVersion;
CString strFileVersion;
CString strFileChangeLog;
CString strProductChangeLog;
CString strDescription;
CString strWikiUrl;
};
// ─────────────────────────────────────────────
// 字符串 / 编码工具
// ─────────────────────────────────────────────
/// <summary>
/// 将宽字符(或 ANSI字符串转换为 UTF-8 编码的 std::string。
/// </summary>
/// <param name="lpText">输入字符串。</param>
/// <returns>UTF-8 编码结果,空输入返回空字符串。</returns>
static std::string ToUtf8String(LPCTSTR lpText)
{
if (lpText == NULL || lpText[0] == _T('\0'))
{
return std::string();
}
#ifdef UNICODE
int nSize = WideCharToMultiByte(CP_UTF8, 0, lpText, -1, NULL, 0, NULL, NULL);
if (nSize <= 1)
{
return std::string();
}
std::string strValue;
strValue.resize(nSize - 1);
WideCharToMultiByte(CP_UTF8, 0, lpText, -1, &strValue[0], nSize, NULL, NULL);
return strValue;
#else
int nLen = (int)_tcslen(lpText);
int nWideSize = MultiByteToWideChar(CP_ACP, 0, lpText, nLen, NULL, 0);
if (nWideSize <= 0)
{
return std::string();
}
CStringW strWide;
MultiByteToWideChar(CP_ACP, 0, lpText, nLen, strWide.GetBuffer(nWideSize), nWideSize);
strWide.ReleaseBuffer(nWideSize);
int nUtf8Size = WideCharToMultiByte(CP_UTF8, 0, strWide, strWide.GetLength(), NULL, 0, NULL, NULL);
if (nUtf8Size <= 0)
{
return std::string();
}
std::string strValue;
strValue.resize(nUtf8Size);
WideCharToMultiByte(CP_UTF8, 0, strWide, strWide.GetLength(), &strValue[0], nUtf8Size, NULL, NULL);
return strValue;
#endif
}
/// <summary>
/// 将 UTF-8 编码的 std::string 转换为 CString。
/// </summary>
/// <param name="strUtf8">UTF-8 字符串。</param>
/// <returns>转换后的 CString。</returns>
static CString Utf8ToCString(const std::string& strUtf8)
{
if (strUtf8.empty())
{
return CString();
}
#ifdef UNICODE
int nSize = MultiByteToWideChar(CP_UTF8, 0, strUtf8.c_str(), (int)strUtf8.size(), NULL, 0);
if (nSize <= 0)
{
return CString();
}
CString strValue;
MultiByteToWideChar(CP_UTF8, 0, strUtf8.c_str(), (int)strUtf8.size(), strValue.GetBuffer(nSize), nSize);
strValue.ReleaseBuffer(nSize);
return strValue;
#else
int nSize = MultiByteToWideChar(CP_UTF8, 0, strUtf8.c_str(), (int)strUtf8.size(), NULL, 0);
if (nSize <= 0)
{
return CString();
}
CStringW strWide;
MultiByteToWideChar(CP_UTF8, 0, strUtf8.c_str(), (int)strUtf8.size(), strWide.GetBuffer(nSize), nSize);
strWide.ReleaseBuffer(nSize);
nSize = WideCharToMultiByte(CP_ACP, 0, strWide, -1, NULL, 0, NULL, NULL);
if (nSize <= 1)
{
return CString();
}
CString strValue;
WideCharToMultiByte(CP_ACP, 0, strWide, -1, strValue.GetBuffer(nSize), nSize, NULL, NULL);
strValue.ReleaseBuffer();
return strValue;
#endif
}
/// <summary>
/// 将 ANSI 编码的 std::string 转换为 CString。
/// </summary>
/// <param name="strAnsi">ANSI 字符串。</param>
/// <returns>转换后的 CString。</returns>
static CString AnsiToCString(const std::string& strAnsi)
{
if (strAnsi.empty())
{
return CString();
}
#ifdef UNICODE
int nSize = MultiByteToWideChar(CP_ACP, 0, strAnsi.data(), (int)strAnsi.size(), NULL, 0);
if (nSize <= 0)
{
return CString();
}
CString strValue;
MultiByteToWideChar(CP_ACP, 0, strAnsi.data(), (int)strAnsi.size(), strValue.GetBuffer(nSize), nSize);
strValue.ReleaseBuffer(nSize);
return strValue;
#else
return CString(strAnsi.c_str(), (int)strAnsi.size());
#endif
}
/// <summary>
/// 粗略检测字节序列是否为合法 UTF-8 编码。
/// </summary>
/// <param name="strValue">待检测的字节串。</param>
/// <returns>看起来像合法 UTF-8 则返回 TRUE。</returns>
static BOOL IsLikelyValidUtf8(const std::string& strValue)
{
size_t i = 0;
while (i < strValue.size())
{
unsigned char ch = (unsigned char)strValue[i];
if (ch <= 0x7F)
{
++i;
continue;
}
int nBytes = 0;
if ((ch & 0xE0) == 0xC0)
{
nBytes = 2;
}
else if ((ch & 0xF0) == 0xE0)
{
nBytes = 3;
}
else if ((ch & 0xF8) == 0xF0)
{
nBytes = 4;
}
else
{
return FALSE;
}
if (i + (size_t)nBytes > strValue.size())
{
return FALSE;
}
for (int j = 1; j < nBytes; ++j)
{
if ((strValue[i + j] & 0xC0) != 0x80)
{
return FALSE;
}
}
i += (size_t)nBytes;
}
return TRUE;
}
/// <summary>
/// 将 info 文件中的原始字节值转为 CString自动区分 UTF-8 与 ANSI。
/// </summary>
/// <param name="strValue">原始字节值。</param>
/// <returns>解码后的 CString。</returns>
static CString InfoValueToCString(const std::string& strValue)
{
if (IsLikelyValidUtf8(strValue))
{
return Utf8ToCString(strValue);
}
return AnsiToCString(strValue);
}
/// <summary>
/// 对 UTF-8 字符串进行 application/x-www-form-urlencoded 风格 URL 编码。
/// </summary>
/// <param name="strValue">待编码的 UTF-8 字符串。</param>
/// <returns>编码后的字符串。</returns>
static std::string UrlEncodeUtf8(const std::string& strValue)
{
static const char* kHex = "0123456789ABCDEF";
std::string strEncoded;
strEncoded.reserve(strValue.size() * 3);
for (unsigned char ch : strValue)
{
if ((ch >= 'A' && ch <= 'Z') ||
(ch >= 'a' && ch <= 'z') ||
(ch >= '0' && ch <= '9') ||
ch == '-' || ch == '_' || ch == '.' || ch == '~')
{
strEncoded.push_back((char)ch);
}
else if (ch == ' ')
{
strEncoded.push_back('+');
}
else
{
strEncoded.push_back('%');
strEncoded.push_back(kHex[ch >> 4]);
strEncoded.push_back(kHex[ch & 0x0F]);
}
}
return strEncoded;
}
/// <summary>
/// 反转义 info 文件值中的 \n 为换行符。
/// </summary>
/// <param name="strValue">含转义序列的原始值。</param>
/// <returns>反转义后的字符串。</returns>
static std::string UnescapeInfoValue(const std::string& strValue)
{
std::string strResult;
strResult.reserve(strValue.size());
for (size_t i = 0; i < strValue.size(); ++i)
{
if (strValue[i] == '\\' && i + 1 < strValue.size() && strValue[i + 1] == 'n')
{
strResult.push_back('\n');
++i;
}
else
{
strResult.push_back(strValue[i]);
}
}
return strResult;
}
/// <summary>
/// 将字符串转义后写入 info 文件(换行 → \n反斜杠 → \\)。
/// </summary>
/// <param name="strValue">原始字符串。</param>
/// <returns>适合写入 info 文件的转义字符串。</returns>
static std::string EscapeInfoValueForFile(const std::string& strValue)
{
std::string strResult;
strResult.reserve(strValue.size());
for (size_t i = 0; i < strValue.size(); ++i)
{
char ch = strValue[i];
if (ch == '\r')
{
continue;
}
if (ch == '\n')
{
strResult += "\\n";
continue;
}
if (ch == '\\')
{
strResult += "\\\\";
continue;
}
strResult.push_back(ch);
}
return strResult;
}
/// <summary>
/// 去除 ANSI 字符串首尾的空格、制表符和回车。
/// </summary>
/// <param name="strValue">待修剪的字符串(原地修改)。</param>
static void TrimAnsiInPlace(std::string& strValue)
{
while (!strValue.empty() && (strValue.front() == ' ' || strValue.front() == '\t' || strValue.front() == '\r'))
{
strValue.erase(strValue.begin());
}
while (!strValue.empty() && (strValue.back() == ' ' || strValue.back() == '\t' || strValue.back() == '\r'))
{
strValue.pop_back();
}
}
/// <summary>
/// 读取二进制文件的全部内容到字节向量。
/// </summary>
/// <param name="lpFile">文件路径。</param>
/// <param name="data">输出:文件字节内容。</param>
/// <returns>读取成功返回 TRUE。</returns>
static BOOL ReadBinaryFile(LPCTSTR lpFile, std::vector<BYTE>& data)
{
data.clear();
CFile myFile;
CFileException fileExp;
if (!myFile.Open(lpFile, CFile::modeRead | CFile::shareDenyWrite, &fileExp))
{
return FALSE;
}
ULONGLONG ullLength = myFile.GetLength();
if (ullLength == 0)
{
myFile.Close();
return TRUE;
}
data.resize((size_t)ullLength);
myFile.Read(data.data(), (UINT)ullLength);
myFile.Close();
return TRUE;
}
/// <summary>
/// 将 ASCII 字母转为小写(仅 A-Z
/// </summary>
/// <param name="strValue">输入字符串。</param>
/// <returns>小写化后的新字符串。</returns>
static std::string ToLowerAscii(std::string strValue)
{
for (char& ch : strValue)
{
if (ch >= 'A' && ch <= 'Z')
{
ch = (char)(ch - 'A' + 'a');
}
}
return strValue;
}
// ─────────────────────────────────────────────
// Cookie 管理httplib 不自动维护 Session
// ─────────────────────────────────────────────
/// <summary>
/// 去除 std::string 首尾的空格和制表符。
/// </summary>
/// <param name="strValue">待修剪的字符串(原地修改)。</param>
static void TrimStdStringInPlace(std::string& strValue)
{
while (!strValue.empty() && (strValue.front() == ' ' || strValue.front() == '\t'))
{
strValue.erase(strValue.begin());
}
while (!strValue.empty() && (strValue.back() == ' ' || strValue.back() == '\t'))
{
strValue.pop_back();
}
}
/// <summary>
/// 将 Set-Cookie 响应头中的 name=value 合并到 Cookie 字符串,同名则覆盖。
/// </summary>
/// <param name="strHeaderValue">Set-Cookie 头的值部分。</param>
/// <param name="strCookies">输入/输出:累积的 Cookie 字符串。</param>
static void MergeSetCookieValue(const std::string& strHeaderValue, std::string& strCookies)
{
std::string strLine = strHeaderValue;
TrimStdStringInPlace(strLine);
if (strLine.empty())
{
return;
}
size_t nSemi = strLine.find(';');
std::string strPair = (nSemi != std::string::npos) ? strLine.substr(0, nSemi) : strLine;
TrimStdStringInPlace(strPair);
if (strPair.empty())
{
return;
}
size_t nEq = strPair.find('=');
if (nEq == std::string::npos || nEq == 0)
{
return;
}
std::string strName = strPair.substr(0, nEq);
std::string strValue = strPair.substr(nEq + 1);
TrimStdStringInPlace(strName);
TrimStdStringInPlace(strValue);
if (strName.empty())
{
return;
}
std::string strNewPair = strName + "=" + strValue;
if (strCookies.empty())
{
strCookies = strNewPair;
return;
}
std::string strResult;
BOOL bReplaced = FALSE;
size_t nStart = 0;
while (nStart < strCookies.size())
{
size_t nSemiPos = strCookies.find(';', nStart);
std::string strItem = (nSemiPos != std::string::npos)
? strCookies.substr(nStart, nSemiPos - nStart)
: strCookies.substr(nStart);
TrimStdStringInPlace(strItem);
if (!strItem.empty())
{
size_t nItemEq = strItem.find('=');
std::string strItemName = (nItemEq != std::string::npos) ? strItem.substr(0, nItemEq) : strItem;
TrimStdStringInPlace(strItemName);
if (!strResult.empty())
{
strResult += "; ";
}
if (ToLowerAscii(strItemName) == ToLowerAscii(strName))
{
strResult += strNewPair;
bReplaced = TRUE;
}
else
{
strResult += strItem;
}
}
if (nSemiPos == std::string::npos)
{
break;
}
nStart = nSemiPos + 1;
}
if (!bReplaced)
{
if (!strResult.empty())
{
strResult += "; ";
}
strResult += strNewPair;
}
strCookies = strResult;
}
/// <summary>
/// 从 HTTP 响应的所有 Set-Cookie 头中收集并合并 Cookie。
/// </summary>
/// <param name="response">HTTP 响应对象。</param>
/// <param name="strCookies">输出:合并后的 Cookie 字符串。</param>
static void CollectResponseCookies(const httplib::Response& response, std::string& strCookies)
{
for (const auto& header : response.headers)
{
if (ToLowerAscii(header.first) == "set-cookie")
{
MergeSetCookieValue(header.second, strCookies);
}
}
}
/// <summary>
/// 根据 Cookie 字符串构造 httplib 请求头。
/// </summary>
/// <param name="strCookies">Cookie 字符串。</param>
/// <returns>含 Cookie 头的 Headers 对象。</returns>
static httplib::Headers MakeCookieHeaders(const std::string& strCookies)
{
httplib::Headers headers;
if (!strCookies.empty())
{
headers.emplace("Cookie", strCookies);
}
return headers;
}
/// <summary>
/// 创建指向 PMC 服务的 httplib 客户端,配置超时且不自动跟随重定向。
/// </summary>
/// <param name="lpBaseUrl">PMC 基址 URL。</param>
/// <returns>有效客户端指针,无效 URL 时返回 nullptr。</returns>
static std::unique_ptr<httplib::Client> CreatePmcClient(LPCTSTR lpBaseUrl)
{
std::string strBaseUrl = ToUtf8String(lpBaseUrl);
auto cli = std::make_unique<httplib::Client>(strBaseUrl);
if (!cli->is_valid())
{
return nullptr;
}
cli->set_follow_location(false);
cli->set_connection_timeout(30, 0);
cli->set_read_timeout(300, 0);
cli->set_write_timeout(300, 0);
return cli;
}
// ─────────────────────────────────────────────
// info 文件解析
// ─────────────────────────────────────────────
/// <summary>
/// 根据键名将 info 文件中的一行键值对写入 UploadInfoConfig 结构体。
/// </summary>
/// <param name="config">输出配置对象。</param>
/// <param name="strKey">键名。</param>
/// <param name="strRawValue">原始值(含转义序列)。</param>
static void ApplyInfoKeyValue(UploadInfoConfig& config, const std::string& strKey, const std::string& strRawValue)
{
std::string strValue = UnescapeInfoValue(strRawValue);
CString strCStringValue = InfoValueToCString(strValue);
if (strKey == "url")
{
config.strBaseUrl = strCStringValue;
}
else if (strKey == "file")
{
config.strPackageFile = strCStringValue;
}
else if (strKey == "package_name")
{
config.strPackageName = strCStringValue;
}
else if (strKey == "package_type_id")
{
config.strPackageTypeId = strCStringValue;
}
else if (strKey == "product_version")
{
config.strProductVersion = strCStringValue;
}
else if (strKey == "file_version")
{
config.strFileVersion = strCStringValue;
}
else if (strKey == "file_change_log")
{
config.strFileChangeLog = strCStringValue;
}
else if (strKey == "product_change_log")
{
config.strProductChangeLog = strCStringValue;
}
else if (strKey == "description")
{
config.strDescription = strCStringValue;
}
else if (strKey == "wiki_url")
{
config.strWikiUrl = strCStringValue;
}
}
/// <summary>
/// 从 upload_info.txt 等 info 配置文件加载上传参数。
/// </summary>
/// <param name="lpInfoFile">info 文件路径。</param>
/// <param name="config">输出:解析后的配置。</param>
/// <returns>文件读取成功返回 TRUE。</returns>
static BOOL LoadUploadInfoFile(LPCTSTR lpInfoFile, UploadInfoConfig& config)
{
std::string strContent;
std::string strBomPrefix;
if (!ReadTextFileAsAnsi(lpInfoFile, strContent, strBomPrefix))
{
_tprintf(_T("错误: 无法读取 info 配置文件 %s\n"), lpInfoFile);
return FALSE;
}
size_t nLineStart = 0;
while (nLineStart <= strContent.size())
{
size_t nLineEnd = strContent.find('\n', nLineStart);
if (nLineEnd == std::string::npos)
{
nLineEnd = strContent.size();
}
std::string strLine = strContent.substr(nLineStart, nLineEnd - nLineStart);
if (!strLine.empty() && strLine.back() == '\r')
{
strLine.pop_back();
}
TrimAnsiInPlace(strLine);
if (!strLine.empty() && strLine[0] != '#')
{
size_t nEq = strLine.find('=');
if (nEq != std::string::npos)
{
std::string strKey = strLine.substr(0, nEq);
std::string strValue = strLine.substr(nEq + 1);
TrimAnsiInPlace(strKey);
TrimAnsiInPlace(strValue);
if (!strKey.empty())
{
ApplyInfoKeyValue(config, strKey, strValue);
}
}
}
if (nLineEnd >= strContent.size())
{
break;
}
nLineStart = nLineEnd + 1;
}
return TRUE;
}
/// <summary>
/// 校验上传配置是否包含必填字段。
/// </summary>
/// <param name="config">待校验的配置。</param>
/// <returns>必填字段齐全返回 TRUE缺失时打印错误并返回 FALSE。</returns>
static BOOL ValidateUploadConfig(const UploadInfoConfig& config)
{
if (config.strPackageName.IsEmpty())
{
_tprintf(_T("错误: info 文件缺少 package_name\n"));
return FALSE;
}
if (config.strPackageTypeId.IsEmpty())
{
_tprintf(_T("错误: info 文件缺少 package_type_id\n"));
return FALSE;
}
if (config.strProductVersion.IsEmpty())
{
_tprintf(_T("错误: info 文件缺少 product_version\n"));
return FALSE;
}
if (config.strFileVersion.IsEmpty())
{
_tprintf(_T("错误: info 文件缺少 file_version\n"));
return FALSE;
}
if (config.strFileChangeLog.IsEmpty())
{
_tprintf(_T("错误: info 文件缺少 file_change_log\n"));
return FALSE;
}
return TRUE;
}
/// <summary>
/// 从与用户名同名的环境变量中读取登录密码。
/// </summary>
/// <param name="lpUsername">PMC 登录用户名(亦作环境变量名)。</param>
/// <param name="strPassword">输出:读取到的密码。</param>
/// <returns>环境变量存在且非空返回 TRUE。</returns>
static BOOL ResolvePasswordFromEnv(LPCTSTR lpUsername, CString& strPassword)
{
if (lpUsername == NULL || lpUsername[0] == _T('\0'))
{
return FALSE;
}
#ifdef UNICODE
LPWSTR lpEnvValue = _wgetenv(lpUsername);
if (lpEnvValue == NULL || lpEnvValue[0] == L'\0')
{
_tprintf(_T("错误: 未设置环境变量 %s用于读取登录密码\n"), lpUsername);
return FALSE;
}
strPassword = lpEnvValue;
#else
std::string strEnvName = ToAnsiString(lpUsername);
char* lpBuffer = NULL;
size_t nLen = 0;
if (_dupenv_s(&lpBuffer, &nLen, strEnvName.c_str()) != 0 || lpBuffer == NULL || lpBuffer[0] == '\0')
{
if (lpBuffer != NULL)
{
free(lpBuffer);
}
_tprintf(_T("错误: 未设置环境变量 %s用于读取登录密码\n"), lpUsername);
return FALSE;
}
strPassword = lpBuffer;
free(lpBuffer);
#endif
return TRUE;
}
/// <summary>
/// 更新 upload_info.txt 中指定键的值,保留其他行和 BOM 前缀。
/// 文件中不存在的键会追加到末尾。
/// </summary>
/// <param name="lpInfoFile">info 文件路径。</param>
/// <param name="updates">要更新的键值对映射。</param>
/// <returns>写入成功返回 TRUE。</returns>
static BOOL UpdateUploadInfoKeys(LPCTSTR lpInfoFile, const std::map<std::string, std::string>& updates)
{
std::vector<std::string> outLines;
std::string strBomPrefix;
std::map<std::string, BOOL> foundKeys;
for (const auto& item : updates)
{
foundKeys[item.first] = FALSE;
}
if (PathFileExists(lpInfoFile))
{
std::string strContent;
if (!ReadTextFileAsAnsi(lpInfoFile, strContent, strBomPrefix))
{
_tprintf(_T("警告: 无法读取 upload_info.txt将创建新文件\n"));
}
else
{
size_t nLineStart = 0;
while (nLineStart <= strContent.size())
{
size_t nLineEnd = strContent.find('\n', nLineStart);
if (nLineEnd == std::string::npos)
{
nLineEnd = strContent.size();
}
std::string strLine = strContent.substr(nLineStart, nLineEnd - nLineStart);
if (!strLine.empty() && strLine.back() == '\r')
{
strLine.pop_back();
}
std::string strOutLine = strLine;
if (!strLine.empty() && strLine[0] != '#')
{
size_t nEq = strLine.find('=');
if (nEq != std::string::npos)
{
std::string strKey = strLine.substr(0, nEq);
TrimAnsiInPlace(strKey);
std::map<std::string, std::string>::const_iterator it = updates.find(strKey);
if (it != updates.end())
{
strOutLine = strKey + "=" + it->second;
foundKeys[strKey] = TRUE;
}
}
}
outLines.push_back(strOutLine);
if (nLineEnd >= strContent.size())
{
break;
}
nLineStart = nLineEnd + 1;
}
}
}
for (const auto& item : updates)
{
if (!foundKeys[item.first])
{
outLines.push_back(item.first + "=" + item.second);
}
}
CFile myFile;
CFileException fileExp;
if (!myFile.Open(lpInfoFile, CFile::modeCreate | CFile::modeWrite, &fileExp))
{
_tprintf(_T("错误: 无法写入 upload_info.txt %s\n"), lpInfoFile);
return FALSE;
}
if (!strBomPrefix.empty())
{
myFile.Write(strBomPrefix.c_str(), (UINT)strBomPrefix.size());
}
for (size_t i = 0; i < outLines.size(); ++i)
{
myFile.Write(outLines[i].c_str(), (UINT)outLines[i].size());
myFile.Write("\n", 1);
}
myFile.Close();
_tprintf(_T("已更新 upload_info.txt: %s\n"), lpInfoFile);
return TRUE;
}
/// <summary>
/// 更新 upload_info.txt 中的版本号与变更日志字段。
/// </summary>
BOOL WriteUploadInfoVersions(LPCTSTR lpInfoFile, LPCTSTR lpProductVersion, LPCTSTR lpFileVersion, LPCTSTR lpFileChangeLog)
{
std::map<std::string, std::string> updates;
updates["product_version"] = lpProductVersion;
updates["file_version"] = lpFileVersion;
updates["file_change_log"] = EscapeInfoValueForFile(lpFileChangeLog);
return UpdateUploadInfoKeys(lpInfoFile, updates);
}
/// <summary>
/// 更新 upload_info.txt 中的 file 字段(安装包路径)。
/// </summary>
BOOL WriteUploadInfoPackageFile(LPCTSTR lpInfoFile, LPCTSTR lpPackageFile)
{
std::map<std::string, std::string> updates;
updates["file"] = ToUtf8String(lpPackageFile);
return UpdateUploadInfoKeys(lpInfoFile, updates);
}
/// <summary>
/// 打印 upload 命令的用法说明。
/// </summary>
static void PrintUploadUsage()
{
_tprintf(_T("用法gitver upload username=<用户名> [file=<上传的文件>] [info=<配置.txt>] [url=<PMC基址>] [-f]\n"));
_tprintf(_T("说明info= 缺省为 upload_info.txtfile= 可省略,从 info 的 file 字段读取\n"));
_tprintf(_T("说明:登录密码从与 username 同名的环境变量读取\n"));
_tprintf(_T("说明:默认要求当前在分支上且 HEAD 指向 Tag加 -f 可忽略 Tag 校验\n"));
_tprintf(_T("示例gitver upload username=admin\n"));
}
/// <summary>
/// 校验当前 Git 仓库是否处于可上传状态(在分支上且 HEAD 指向 Tag
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <returns>0 表示通过54 表示 HEAD 游离或未指向 Tag。</returns>
static int ValidateUploadGitTagState(LPCTSTR lpRepoPath)
{
CString strStatus;
if (!TryGetGitStatus(lpRepoPath, strStatus))
{
CString strTag;
if (TryGetDetectedTag(lpRepoPath, strTag))
{
_tprintf(_T("错误: 当前处于 HEAD 游离状态Tag: %s不允许上传\n"), strTag.GetString());
}
else
{
_tprintf(_T("错误: 当前处于 HEAD 游离状态,不允许上传\n"));
}
_tprintf(_T("提示: 使用 upload ... -f 可强制上传\n"));
return 54;
}
CString strTag;
if (!TryGetDetectedTag(lpRepoPath, strTag))
{
_tprintf(_T("错误: 当前 commit 未指向 Tag 标签,不允许上传\n"));
_tprintf(_T("提示: 使用 upload ... -f 可强制上传\n"));
return 54;
}
return 0;
}
// ─────────────────────────────────────────────
// PMC 交互cpp-httplib
// ─────────────────────────────────────────────
/// <summary>
/// 向 PMC 发送登录请求,获取 Session Cookie。
/// </summary>
/// <param name="cli">httplib 客户端。</param>
/// <param name="lpUsername">登录用户名。</param>
/// <param name="lpPassword">登录密码。</param>
/// <param name="strCookies">输出:登录成功后的 Cookie 字符串。</param>
/// <returns>0 成功50 登录失败53 网络错误。</returns>
static int PmcLogin(httplib::Client& cli, LPCTSTR lpUsername, LPCTSTR lpPassword, std::string& strCookies)
{
strCookies.clear();
httplib::Params params;
params.emplace("username", ToUtf8String(lpUsername));
params.emplace("password", ToUtf8String(lpPassword));
httplib::Result result = cli.Post("/auth/login", params);
if (!result)
{
_tprintf(_T("错误: 登录请求失败(网络错误)\n"));
return 53;
}
if (result->status == 401 || result->status == 403)
{
_tprintf(_T("错误: 登录被拒绝HTTP %d\n"), result->status);
return 50;
}
if (result->status != 303)
{
_tprintf(_T("错误: 登录返回意外状态码 HTTP %d\n"), result->status);
return 50;
}
std::string strLocation = ToLowerAscii(result->get_header_value("Location"));
if (strLocation.find("/login") != std::string::npos)
{
_tprintf(_T("错误: 登录失败,用户名或密码错误\n"));
return 50;
}
CollectResponseCookies(*result, strCookies);
if (strCookies.empty())
{
_tprintf(_T("错误: 登录成功但未收到 Session Cookie\n"));
return 50;
}
_tprintf(_T("已登录 PMC 账号 %s\n"), lpUsername);
return 0;
}
/// <summary>
/// 以 multipart/form-data 方式上传安装包到 PMC。
/// </summary>
/// <param name="cli">httplib 客户端。</param>
/// <param name="strCookies">登录 Session Cookie。</param>
/// <param name="config">上传配置(包名、版本、变更日志等)。</param>
/// <param name="lpFilePath">待上传的文件路径。</param>
/// <returns>0 成功48 文件读取失败51 上传 HTTP 失败53 网络错误。</returns>
static int PmcUploadPackage(httplib::Client& cli, const std::string& strCookies, const UploadInfoConfig& config, LPCTSTR lpFilePath)
{
std::vector<BYTE> fileData;
if (!ReadBinaryFile(lpFilePath, fileData))
{
_tprintf(_T("错误: 读取上传文件失败 %s\n"), lpFilePath);
return 48;
}
std::string strFileContent((const char*)fileData.data(), fileData.size());
std::string strFileName = ToUtf8String(PathFindFileName(lpFilePath));
httplib::UploadFormDataItems items;
items.push_back({ "package_name", ToUtf8String(config.strPackageName), "", "" });
items.push_back({ "package_type_id", ToUtf8String(config.strPackageTypeId), "", "" });
items.push_back({ "description", ToUtf8String(config.strDescription), "", "" });
items.push_back({ "wiki_url", ToUtf8String(config.strWikiUrl), "", "" });
items.push_back({ "product_version", ToUtf8String(config.strProductVersion), "", "" });
items.push_back({ "file_version", ToUtf8String(config.strFileVersion), "", "" });
items.push_back({ "product_change_log", ToUtf8String(config.strProductChangeLog), "", "" });
items.push_back({ "file_change_log", ToUtf8String(config.strFileChangeLog), "", "" });
items.push_back({ "package_file", strFileContent, strFileName, "application/octet-stream" });
httplib::Result result = cli.Post("/admin/packages/upload", MakeCookieHeaders(strCookies), items);
if (!result)
{
_tprintf(_T("错误: 上传请求失败(网络错误)\n"));
return 53;
}
if (result->status == 401)
{
_tprintf(_T("错误: 上传时未登录HTTP 401\n"));
return 51;
}
if (result->status == 403)
{
_tprintf(_T("错误: 上传需要管理员权限HTTP 403\n"));
return 51;
}
if (result->status != 303)
{
_tprintf(_T("错误: 上传返回意外状态码 HTTP %d\n"), result->status);
return 51;
}
CString strLocation = Utf8ToCString(result->get_header_value("Location"));
_tprintf(_T("已提交上传请求HTTP 303 -> %s\n"), strLocation.GetString());
return 0;
}
/// <summary>
/// 在 JSON 响应体中查找 "field":"value" 形式的字段值(简单字符串匹配)。
/// </summary>
/// <param name="strBody">JSON 响应正文。</param>
/// <param name="lpField">字段名。</param>
/// <param name="lpValue">期望的字段值。</param>
/// <returns>找到匹配则返回 TRUE。</returns>
static BOOL JsonContainsFieldValue(const std::string& strBody, LPCSTR lpField, LPCSTR lpValue)
{
if (lpField == NULL || lpValue == NULL)
{
return FALSE;
}
std::string strNeedle = std::string("\"") + lpField + "\":\"" + lpValue + "\"";
return strBody.find(strNeedle) != std::string::npos;
}
/// <summary>
/// 调用 PMC API 验证上传后是否存在对应包名和版本记录。
/// </summary>
/// <param name="cli">httplib 客户端。</param>
/// <param name="strCookies">登录 Session Cookie。</param>
/// <param name="config">含包名和版本信息的配置。</param>
/// <returns>0 验证通过52 未找到版本53 网络错误。</returns>
static int VerifyUploadResult(httplib::Client& cli, const std::string& strCookies, const UploadInfoConfig& config)
{
std::string strPath = "/api/packages/latest?keyword=" +
UrlEncodeUtf8(ToUtf8String(config.strPackageName)) + "&page_size=50";
httplib::Result result = cli.Get(strPath, MakeCookieHeaders(strCookies));
if (!result)
{
_tprintf(_T("错误: 验证上传结果时网络请求失败\n"));
return 53;
}
if (result->status != 200)
{
_tprintf(_T("错误: 验证上传结果返回 HTTP %d\n"), result->status);
return 52;
}
std::string strPackageName = ToUtf8String(config.strPackageName);
std::string strProductVersion = ToUtf8String(config.strProductVersion);
std::string strFileVersion = ToUtf8String(config.strFileVersion);
if (JsonContainsFieldValue(result->body, "package_name", strPackageName.c_str()) &&
JsonContainsFieldValue(result->body, "product_version", strProductVersion.c_str()) &&
JsonContainsFieldValue(result->body, "file_version", strFileVersion.c_str()))
{
_tprintf(_T("upload ok: %s product_version="), config.strPackageName.GetString());
_tprintf(_T("%s file_version="), config.strProductVersion.GetString());
_tprintf(_T("%s\n"), config.strFileVersion.GetString());
return 0;
}
_tprintf(_T("错误: 上传后未在 PMC 中找到对应版本,可能表单校验失败或版本冲突\n"));
return 52;
}
// ─────────────────────────────────────────────
// 命令入口
// ─────────────────────────────────────────────
/// <summary>
/// 处理 upload 命令:解析参数、登录 PMC、上传包并验证结果。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <returns>0 成功47-54 各类错误,详见头文件说明。</returns>
int HandleUploadCommand(int argc, TCHAR* argv[])
{
CString strUsername;
CString strFilePath;
CString strInfoPath;
CString strUrlOverride;
BOOL bForceUpload = FALSE;
for (int i = 2; i < argc; ++i)
{
CString strArg = argv[i];
if (strArg.CompareNoCase(_T("-f")) == 0)
{
bForceUpload = TRUE;
continue;
}
if (strArg.GetLength() > 9 && strArg.Left(9).CompareNoCase(_T("username=")) == 0)
{
strUsername = strArg.Mid(9);
continue;
}
if (strArg.GetLength() > 5 && strArg.Left(5).CompareNoCase(_T("file=")) == 0)
{
strFilePath = strArg.Mid(5);
continue;
}
if (strArg.GetLength() > 5 && strArg.Left(5).CompareNoCase(_T("info=")) == 0)
{
strInfoPath = strArg.Mid(5);
continue;
}
if (strArg.GetLength() > 4 && strArg.Left(4).CompareNoCase(_T("url=")) == 0)
{
strUrlOverride = strArg.Mid(4);
continue;
}
_tprintf(_T("错误: 多余的参数 %s\n"), argv[i]);
PrintUploadUsage();
return 3;
}
if (strUsername.IsEmpty())
{
_tprintf(_T("错误: upload 缺少必填参数 username=\n"));
PrintUploadUsage();
return 47;
}
if (strInfoPath.IsEmpty())
{
strInfoPath = _T("upload_info.txt");
}
if (!PathFileExists(strInfoPath))
{
_tprintf(_T("错误: info 配置文件不存在 %s\n"), strInfoPath.GetString());
return 49;
}
UploadInfoConfig config;
config.strBaseUrl = DEFAULT_BASE_URL;
if (!LoadUploadInfoFile(strInfoPath, config))
{
return 49;
}
if (strFilePath.IsEmpty())
{
strFilePath = config.strPackageFile;
}
if (strFilePath.IsEmpty())
{
_tprintf(_T("错误: 未指定 file=,且 info 文件中缺少 file 字段\n"));
PrintUploadUsage();
return 47;
}
if (!PathFileExists(strFilePath))
{
_tprintf(_T("错误: 上传文件不存在 %s\n"), strFilePath.GetString());
return 48;
}
if (!strUrlOverride.IsEmpty())
{
config.strBaseUrl = strUrlOverride;
}
if (config.strBaseUrl.IsEmpty())
{
config.strBaseUrl = DEFAULT_BASE_URL;
}
if (!ValidateUploadConfig(config))
{
return 49;
}
if (!bForceUpload)
{
int nTagRet = ValidateUploadGitTagState(g_szCurModuleDir);
if (nTagRet != 0)
{
return nTagRet;
}
}
CString strPassword;
if (!ResolvePasswordFromEnv(strUsername, strPassword))
{
return 49;
}
std::unique_ptr<httplib::Client> cli = CreatePmcClient(config.strBaseUrl);
if (!cli)
{
_tprintf(_T("错误: 无法连接 PMC 服务 %s\n"), config.strBaseUrl.GetString());
return 53;
}
std::string strCookies;
int nLoginRet = PmcLogin(*cli, strUsername, strPassword, strCookies);
if (nLoginRet != 0)
{
return nLoginRet;
}
int nUploadRet = PmcUploadPackage(*cli, strCookies, config, strFilePath);
if (nUploadRet != 0)
{
return nUploadRet;
}
return VerifyUploadResult(*cli, strCookies, config);
}