添加文件更新工具。
This commit is contained in:
@@ -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)
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
简易工具。
|
||||
|
||||
# ReplaceStr
|
||||
# strReplace
|
||||
|
||||
字符串替换。
|
||||
字符串替换。
|
||||
|
||||
# fileUpdater
|
||||
|
||||
文件更新工具。
|
||||
@@ -1,5 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(ReplaceStr LANGUAGES CXX)
|
||||
|
||||
add_executable(ReplaceStr main.cpp)
|
||||
50
fileUpdater/CMakeLists.txt
Normal file
50
fileUpdater/CMakeLists.txt
Normal 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}
|
||||
)
|
||||
7
fileUpdater/fileUpdaterVer.h.in
Normal file
7
fileUpdater/fileUpdaterVer.h.in
Normal 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
462
fileUpdater/main.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
8
strReplace/CMakeLists.txt
Normal file
8
strReplace/CMakeLists.txt
Normal 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)
|
||||
Reference in New Issue
Block a user