2026-03-15 09:19:30 +08:00
|
|
|
#include <CLI/CLI.hpp>
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <boost/algorithm/string.hpp>
|
|
|
|
|
#include <boost/filesystem.hpp>
|
|
|
|
|
#include <boost/nowide/filesystem.hpp>
|
|
|
|
|
#include <boost/nowide/iostream.hpp>
|
|
|
|
|
#include <chrono>
|
|
|
|
|
#include <fileUpdaterVer.h>
|
|
|
|
|
#include <iomanip>
|
|
|
|
|
#include <iostream>
|
|
|
|
|
#include <spdlog/sinks/stdout_color_sinks.h>
|
|
|
|
|
#include <spdlog/spdlog.h>
|
|
|
|
|
#include <sstream>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <tinyxml2.h>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
#include <windows.h>
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
namespace fs = boost::filesystem;
|
|
|
|
|
namespace xml = tinyxml2;
|
|
|
|
|
|
|
|
|
|
// 文件映射结构
|
|
|
|
|
struct FileMapping {
|
|
|
|
|
std::string m; // 源路径(相对工作目录)
|
|
|
|
|
std::string n; // 目标路径(相对更新目录)
|
|
|
|
|
std::string type; // "file" 或 "dir"
|
|
|
|
|
std::vector<std::string> extensions; // 扩展名过滤列表
|
|
|
|
|
bool isFile; // 是否为文件
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 操作记录结构
|
|
|
|
|
struct OperationRecord {
|
|
|
|
|
fs::path source; // 源文件绝对路径
|
|
|
|
|
fs::path target; // 目标文件绝对路径
|
|
|
|
|
fs::path backup; // 备份路径
|
|
|
|
|
bool exists; // 目标文件是否存在
|
|
|
|
|
bool isNew; // 是否是新增文件
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class AutoUpdateTool
|
|
|
|
|
{
|
|
|
|
|
private:
|
|
|
|
|
std::string configFile;
|
|
|
|
|
fs::path workDir; // 工作目录(新文件所在)
|
|
|
|
|
fs::path updateDir; // 更新目录(目标目录)
|
|
|
|
|
fs::path backupDir; // 备份目录
|
|
|
|
|
std::string fromType; // 源类型:"m" 或 "n"
|
|
|
|
|
std::string toType; // 目标类型:"m" 或 "n"
|
|
|
|
|
std::vector<FileMapping> mappings;
|
|
|
|
|
std::shared_ptr<spdlog::logger> logger;
|
|
|
|
|
std::string markerDir; // 标记目录名
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
AutoUpdateTool()
|
|
|
|
|
{
|
|
|
|
|
logger = spdlog::stdout_color_mt("AutoUpdate");
|
2026-03-15 09:33:33 +08:00
|
|
|
logger->set_pattern("[%H:%M:%S.%e] %^[%l] %v%$");
|
2026-03-15 09:19:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析命令行参数
|
|
|
|
|
bool parseArguments(int argc, char** argv)
|
|
|
|
|
{
|
|
|
|
|
auto msg = fmt::format("fileUpdater - 文件更新工具 \n\n {} on {} at {} {}", VERSION_GIT_COMMIT, VERSION_GIT_BRANCH,
|
|
|
|
|
__DATE__, __TIME__);
|
|
|
|
|
CLI::App app{msg};
|
|
|
|
|
argv = app.ensure_utf8(argv);
|
|
|
|
|
|
|
|
|
|
app.add_option("-c,--config", configFile, "XML配置文件路径")->required()->check(CLI::ExistingFile);
|
|
|
|
|
|
|
|
|
|
app.add_option("-w,--work-dir", workDir, "工作目录(新文件所在)")->default_val(fs::current_path());
|
|
|
|
|
|
|
|
|
|
app.add_option("-u,--update-dir", updateDir, "更新目录(目标目录)")->required();
|
|
|
|
|
|
|
|
|
|
app.add_option("-b,--backup-dir", backupDir, "备份目录")->required();
|
|
|
|
|
|
|
|
|
|
app.add_option("-f,--from", fromType, "源类型 (m 或 n)")->required()->check(CLI::IsMember({"m", "n"}));
|
|
|
|
|
|
|
|
|
|
app.add_option("-t,--to", toType, "目标类型 (m 或 n)")->required()->check(CLI::IsMember({"m", "n"}));
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
app.parse(argc, argv);
|
|
|
|
|
|
|
|
|
|
// 规范化路径
|
|
|
|
|
if (workDir.is_relative()) {
|
|
|
|
|
workDir = fs::absolute(workDir);
|
|
|
|
|
}
|
|
|
|
|
if (updateDir.is_relative()) {
|
|
|
|
|
updateDir = fs::absolute(updateDir);
|
|
|
|
|
}
|
|
|
|
|
if (backupDir.is_relative()) {
|
|
|
|
|
backupDir = fs::absolute(backupDir);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger->info("工作目录: {}", workDir.string());
|
|
|
|
|
logger->info("更新目录: {}", updateDir.string());
|
|
|
|
|
logger->info("备份目录: {}", backupDir.string());
|
|
|
|
|
logger->info("映射方向: {} -> {}", fromType, toType);
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
} catch (const CLI::ParseError& e) {
|
|
|
|
|
return app.exit(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析扩展名列表
|
|
|
|
|
std::vector<std::string> parseExtensions(const std::string& extStr)
|
|
|
|
|
{
|
|
|
|
|
std::vector<std::string> exts;
|
|
|
|
|
if (!extStr.empty()) {
|
|
|
|
|
boost::split(exts, extStr, boost::is_any_of("|"));
|
|
|
|
|
|
|
|
|
|
// 确保扩展名以点开头
|
|
|
|
|
for (auto& ext : exts) {
|
|
|
|
|
boost::trim(ext);
|
|
|
|
|
if (!ext.empty() && ext[0] != '.') {
|
|
|
|
|
ext = "." + ext;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return exts;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 加载XML配置文件
|
|
|
|
|
bool loadConfig()
|
|
|
|
|
{
|
|
|
|
|
if (configFile.empty()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
xml::XMLDocument doc;
|
|
|
|
|
if (doc.LoadFile(configFile.c_str()) != xml::XML_SUCCESS) {
|
|
|
|
|
logger->error("无法加载配置文件: {}", configFile);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto root = doc.FirstChildElement("AutoUpdate");
|
|
|
|
|
if (!root) {
|
|
|
|
|
logger->error("配置文件格式错误: 未找到AutoUpdate根节点");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 遍历所有FileMapping节点
|
|
|
|
|
for (auto elem = root->FirstChildElement("FileMapping"); elem != nullptr;
|
|
|
|
|
elem = elem->NextSiblingElement("FileMapping")) {
|
|
|
|
|
|
|
|
|
|
FileMapping mapping;
|
|
|
|
|
mapping.m = elem->Attribute("m");
|
|
|
|
|
mapping.n = elem->Attribute("n");
|
|
|
|
|
mapping.type = elem->Attribute("type");
|
|
|
|
|
mapping.isFile = (mapping.type == "file");
|
|
|
|
|
|
|
|
|
|
const char* extAttr = elem->Attribute("ext");
|
|
|
|
|
std::string extStr = extAttr ? extAttr : "";
|
|
|
|
|
mapping.extensions = parseExtensions(extStr);
|
|
|
|
|
|
|
|
|
|
mappings.push_back(mapping);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger->info("已加载 {} 个文件映射配置", mappings.size());
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查文件或目录是否存在
|
|
|
|
|
bool checkPath(const fs::path& path, bool isFile)
|
|
|
|
|
{
|
|
|
|
|
if (isFile) {
|
|
|
|
|
return fs::is_regular_file(path);
|
|
|
|
|
} else {
|
|
|
|
|
return fs::is_directory(path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取当前时间戳字符串
|
|
|
|
|
std::string getCurrentTimestamp()
|
|
|
|
|
{
|
|
|
|
|
auto now = std::chrono::system_clock::now();
|
|
|
|
|
auto in_time_t = std::chrono::system_clock::to_time_t(now);
|
|
|
|
|
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
|
|
|
|
|
|
|
|
|
|
std::stringstream ss;
|
|
|
|
|
ss << std::put_time(std::localtime(&in_time_t), "%Y_%m%d_%H%M%S_");
|
|
|
|
|
ss << std::setfill('0') << std::setw(3) << ms.count();
|
|
|
|
|
return ss.str();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证映射并生成操作记录
|
|
|
|
|
std::vector<OperationRecord> validateMappings()
|
|
|
|
|
{
|
|
|
|
|
std::vector<OperationRecord> records;
|
|
|
|
|
|
|
|
|
|
for (const auto& mapping : mappings) {
|
|
|
|
|
// 确定源路径和目标路径
|
|
|
|
|
std::string fromPath, toPath;
|
|
|
|
|
if (fromType == "m") {
|
|
|
|
|
fromPath = mapping.m;
|
|
|
|
|
} else {
|
|
|
|
|
fromPath = mapping.n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (toType == "m") {
|
|
|
|
|
toPath = mapping.m;
|
|
|
|
|
} else {
|
|
|
|
|
toPath = mapping.n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建绝对路径
|
|
|
|
|
fs::path sourceAbs = workDir / fromPath;
|
|
|
|
|
fs::path targetAbs = updateDir / toPath;
|
|
|
|
|
|
|
|
|
|
// 步骤1:检查源路径是否存在
|
|
|
|
|
if (!checkPath(sourceAbs, mapping.isFile)) {
|
|
|
|
|
logger->error("源路径不存在: {}", sourceAbs.string());
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查是否是同一个文件
|
|
|
|
|
if (fs::exists(targetAbs)) {
|
|
|
|
|
try {
|
|
|
|
|
if (fs::equivalent(sourceAbs, targetAbs)) {
|
|
|
|
|
logger->warn("源和目标相同,跳过: {}", sourceAbs.string());
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
} catch (const fs::filesystem_error& e) {
|
|
|
|
|
// 如果无法比较等价性,继续执行
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OperationRecord record;
|
|
|
|
|
record.source = sourceAbs;
|
|
|
|
|
record.target = targetAbs;
|
|
|
|
|
record.exists = fs::exists(targetAbs);
|
|
|
|
|
record.isNew = !record.exists;
|
|
|
|
|
|
|
|
|
|
if (mapping.isFile) {
|
|
|
|
|
// 文件处理
|
|
|
|
|
records.push_back(record);
|
|
|
|
|
} else {
|
|
|
|
|
// 目录处理
|
|
|
|
|
if (fs::is_directory(sourceAbs)) {
|
|
|
|
|
for (const auto& entry : fs::recursive_directory_iterator(sourceAbs)) {
|
|
|
|
|
if (fs::is_regular_file(entry)) {
|
|
|
|
|
// 计算相对路径
|
|
|
|
|
fs::path relPath = fs::relative(entry.path(), sourceAbs);
|
|
|
|
|
|
|
|
|
|
// 检查扩展名过滤
|
|
|
|
|
if (!mapping.extensions.empty()) {
|
|
|
|
|
std::string ext = entry.path().extension().string();
|
|
|
|
|
if (std::find(mapping.extensions.begin(), mapping.extensions.end(), ext) ==
|
|
|
|
|
mapping.extensions.end()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OperationRecord dirRecord;
|
|
|
|
|
dirRecord.source = entry.path();
|
|
|
|
|
dirRecord.target = targetAbs / relPath;
|
|
|
|
|
dirRecord.exists = fs::exists(dirRecord.target);
|
|
|
|
|
dirRecord.isNew = !dirRecord.exists;
|
|
|
|
|
records.push_back(dirRecord);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return records;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 显示预执行计划
|
|
|
|
|
void showPlan(const std::vector<OperationRecord>& records)
|
|
|
|
|
{
|
|
|
|
|
logger->info("=== 更新计划 ===");
|
|
|
|
|
|
|
|
|
|
int fileCount = 0;
|
|
|
|
|
int dirCount = 0;
|
|
|
|
|
int newCount = 0;
|
|
|
|
|
int updateCount = 0;
|
|
|
|
|
|
|
|
|
|
for (const auto& record : records) {
|
|
|
|
|
if (record.isNew) {
|
|
|
|
|
newCount++;
|
|
|
|
|
logger->info("[新增] {} -> {}", record.source.string(), record.target.string());
|
|
|
|
|
} else {
|
|
|
|
|
updateCount++;
|
|
|
|
|
logger->info("[更新] {} -> {}", record.source.string(), record.target.string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger->info("=== 统计 ===");
|
|
|
|
|
logger->info("新增文件: {} 个", newCount);
|
|
|
|
|
logger->info("更新文件: {} 个", updateCount);
|
|
|
|
|
logger->info("总计: {} 个文件", records.size());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取用户输入的标记目录
|
|
|
|
|
bool getMarkerDirectory()
|
|
|
|
|
{
|
|
|
|
|
std::cout << "\n请输入标记目录名称(留空使用时间戳): ";
|
|
|
|
|
std::getline(boost::nowide::cin, markerDir);
|
|
|
|
|
|
|
|
|
|
if (markerDir.empty()) {
|
|
|
|
|
markerDir = getCurrentTimestamp();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查是否已存在
|
|
|
|
|
fs::path markerPath = backupDir / markerDir;
|
|
|
|
|
if (fs::exists(markerPath)) {
|
|
|
|
|
logger->error("标记目录已存在: {}", markerPath.string());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger->info("标记目录: {}", markerDir);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 执行备份操作
|
|
|
|
|
bool backupFile(const OperationRecord& record)
|
|
|
|
|
{
|
|
|
|
|
try {
|
|
|
|
|
// 计算相对更新目录的路径
|
|
|
|
|
fs::path relPath = fs::relative(record.target, updateDir);
|
|
|
|
|
fs::path backupPath = backupDir / markerDir / relPath;
|
|
|
|
|
|
|
|
|
|
// 创建目标目录
|
|
|
|
|
fs::create_directories(backupPath.parent_path());
|
|
|
|
|
|
|
|
|
|
// 复制文件
|
|
|
|
|
fs::copy(record.target, backupPath, fs::copy_options::overwrite_existing);
|
|
|
|
|
|
|
|
|
|
logger->debug("已备份: {} -> {}", record.target.string(), backupPath.string());
|
|
|
|
|
return true;
|
|
|
|
|
} catch (const fs::filesystem_error& e) {
|
|
|
|
|
logger->error("备份失败: {} - {}", record.target.string(), e.what());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 执行更新操作
|
|
|
|
|
bool updateFile(const OperationRecord& record)
|
|
|
|
|
{
|
|
|
|
|
try {
|
|
|
|
|
// 创建目标目录
|
|
|
|
|
fs::create_directories(record.target.parent_path());
|
|
|
|
|
|
|
|
|
|
// 复制文件
|
|
|
|
|
fs::copy(record.source, record.target, fs::copy_options::overwrite_existing);
|
|
|
|
|
|
|
|
|
|
if (record.isNew) {
|
|
|
|
|
logger->info("[新增] {}", record.target.string());
|
|
|
|
|
} else {
|
|
|
|
|
logger->info("[更新] {}", record.target.string());
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
} catch (const fs::filesystem_error& e) {
|
|
|
|
|
logger->error("更新失败: {} -> {} - {}", record.source.string(), record.target.string(), e.what());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 执行更新
|
|
|
|
|
bool executeUpdate(const std::vector<OperationRecord>& records)
|
|
|
|
|
{
|
|
|
|
|
int successCount = 0;
|
|
|
|
|
int backupCount = 0;
|
|
|
|
|
|
|
|
|
|
logger->info("开始执行更新...");
|
|
|
|
|
|
|
|
|
|
// 先备份所有需要更新的文件
|
|
|
|
|
for (const auto& record : records) {
|
|
|
|
|
if (!record.isNew) { // 只有已存在的文件需要备份
|
|
|
|
|
if (backupFile(record)) {
|
|
|
|
|
backupCount++;
|
|
|
|
|
} else {
|
|
|
|
|
logger->warn("备份失败,但继续执行更新");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger->info("已备份 {} 个文件", backupCount);
|
|
|
|
|
|
|
|
|
|
// 执行更新
|
|
|
|
|
for (const auto& record : records) {
|
|
|
|
|
if (updateFile(record)) {
|
|
|
|
|
successCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 备份配置文件
|
|
|
|
|
try {
|
|
|
|
|
fs::path configBackupPath = backupDir / (markerDir + ".xml");
|
|
|
|
|
fs::copy(configFile, configBackupPath, fs::copy_options::overwrite_existing);
|
|
|
|
|
logger->info("配置文件已备份到: {}", configBackupPath.string());
|
|
|
|
|
} catch (const fs::filesystem_error& e) {
|
|
|
|
|
logger->error("配置文件备份失败: {}", e.what());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger->info("=== 完成 ===");
|
|
|
|
|
logger->info("成功更新 {} 个文件(共 {} 个)", successCount, records.size());
|
|
|
|
|
logger->info("标记目录: {}", markerDir);
|
|
|
|
|
|
|
|
|
|
return successCount == records.size();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 运行主流程
|
|
|
|
|
int run()
|
|
|
|
|
{
|
|
|
|
|
if (!loadConfig()) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证并生成操作记录
|
|
|
|
|
auto records = validateMappings();
|
|
|
|
|
if (records.empty()) {
|
|
|
|
|
logger->warn("没有找到需要更新的文件");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 显示计划
|
|
|
|
|
showPlan(records);
|
|
|
|
|
|
|
|
|
|
// 获取标记目录
|
|
|
|
|
if (!getMarkerDirectory()) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确认执行
|
|
|
|
|
std::cout << "\n确认执行更新操作?(y/yes 确认,其他取消): ";
|
|
|
|
|
std::string confirm;
|
|
|
|
|
std::getline(boost::nowide::cin, confirm);
|
|
|
|
|
|
|
|
|
|
if (confirm != "y" && confirm != "yes") {
|
|
|
|
|
logger->info("操作已取消");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 执行更新
|
|
|
|
|
if (!executeUpdate(records)) {
|
|
|
|
|
logger->error("更新过程中出现错误");
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
int main(int argc, char** argv)
|
|
|
|
|
{
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
SetConsoleOutputCP(CP_UTF8);
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
boost::nowide::nowide_filesystem();
|
|
|
|
|
AutoUpdateTool tool;
|
|
|
|
|
|
|
|
|
|
if (tool.parseArguments(argc, argv)) {
|
|
|
|
|
return tool.run();
|
|
|
|
|
} else {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
}
|