diff --git a/CMakeLists.txt b/CMakeLists.txt index a78da3b..41a2d12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,11 +8,8 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin/${CMAKE_BUILD_TYPE} if (MSVC) add_compile_options(/utf-8) + add_definitions(-D_CRT_SECURE_NO_WARNINGS) endif() -find_package(Boost REQUIRED) - -add_subdirectory(ReplaceStr) -target_link_libraries(ReplaceStr PRIVATE - Boost::headers -) \ No newline at end of file +add_subdirectory(strReplace) +add_subdirectory(fileUpdater) \ No newline at end of file diff --git a/README.md b/README.md index 08f8b1a..3ca9efb 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ 简易工具。 -# ReplaceStr +# strReplace -字符串替换。 \ No newline at end of file +字符串替换。 + +# fileUpdater + +文件更新工具。 \ No newline at end of file diff --git a/ReplaceStr/CMakeLists.txt b/ReplaceStr/CMakeLists.txt deleted file mode 100644 index ac80228..0000000 --- a/ReplaceStr/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -cmake_minimum_required(VERSION 3.16) - -project(ReplaceStr LANGUAGES CXX) - -add_executable(ReplaceStr main.cpp) \ No newline at end of file diff --git a/fileUpdater/CMakeLists.txt b/fileUpdater/CMakeLists.txt new file mode 100644 index 0000000..330ce4d --- /dev/null +++ b/fileUpdater/CMakeLists.txt @@ -0,0 +1,50 @@ +cmake_minimum_required(VERSION 3.16) + +project(fileUpdater LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +if (MSVC) + add_compile_options(/utf-8) + add_definitions(-D_CRT_SECURE_NO_WARNINGS) +endif() + +execute_process( + COMMAND git rev-parse --short HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE VERSION_GIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE +) +execute_process( + COMMAND git rev-parse --abbrev-ref HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE VERSION_GIT_BRANCH + OUTPUT_STRIP_TRAILING_WHITESPACE +) +configure_file(fileUpdaterVer.h.in fileUpdaterVer.h) +message(STATUS "Version file config to: ${CMAKE_CURRENT_BINARY_DIR}") +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +find_package(fmt REQUIRED) +find_package(tinyxml2 REQUIRED) +find_package(CLI11 REQUIRED) +find_package(spdlog REQUIRED) +find_package(Boost REQUIRED COMPONENTS locale filesystem nowide) + +add_executable(fileUpdater main.cpp) +target_link_libraries(fileUpdater PRIVATE + fmt::fmt + Boost::locale + Boost::filesystem + Boost::nowide + CLI11::CLI11 + spdlog::spdlog + tinyxml2::tinyxml2 +) + +include(GNUInstallDirs) +install(TARGETS fileUpdater + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) diff --git a/fileUpdater/fileUpdaterVer.h.in b/fileUpdater/fileUpdaterVer.h.in new file mode 100644 index 0000000..806c929 --- /dev/null +++ b/fileUpdater/fileUpdaterVer.h.in @@ -0,0 +1,7 @@ +#ifndef FILEUPDATER_VERSION_H +#define FILEUPDATER_VERSION_H + +#define VERSION_GIT_COMMIT "@VERSION_GIT_HASH@" +#define VERSION_GIT_BRANCH "@VERSION_GIT_BRANCH@" + +#endif // FILEUPDATER_VERSION_H \ No newline at end of file diff --git a/fileUpdater/main.cpp b/fileUpdater/main.cpp new file mode 100644 index 0000000..fd4a421 --- /dev/null +++ b/fileUpdater/main.cpp @@ -0,0 +1,462 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +namespace fs = boost::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; // 标记目录名 + +public: + AutoUpdateTool() + { + logger = spdlog::stdout_color_mt("AutoUpdate"); + logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v"); + } + + // 解析命令行参数 + 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 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) +{ +#ifdef _WIN32 + SetConsoleOutputCP(CP_UTF8); +#endif + + boost::nowide::nowide_filesystem(); + AutoUpdateTool tool; + + if (tool.parseArguments(argc, argv)) { + return tool.run(); + } else { + return 1; + } +} diff --git a/strReplace/CMakeLists.txt b/strReplace/CMakeLists.txt new file mode 100644 index 0000000..8f31d0d --- /dev/null +++ b/strReplace/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.16) + +project(strReplace LANGUAGES CXX) + +find_package(Boost REQUIRED) + +add_executable(strReplace main.cpp) +target_link_libraries(strReplace PRIVATE Boost::headers) \ No newline at end of file diff --git a/ReplaceStr/main.cpp b/strReplace/main.cpp similarity index 100% rename from ReplaceStr/main.cpp rename to strReplace/main.cpp