添加文件更新工具。

This commit is contained in:
2026-03-15 09:19:30 +08:00
parent 8142ecd3fa
commit 55b8574f8a
8 changed files with 536 additions and 13 deletions

View File

@@ -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
)
add_subdirectory(strReplace)
add_subdirectory(fileUpdater)

View File

@@ -2,6 +2,10 @@
简易工具。
# ReplaceStr
# strReplace
字符串替换。
字符串替换。
# fileUpdater
文件更新工具。

View File

@@ -1,5 +0,0 @@
cmake_minimum_required(VERSION 3.16)
project(ReplaceStr LANGUAGES CXX)
add_executable(ReplaceStr main.cpp)

View File

@@ -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}
)

View File

@@ -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

462
fileUpdater/main.cpp Normal file
View File

@@ -0,0 +1,462 @@
#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");
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<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;
}
}

View File

@@ -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)