From bac83f6cf8205d089d72652ba572f9d3b9682b61 Mon Sep 17 00:00:00 2001 From: taynpg Date: Mon, 30 Mar 2026 16:06:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E7=BB=93=E6=9E=84=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .clang-format | 20 ++ .clangd | 12 + .gitignore | 1 + .vscode/settings.json | 145 +++++++++++ CMakeLists.txt | 30 +++ LibUse.md | 366 ++++++++++++++++++++++++++ README.md | 11 + ToolBox.h.in | 10 + fileUpdater/CMakeLists.txt | 22 ++ fileUpdater/main.cpp | 512 +++++++++++++++++++++++++++++++++++++ 10 files changed, 1129 insertions(+) create mode 100644 .clang-format create mode 100644 .clangd create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 CMakeLists.txt create mode 100644 LibUse.md create mode 100644 README.md create mode 100644 ToolBox.h.in create mode 100644 fileUpdater/CMakeLists.txt create mode 100644 fileUpdater/main.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..8d63d3f --- /dev/null +++ b/.clang-format @@ -0,0 +1,20 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +PointerAlignment: Left +AccessModifierOffset: -4 +ReflowComments: true +SpacesBeforeTrailingComments: 3 +AllowShortFunctionsOnASingleLine: None +AllowShortEnumsOnASingleLine: false +BreakBeforeBraces: Custom +BraceWrapping: + AfterFunction: true + AfterClass: true +TabWidth: 4 +ColumnLimit: 130 +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^<.*>' + Priority: 1 + - Regex: '^".*"' + Priority: 2 diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..40557fb --- /dev/null +++ b/.clangd @@ -0,0 +1,12 @@ +Hover: + ShowAKA: Yes +Diagnostics: + UnusedIncludes: None # 禁用未使用头文件提示 + Suppress: [ + anon_type_definition, # 禁用匿名的typedef提示 + unused-variable, # 禁用未使用变量提示 + unused-function, # 禁用未使用函数提示 + unused-includes, + ] + ClangTidy: + Remove: misc-unused-alias-decls diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30d388a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fcf3710 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,145 @@ +{ + "files.autoSave": "onFocusChange", + "editor.fontSize": 14, + "editor.fontFamily": "'Mononoki Nerd Font Mono', 'Mononoki Nerd Font Mono', 'Mononoki Nerd Font Mono'", + "editor.wordWrap": "on", + "terminal.integrated.fontFamily": "'Mononoki Nerd Font Mono'", + "cmake.configureOnOpen": true, + //"C_Cpp.intelliSenseEngine": "disabled", + "cmake.debugConfig": { + "console": "externalTerminal", + "setupCommands": [ + { + "description": "-gdb-set charset utf-8", + "text": "-gdb-set charset UTF-8" + }, + { + "description": "Enable gdb pretty-printing", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + //"visualizerFile": "${workspaceRoot}/.vscode/qt6.natvis", + }, + "cmake.configureArgs": [ + "-Wno-dev" + ], + "cmake.configureSettings": { + //"CMAKE_TOOLCHAIN_FILE": "${env:VCPKG_DIR}/scripts/buildsystems/vcpkg.cmake" + "CMAKE_PREFIX_PATH": "C:/local" + }, + "cmake.options.statusBarVisibility": "visible", + "cmake.generator": "Ninja", + "C_Cpp.default.compileCommands": "${workspaceRoot}/build/compile_commands.json", + "C_Cpp.default.cppStandard": "c++17", + "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", + "editor.inlayHints.enabled": "off", + "editor.unicodeHighlight.allowedLocales": { + "ja": true, + "zh-hant": true, + "zh-hans": true + }, + "files.associations": { + "*.cfg": "json", + "xstring": "cpp", + "algorithm": "cpp", + "array": "cpp", + "atomic": "cpp", + "cctype": "cpp", + "charconv": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "condition_variable": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "exception": "cpp", + "filesystem": "cpp", + "forward_list": "cpp", + "fstream": "cpp", + "functional": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "ios": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "iterator": "cpp", + "limits": "cpp", + "list": "cpp", + "locale": "cpp", + "map": "cpp", + "memory": "cpp", + "mutex": "cpp", + "new": "cpp", + "optional": "cpp", + "ostream": "cpp", + "ratio": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "string": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "thread": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "typeinfo": "cpp", + "unordered_map": "cpp", + "utility": "cpp", + "vector": "cpp", + "xfacet": "cpp", + "xhash": "cpp", + "xiosbase": "cpp", + "xlocale": "cpp", + "xlocbuf": "cpp", + "xlocinfo": "cpp", + "xlocmes": "cpp", + "xlocmon": "cpp", + "xlocnum": "cpp", + "xloctime": "cpp", + "xmemory": "cpp", + "xmemory0": "cpp", + "xstddef": "cpp", + "xtr1common": "cpp", + "xtree": "cpp", + "xutility": "cpp", + "bit": "cpp", + "compare": "cpp", + "concepts": "cpp", + "coroutine": "cpp", + "format": "cpp", + "stop_token": "cpp", + "bitset": "cpp", + "complex": "cpp", + "cstdarg": "cpp", + "set": "cpp", + "variant": "cpp", + "expected": "cpp", + "source_location": "cpp", + "regex": "cpp", + "*.in": "cpp", + "deque": "cpp", + "future": "cpp", + "queue": "cpp", + "resumable": "cpp", + "any": "cpp", + "codecvt": "cpp", + "csignal": "cpp", + "cwctype": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "random": "cpp", + "shared_mutex": "cpp", + "cinttypes": "cpp", + "cfenv": "cpp", + "unordered_set": "cpp", + "ranges": "cpp", + "typeindex": "cpp" + } +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..bc11c24 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.16) + +project(ToolBox LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) + +set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib/${CMAKE_BUILD_TYPE}) +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() + +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(ToolBox.h.in ToolBox.h) +message(STATUS "Version file config to: ${CMAKE_CURRENT_BINARY_DIR}") +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +add_subdirectory(fileUpdater) \ No newline at end of file diff --git a/LibUse.md b/LibUse.md new file mode 100644 index 0000000..36c7de1 --- /dev/null +++ b/LibUse.md @@ -0,0 +1,366 @@ +# Spdlog + +## sink + +spdlog定义了几种sinks用于不同场景(也可自定义)下的日志输出,sink中主要包含: + +- `set_pattern(const std::string&)`:设置日志输出的内容格式。 +- `set_level(level_enum)`: 设置日志输出的最低等级。 +- `log(log_msg)`:由logger自动调用,外部不会主动调用。 + +## logger + +一个logger对象中存储有多个sink,当调用*logger*的日志输出函数时,*logger*会调用自身存储的所有*sink*对象的log(log_msg) 函数进行输出。logger中主要包括: + +- `set_pattern(const std::string&)`:设置logger包含的所有sink的日志输出内容格式。 +- `set_level(level_enum)`:设置logger日志输出最低等级,如果logger包含的sink没有设置日志等级的话,则会为其设置日志等级。 +- `log(level_enum level,log_msg content)`:按照level等级进行输出content,logger其中日志输出最低等级小于或等于level的sink会进行执行输出操作。 +- `trace(content,arg1,arg2…)`:按照trace等级进行输出,输出内容由content与后面的参数格式化而成。同类的函数还包括:debug/info/warn…。 + +## 输出格式pattern + +通过`set_pattern`可设定日志格式,如`set_pattern("[%Y-%m-%d %H:%M:%S.%e][%l](%@): %v");` + +输出标记flag: + +| flag | meaning | example | +| ----- | ------------------------------------------------------------ | ---------------------------------------------------------- | +| %v | 日志内容 | “my log test content” | +| %t | 线程ID | “123” | +| %P | 进程ID | “234” | +| %n | 记录器Logger名 | “basicLogger” | +| %l | 日志级别 | “debug”, “info”, etc | +| %L | 日志级别简称 | “D”, “I”, etc | +| %a | 星期几(简称) | “Thu” | +| %A | 星期几 | “Thursday” | +| %b | 月份简称 | “Aug” | +| %B | 月份 | “August” | +| %c | 日期时间 | “Thu Aug 23 15:35:46 2014” | +| %C | 年(两位) | “14” | +| %Y | 年 | “2014” | +| %D %x | 日期简写 | “08/23/14” | +| %m | 月份(数字) | “11” | +| %d | 日(数组) | “29” | +| %H | 小时(24制) | “23” | +| %I | 小时(12制) | “11” | +| %M | 分钟 | “59” | +| %S | 秒 | “58” | +| %e | 毫秒 | “678” | +| %f | 微秒 | “056789” | +| %F | 纳秒 | “256789123” | +| %p | AM/PM | “AM” | +| %r | 时间(12制) | “02:55:02 pm” | +| %R | 时分(24制) | “23:55” | +| %T %X | 时间(24制) | “23:55:59” | +| %z | 时区(偏移) | “+02:00” | +| %E | epoch(秒) | “1528834770” | +| %% | 百分号 | “%” | +| %+ | 默认格式 | “[2014-10-31 23:46:59.678] [mylogger] [info] Some message” | +| %^ | start color range (can be used only once) | “[mylogger] [info(green)] Some message” | +| %$ | end color range (for example %^[+++]%$ %v) (can be used only once) | [+++] Some message | +| %@ | 文件名与行数 | my_file.cpp:123 | +| %s | 文件名 | my_file.cpp | +| %g | 文件名(含路径) | /some/dir/my_file.cpp | +| %# | 行数 | 123 | +| %! | 函数名 | my_func | +| %o | 相对上一条记录的时间间隔(毫秒) | 456 | +| %i | 相对上一条记录的时间间隔(微秒) | 456 | +| %u | 相对上一条记录的时间间隔(纳秒) | 11456 | +| %O | 相对上一条记录的时间间隔(秒) | 4 | + +日志输出中要携带文件名、行数或函数名时,必须使用`SPDLOG_LOGGER_*`宏,且要激活对应的级别(哪些级别以上的日志会被记录): + +```text +// 记录INFO及以上级别日志 +#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_INFO +#include "spdlog/spdlog.h" + +SPDLOG_LOGGER_INFO(myLogger, "Support for floats {:03.2f}", 1.23456); +SPDLOG_LOGGER_WARN(myLogger, "Easy padding in numbers like {:08d}", 12); +``` + +### 对齐方式 + +每个flag都可携带对齐方式(最多支持64字符), + +| align | meaning | example | result | +| --------------- | ------- | ------- | -------- | +| % | 右对齐 | %8l | " info" | +| %- | 左对齐 | %-8l | "info " | +| %= | 居中 | %=8l | " info " | + +### 截断 + +通过!可设定对应输出的最大长度: + +| align | meaning | example | result | +| ---------------- | ------------ | ------- | ------ | +| %! | 右对齐且截断 | %3!l | “inf” | +| %-! | 左对齐且截断 | %-2!l | “in” | +| %=! | 居中且截断 | %=1!l | “i” | + +## 字符串格式化fmt + +spdlog中字符串格式化使用fmt([https://github.com/fmtlib/fmt](https://link.zhihu.com/?target=https%3A//github.com/fmtlib/fmt))库。 + +格式化方式:`{ [arg_id] [: (format_spec | chrono_format_spec)] }` + +- arg_id:参数标识; + - 忽略(为空时),依次对应每一个参数; + - 索引(数字,从0开始),引用第几个索引; + - 名称,命名参数; +- format_spec:参数格式化方式(类型、对齐、填充等); + +### Format Specification + +格式化符说明: + +```text +format_spec ::= [[fill]align][sign]["#"]["0"][width]["." precision]["L"][type] +fill ::= +align ::= "<" | ">" | "^" // 左、右、居中对齐 +sign ::= "+" | "-" | " " +width ::= integer | {[arg_id]} // 宽度:数字或指定的参数 +precision ::= integer | {[arg_id]} // 精度:数字或指定的参数 +type ::= "a" | "A" | "b" | "B" | "c" | "d" | "e" | "E" | "f" | "F" + | "g" | "G" | "o" | "p" | "s" | "x" | "X" +``` + +`#`不同的转换下有不同的意义: + +- 整数时,表示前面添加进制前缀,如0x, 0b等; +- 浮点数时:总是有小数点(即使没有小数部分); + +`L`只对数字有效,根据本地设置来输出:如, + +```text +auto s = fmt::format(std::locale("en_US.UTF-8"), "{:L}", 1234567890); +// s == "1,234,567,890" +``` + +格式化类型: + +格式化类型: + +| type | meaning | +| ---- | ---------------------------------------- | +| s | 字符串 | +| c | 字符 | +| b/B | 二进制 | +| d | 数字(十进制) | +| o | 八进制 | +| x/X | 十六进制 | +| a/A | 十六进制浮点数(p表示指数) | +| e/E | 科学计数 | +| f/F | 浮点数(包括NAN,INF),固定小数位数输出 | +| g/G | 浮点数输出 | +| p | 指针 | + +示例: + +```text +fmt::format("{:*^30}", "centered"); // use '*' as a fill char +// Result: "***********centered***********" + +fmt::format("{:#04x}", 0); +// Result: "0x00" + +fmt::print( + "┌{0:─^{2}}┐\n" + "│{1: ^{2}}│\n" + "└{0:─^{2}}┘\n", "", "Hello, world!", 20); +┌────────────────────┐ +│ Hello, world! │ +└────────────────────┘ +``` + +## spdlog使用 + +spdlog默认日志输出级别是INFO。 + +默认情况下,日志是同步模式的,可通过以下方法开启异步模式: + +```text +size_t q_size = 4096; //queue size must be power of 2 +spdlog::set_async_mode(q_size); +``` + +在异步模式下,日志先存入队列(队列占用的内存 = 设置的队列大小 * slot的大小, 64位系统下slot大小为104字节。),再由工作者线程从队列中取出并输出。当队列满时,会根据设定策略处理: + +- 阻塞新来的日志,直到队列中有剩余空间(默认处理方式); +- 丢弃新来的日志,需要如下设定策略: + +```text +spdlog::set_async_mode(q_size, spdlog::async_overflow_policy::discard_log_msg); +``` + +## 异常处理 + +当输出日志时发生异常时,spdlog会向std::err 打印一条语句,为了避免输出的异常语句刷屏,打印频率被限制在每分钟一条。可通过set_error_handler来设定异常处理函数: + +```text + //can be set globaly or per logger(logger->set_error_handler(..)) + spdlog::set_error_handler([](const std::string& msg) + { + std::cerr << "my err handler: " << msg << std::endl; + }); +``` + +## logger + +默认情况下,spdlog的默认logger为输出到stdout: + +```text +# ifdef _WIN32 + auto color_sink = std::make_shared(); +# else + auto color_sink = std::make_shared(); +# endif +``` + +在使用完logger后,要关闭掉以释放(否则无再建立同名logger) + +```text +spdlog::drop_all(); // 关闭所有logger +spd::drop("basic_logger"); // 关闭指定logger +``` + +### 基础用法 + +spdlog中使用`{}`(里面可指定格式)作为格式化符 + +以下方式把日志输出到默认logger上: + +```text +//#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG +#include "spdlog/spdlog.h" + +int main() +{ + spdlog::info("{:<30}", "left aligned"); + spdlog::warn("Easy padding in numbers like {:08d}", 12); + spdlog::error("Some error message with arg: {}", 1); + spdlog::critical("Support for int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}", 42); + + spdlog::set_level(spdlog::level::debug); // Set global log level to debug + spdlog::debug("This message should be displayed.."); + + // change log pattern + spdlog::set_pattern("[%H:%M:%S %z] [%n] [%^---%L---%$] [thread %t] %v"); + + // Compile time log levels + // define SPDLOG_ACTIVE_LEVEL to desired level + SPDLOG_TRACE("Some trace message with param {}", 42); + SPDLOG_DEBUG("Some debug message"); +} +``` + +### stdout日志 + +以彩色方式输出到标准输出设备上: + +```text +#include "spdlog/spdlog.h" +#include "spdlog/sinks/stdout_color_sinks.h" + +void stdout_example() +{ + // create color multi threaded logger + auto console = spdlog::stdout_color_mt("console"); + auto err_logger = spdlog::stderr_color_mt("stderr"); + spdlog::get("console")->info("loggers can be retrieved from a global registry using the spdlog::get(logger_name)"); +} +``` + +### 文件日志 + +### 基本文件 + +最简单的日志文件: + +```text +#include "spdlog/sinks/basic_file_sink.h" +void basic_logfile_example() +{ + try + { + auto logger = spdlog::basic_logger_mt("basic_logger", "logs/basic-log.txt"); + } + catch (const spdlog::spdlog_ex &ex) + { + std::cout << "Log init failed: " << ex.what() << std::endl; + } +} +``` + +### 循环文件 + +日志文件超过指定大小后,自动生成一个新的;并且只保留最多指定数量的日志文件: + +```text +#include "spdlog/sinks/rotating_file_sink.h" +void rotating_example() +{ + // Create a file rotating logger with 5mb size max and 3 rotated files + auto max_size = 1024*1024 * 5; + auto max_files = 3; + auto logger = spdlog::rotating_logger_mt("some_logger_name", "logs/rotating.txt", max_size, max_files); +} +``` + +### 每日文件 + +每天指定时间生成一个新的日志文件: + +```text +#include "spdlog/sinks/daily_file_sink.h" +void daily_example() +{ + // Create a daily logger - a new file is created every day on 2:30am + auto logger = spdlog::daily_logger_mt("daily_logger", "logs/daily.txt", 2, 30); +} +``` + +## 示例 + +设定默认日志记录文件并在不同地方获取使用: + +```text +#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_INFO + +#include "spdlog/spdlog.h" +#include "spdlog/sinks/rotating_file_sink.h" + + +void writeLog(int n) { + for (int i = 0; i < n; ++i) { + // 获取logger后输出日志 + auto myLogger = spdlog::get("baseLogger"); + myLogger->info("{}: Hello, {}!", i + 1, "World"); + myLogger->info("Welcome to spdlog!"); + myLogger->error("Some error message with arg: {}", 1); + + // 带文件名与行号的日志输出 + SPDLOG_LOGGER_INFO(myLogger, "Support for floats {:03.2f}", 1.23456); + SPDLOG_LOGGER_WARN(myLogger, "Easy padding in numbers like {:08d}", 12); + + // 输出到默认日志中 + spdlog::critical("Support for int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}", 42); + spdlog::error("Some error message with arg: {}", 1); + spdlog::warn("Easy padding in numbers like {:08d}", 12); + spdlog::info("Support for floats {:03.2f}", 1.23456); + } +} + +void testSPDLog() { + // 设定日志最大100k,且最多保留10个 + auto myLogger = spdlog::rotating_logger_mt("baseLogger", "logs/basic.log", 1024 * 100, 10); + spdlog::set_default_logger(myLogger); + myLogger->set_pattern("[%Y-%m-%d %H:%M:%S.%e][%l](%@): %v"); // 非通过宏输出的日志%@输出为空 + myLogger->set_level(spdlog::level::info); + + myLogger->info("Hello, {}!", "World"); + + writeLog(10); +} +``` \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3ca9efb --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Tools + +简易工具。 + +# strReplace + +字符串替换。 + +# fileUpdater + +文件更新工具。 \ No newline at end of file diff --git a/ToolBox.h.in b/ToolBox.h.in new file mode 100644 index 0000000..9dca34d --- /dev/null +++ b/ToolBox.h.in @@ -0,0 +1,10 @@ +#ifndef FILEUPDATER_VERSION_H +#define FILEUPDATER_VERSION_H + +#define PROJECT_NAME "@PROJECT_NAME@" +#define VERSION_GIT_COMMIT "@VERSION_GIT_HASH@" +#define VERSION_GIT_BRANCH "@VERSION_GIT_BRANCH@" +#define CMAKE_RUNTIME_OUTPUT_DIRECTORY "@CMAKE_RUNTIME_OUTPUT_DIRECTORY@" +#define LIBRARY_OUTPUT_PATH "@LIBRARY_OUTPUT_PATH@" + +#endif // FILEUPDATER_VERSION_H \ No newline at end of file diff --git a/fileUpdater/CMakeLists.txt b/fileUpdater/CMakeLists.txt new file mode 100644 index 0000000..d132d3b --- /dev/null +++ b/fileUpdater/CMakeLists.txt @@ -0,0 +1,22 @@ +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() + +find_package(cxxLibrary CONFIG REQUIRED) + +add_executable(fileUpdater main.cpp) +target_link_libraries(fileUpdater PRIVATE cxxLibrary::cxxLibrary) + +include(GNUInstallDirs) +install(TARGETS fileUpdater + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) diff --git a/fileUpdater/main.cpp b/fileUpdater/main.cpp new file mode 100644 index 0000000..eb8aa27 --- /dev/null +++ b/fileUpdater/main.cpp @@ -0,0 +1,512 @@ +#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 notice = "使用特殊参数 default 可生成一个默认配置文件模板。"; + auto msg = fmt::format("fileUpdater - 文件更新工具 \n\n {} on {} at {} {}\n{}", VERSION_GIT_COMMIT, VERSION_GIT_BRANCH, + __DATE__, __TIME__, notice); + 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(); + + if (argc > 1) { + std::string specialStr = argv[1]; + if (specialStr == "default") { + std::string defaultContent = + R"( + + + + +)"; + std::ifstream ifs("fileUpdater.xml"); + if (!ifs) { + std::ofstream ofs("fileUpdater.xml"); + ofs << defaultContent; + ofs.close(); + } + ifs.close(); + boost::nowide::cout << "已生成默认配置文件: fileUpdater.xml" << std::endl; + return 0; + } + } + + AutoUpdateTool tool; + + if (tool.parseArguments(argc, argv)) { + return tool.run(); + } else { + return 1; + } +}