#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #endif namespace fs = std::filesystem; namespace xml = tinyxml2; // 文件映射结构 struct FileMapping { std::string m; // 源路径(相对工作目录) std::string n; // 目标路径(相对更新目录) std::string type; // "file" 或 "dir" std::vector 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 mappings; std::shared_ptr logger; std::string markerDir; // 标记目录名 std::string logPath_; std::string logName_{"fileUpdater.log"}; public: AutoUpdateTool() { zoostPath zp; if (!zp.GetConfigFile("fileUpdater", logName_, logPath_)) { boost::nowide::cerr << "获取日志文件路径失败。" << std::endl; exit(1); } if (!zoostFs::exists((zp.GetParentPath(logPath_)))) { if (!zp.CreateConfigDir("fileUpdater")) { boost::nowide::cerr << "创建配置目录失败。" << std::endl; exit(1); } } auto file_sink = std::make_shared(logPath_, 1024 * 1024 * 50, 3); auto console_sink = std::make_shared(); file_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e][%l]: %v"); console_sink->set_pattern("[%H:%M:%S.%e] %^[%l] %v%$"); std::vector sinks{file_sink, console_sink}; logger = std::make_shared(logName_, sinks.begin(), sinks.end()); logger->set_level(spdlog::level::debug); spdlog::register_logger(logger); } // 解析命令行参数 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); } std::string rec; for (int i = 1; i < argc; i++) { rec += argv[i]; rec += " "; } logger->debug("原始参数: {}", rec); logger->debug("CMD目录: {}", zoostFs::current_path().string()); 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 parseExtensions(const std::string& extStr) { std::vector 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(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 validateMappings() { std::vector 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& 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& 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) { zoostCommon::SetOutputU8(); zoostCommon::SetStdLibrayU8(); AutoUpdateTool tool; if (tool.parseArguments(argc, argv)) { return tool.run(); } else { return 1; } }